redis实现分布式锁
为什么要用分布式锁?
在平常开发业务中,肯定遇到过对共享资源多线程访问,这时候就需要加锁保证业务的正确性。单机环境下这样做没问题,因为多线程都能获取到jvm中的锁(单机环境)。随着业务发展,机器开始做集群,分布式部署保证高可用,那么问题来了,多个jvm之间是不能通讯的,那么按照前面说的加锁就有问题,因为每个jvm的锁是只存在自及的jvm中。要解决这个问题,需要每台机器都能获取到同一把锁也就是分布式锁。
redis实现分布式锁
举个秒杀活动列子:用户抢购商品,获取到库存,检查库存,如果库存不足返回,库存充足,处理扣除逻辑。
public Boolean purchase(){
Integer count = Integer.parseInt(stringRedisTemplate.opsForValue().get("commodity"));
if(count <=0){
System.out.println("商品已卖完");
return false;
}
System.out.println("购买成功,处理逻辑");
stringRedisTemplate.opsForValue().set("commodity",String.valueOf(count-1));
return true;
}
这段代码有什么问题呢?在并发量不高的情况下,是能满足需求的,但如果并发量高的话,就会存在超卖的现象。 原因分析:在并发情况下,线程1先获取到库存,发现库存充足,执行扣除逻辑,在线程1还没有执行到删库存的时候,线程2也获取库存,这时候线程2获取到和线程1还没扣减的库存,这就导致了超卖。解决思路:就是加锁
tips: Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。
上面代码修改为:
public Boolean purchase(){
String lockName ="lock";
//获取锁
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockName, "lock");
if(!lock){
System.out.println("获取锁失败");
return false;
}
try {
Integer count=Integer.parseInt(stringRedisTemplate.opsForValue().get("commodity"));
if (count <= 0) {
System.out.println("商品已卖完");
return false;
}
System.out.println("购买成功,处理逻辑");
stringRedisTemplate.opsForValue().set("commodity", String.valueOf(count - 1));
}catch (Exception e){
return false;
}finally {
//释放锁
stringRedisTemplate.delete(lockName);
}
return true;
}
分析:修改后的代码有什么问题呢?如果一个线程获取到锁后,以为一些其他原因,比如:比如断电,服务器重启了导致没有执行释放锁操作,其他线程也一直获取不到锁,那么这把锁会变成死锁。解决思路:给key加一个过期时间
tips: SETNX key <current Unix time + lock timeout + 1> 命令在指定的 key 不存在时,为 key 设置指定的值和过期时间
public Boolean purchase(){
String lockName ="lock";
//获取锁,加上过期时间
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockName, "lock",10, TimeUnit.SECONDS);
if(!lock){
System.out.println("获取锁失败");
return false;
}
try {
Integer count=Integer.parseInt(stringRedisTemplate.opsForValue().get("commodity"));
if (count <= 0) {
System.out.println("商品已卖完");
return false;
}
System.out.println("购买成功,处理逻辑");
stringRedisTemplate.opsForValue().set("commodity", String.valueOf(count - 1));
}catch (Exception e){
return false;
}finally {
//释放锁
stringRedisTemplate.delete(lockName);
}
return true;
}
分析:上面代码有什么问题呢?假如线程1获取到了锁,执行处理逻辑的时间超过了10秒,redis主动吧key过期了,线程2过来获取到了锁,线程2开始获取库存执行执行操作,执行过程中,线程1开始执行了删除key操作,然后线程3获取了锁也开始获取库存执行操作,这样也要导致超卖的问题。解决思路:吧当前线程设置一个Id设置到锁的值中,让线程只能删除线程自己获取的锁。
public Boolean purchase(){
String uuid = UUID.randomUUID().toString();
String lockName ="lock";
//获取锁
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockName, uuid,10, TimeUnit.SECONDS);
if(!lock){
System.out.println("获取锁失败");
return false;
}
try {
Integer count=Integer.parseInt(stringRedisTemplate.opsForValue().get("commodity"));
if (count <= 0) {
System.out.println("商品已卖完");
return false;
}
System.out.println("购买成功,处理逻辑");
stringRedisTemplate.opsForValue().set("commodity", String.valueOf(count - 1));
}catch (Exception e){
return false;
}finally {
//释放锁
if(uuid.equals(stringRedisTemplate.opsForValue().get(lockName))) {
stringRedisTemplate.delete(lockName);
}
}
return true;
}
代码写到现在这个位置就差不多了,但锁的过期时间到底设置多少秒?设置大了,如果服务器宕机,重启服务器后锁很久才过期,设置小了怕因为其他以为导致没有执行到释放锁操作。解决思路:在处理业务逻辑任务的时候,开一个新的线程,在新线程中每隔一段时间重新设置过期时间。一般过了设置过期时间的1/3后重新设置过期时间。
思考问题: 以上都是redis在单机的情况下,如redis做了主从,做了集群,从reids服务器还没有同步到主redis服务器的数据, 又会产生什么问题,怎么解决?