<返回更多

基于Redis实现的分布式锁知识点总结

2020-11-09    
加入收藏

应用场景

分布式系统中,面对高并发场景,又对数据一致性有一定要求的情况下,使用分布式锁。例如商城中下单扣库存这种情况。

解决方案

基于数据库

例如:

select * from mall_spu where id=111 for update

例如:专门建一张表用来实现。例如以类名、方法名、数据ID作为唯一主键,org.leo.mall.order.OrderServer.addOrder.skuId.111,方法执行的时候,如果能插入成功,代表拿到锁,如果报主键冲突,则拿锁失败。


基于Zookeeper

以类、方法、数据ID作为目录,请求取顺序节点&节点列表,如果自己的节点最小,说明拿锁成功。而且还可以通过watch,在锁释放的时候重新拿锁。因为是临时锁,所以主动释放,或者session失效都可以释放锁,避免死锁产生。

性能差点,因为Zookeeper的操作都在主节点上。


基于redis

本文主要讲讲应用的一些变革。

1、加锁。

原来的做法是:

public static boolean getLock(String key,int expireTime){
 Long result=RedisClient.setnx(key,"");
 if(result!=1){return false}
 RedisClient.expire(key,expireTime);
 return true;
}

setnx加锁,成功后用expire加上超时时间。

问题在于:sennx和expire不是原子操作,万一expire的时候崩了,这条命令永远不过期了。

所以后来基于Redis的升级,有了下面正确的加锁方法:

public static boolean getLock(String key,String requestId,int expireTime){
 String result=RedisClient.set(key,requestId,"NX","PK",expireTime);
 if(result.equals("OK")){return true;}
 return false;
}

其实就是用Redis提供的一条set命令,替代了前面的setnx、expire两条命令,保证了原子性。

NX是指Key不存在就新增。PX是指设置超时时间。

requestId是为了后面解锁用。

2、解锁

解锁看着最简单,其实蛮复杂。

脑子里第一想法就是:

public static void releaseLock(String key){
 RedisClient.del(key);
}

这个危险性在于任何人都可以解锁!比如A请求加了锁:spu_id_111。B请求也要对111进行操作,一看锁被占了,直接del,然后自己拿锁——虽然在程序开发上讲,没有哪个傻子会这么干!!

所以这才有了第二种做法:

A请求加锁的时候,通过UUID、Random等方法生成随机数requestId。

public static void releaseLock(String key,String requestId){
  String result=RedisClient.get(key);//步骤1
  if(result.equals(requestId)){//步骤2
   //二者相等,说明加解锁的请求是同一个
   RedisClient.del(key);//步骤3
 }
}

看似很严谨,但是问题出在哪呢?还是出在操作不是原子性上。

A请求执行步骤1、2完毕,还未执行步骤2时,锁过期了,自动解锁!这时B请求加锁必然成功,而A请求继续执行步骤3,把B请求的锁给删了。

正确的做法如下:

public static boolean releaseLock(String key,String requestId){
 String luaCommand="if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
 List<String> keyList=Lists.newArrayList(key);//这里我用的是Guava
List<String> valList=Lists.newArrayList(requestId);
 Object result=RedisClient.eval(luaCommand,keyList,valList);
 if(result.equals(1L)){return true;}
 return false;
}

利用的就是Redis通过eval命令执行LUA脚本是原子性的特性。


再然后就是使用Redisson实现了,这个适用于集群部署的Redis。

我在实际使用Redis分布式锁的时候遇到过一种情况。使用分布式锁后,要调用第三方接口,从而导致整个流程时间偏长,锁过期的情况下还没有执行完,当时的处理方式是加大了过期时间。

如果使用Redisson,因为有看门狗机制,就很好地解决了这个问题。看门狗会定时去检查,如果请求实例还在则自动去延长超时时间。不过这带来的问题一定是性能的下降,所以当时我们还是采用了粗暴的延长设置过期时间来解决此类问题。

Redisson也是个可重入锁,因为锁的内容除了key、实例ID之外还有数字Value,这样一来同样的实例多次拿锁,Value+1,释放锁,Value-1即可。

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>