分布式环境下利用Redis实现分布式锁
在某些高并发的业务场景下,例如秒杀、选课等系统,为了避免出现商品超卖、选课人数超出课程规定人数的问题发生,读写数据库时需要进行加锁操作,保证某时刻已有一个用户在操作。在Java单机应用中,直接使用synchronized关键字没有任何毛病,但在分布式系统中就不行了,这时就需要引入分布式锁来解决问题。分布式锁可以用Zookeeper或Redis来实现,本文重点讲解使用Redis实现分布式锁。
在某些高并发的业务场景下,例如秒杀、选课等系统,为了避免出现商品超卖、选课人数超出课程规定人数的问题发生,读写数据库时需要进行加锁操作,保证某时刻已有一个用户在操作。在Java单机应用中,直接使用synchronized关键字没有任何毛病,但在分布式系统中就不行了,这时就需要引入分布式锁来解决问题。分布式锁可以用Zookeeper或Redis来实现,本文重点讲解使用Redis实现分布式锁。
在Redis中有一个SETNX命令,如果key在Redis中存在,返回0,否则返回1,执行成功。下面是一种错误的实现分布式锁的思路,最后我会写一个正确的实现分布式锁的代码,请大家参考
错误的分布式锁思路
假如客户1需要加锁时执行SETNX("key","value")返回1加锁成功,其余客户执行SETNX返回0没有获取锁,加锁失败会一直监听key并循环执行SETNX("key","value"),直到返回1。当客户1需要释放锁时需要执行DEL("key")释放锁。但这里有一个问题,如果客户1所在的进程挂掉后,key永远不会被删除,其余客户执行SETNX("key" ,"value")永远返回0,产生死锁,这样的分布式锁思路是错误的。
正确的思路
如果客户进程获得锁后,设置一个超时移除的动作,如expire命令,客户进程挂掉后超时锁自动释放,其他客户任然可以获得锁。下面我用多线程来模拟高并发下的分布式锁的获取、释放。
客户端获得锁:
将k v通过SETNX命令存入Redis,如果返回1则表明Redis没有这条数据,操作成功获得锁,如果返回0则表明Redis中有这条 数据,其他客户端占有锁,获得锁失败,进入等待状态。
RedisThread.java
/**
* 获得锁
*/
public boolean getLock(){
Jedis jedis = pool.getResource();
Long ret = jedis.setnx("k", "v");
//ret==1,获得锁
if(ret == 1){
System.out.println(Thread.currentThread().getName()+":lock");
//如果某个客户端挂掉没有执行del操作,则设置超时时间释放锁,否则会死锁。
jedis.expire("k",10);
jedis.close();
return true;
}
//没有获得锁
jedis.close();
return false;
}
模拟一个客户端
如果getLock()方法返回true,则表明获得锁,执行后面的业务逻辑,最后通过DEL(k)命令释放锁。
如果getLock()方法返回false,循环持续监听(当然也可以通过Thread.sleep()控制监听频率),直到获得锁后执行业务逻辑最后释放锁。
RedisThread.java
/**
* 模拟客户端
* @throws Exception
*/
public void client() throws Exception{
boolean b = getLock();
//如果没有获得锁,持续监听
while(!b){
b = getLock();
}
System.out.println(Thread.currentThread().getName()+"执行业务开始");
Long startTime = System.currentTimeMillis();
Thread.sleep(new Random().nextInt(1000));
System.out.println(Thread.currentThread().getName()+"执行业务结束,共用时"+(System.currentTimeMillis()-startTime)+"ms");
Jedis jedis = pool.getResource();
jedis.del("k"); //unlock
jedis.close();
}
开启多个线程模拟高并发
RedisThread.java
@Override
public void run() {
try {
//调用客户端
client();
} catch (Exception e) {
e.printStackTrace();
}
}
RedisLock.java 测试
开启50个线程模拟50个客户(50个服务组成的集群)
public class RedisLock {
public static void main(String[] args) {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(50);
config.setMaxTotal(50);
JedisPool pool = new JedisPool(config, "192.168.1.104", IpService.REDIS_PORT);
Thread[] t = new Thread[50];
for(int i = 0;i<t.length;i++){
t[i] = new Thread(new RedisThread(pool));
}
for(int i = 0;i<t.length;i++){
t[i].start();
}
}
}
输出结果如下:
lock-->业务开始-->业务结束-->unlock--lock-->.......
Thread-52:lock
Thread-52执行业务开始
Thread-52执行业务结束,共用时802ms
Thread-52unlock
Thread-13:lock
Thread-13执行业务开始
Thread-13执行业务结束,共用时65ms
Thread-13unlock
Thread-45:lock
Thread-45执行业务开始
Thread-45执行业务结束,共用时773ms
Thread-45unlock
Thread-4:lock
Thread-4执行业务开始
Thread-4执行业务结束,共用时860ms
Thread-4unlock
Thread-33:lock
Thread-33执行业务开始
Thread-33执行业务结束,共用时127ms
Thread-33unlock
Thread-20:lock
Thread-20执行业务开始
Thread-20执行业务结束,共用时510ms
Thread-20unlock
Thread-40:lock
Thread-40执行业务开始
Thread-40执行业务结束,共用时450ms
Thread-40unlock
更多推荐
所有评论(0)