redis分布式锁在实际开发过程中的重要性,不言而喻,设计高并发的业务场景,redis几乎都会登场。
今天给大家分享一下redis分布式锁的几大坑吧😂
1、锁未被释放
请看下面一段代码
1 /**
2 * @author fu JC
3 * @description 扣减库存
4 * @date 2020/4/21 12:10
5 */
6 public String stockLock() {
7 RLock lock = redissonClient.getLock("stockLock");
8 try {
9 /**
10 * 获取锁
11 */
12 if (lock.tryLock(10, TimeUnit.SECONDS)) {
13 /**
14 * 查询库存数
15 */
16 Integer stock = Integer.valueOf(stringRedisTemplate.opsForValue().get("stockCount"));
17 /**
18 * 扣减库存
19 */
20 if (stock > 0) {
21 stock = stock - 1;
22 stringRedisTemplate.opsForValue().set("stockCount", stock.toString());
23 LOGGER.info("库存扣减成功,剩余库存数量:{}", stock);
24 } else {
25 LOGGER.info("库存不足~");
26 }
27 } else {
28 LOGGER.info("未获取到锁业务结束..");
29 }
30 } catch (Exception e) {
31 LOGGER.info("处理异常", e);
32 }
35 return "ok";
36 }
这是正常的商场扣减库存的redis分布式锁,方式超卖现象的发生。但细心的朋友们就会看到,在结尾时未正常释放锁,导致redis线程池被打满,redis服务大面积故障,可能会造成库存数据扣减混乱。
解决的方法也很简单,只要我们细心一点,拿到锁的线程处理完业务及时释放锁,如果是重入锁未拿到锁后,线程可以释放当前连接并且sleep一段时间。
2、B的锁被A释放了
redis实现锁的原理是setnx命令,当 key不存在时将 key的值设为 value ,返回值为 1;若给定的 key已经存在,则 SETNX不做任何动作,返回值为 0 。
1 SETNX key value
但是来假设一个场景,当A、B两个线程尝试给key 加锁的时候,A先拿到锁(假设锁3秒后过期),然后B在外面尝试拿锁。~到这,其实一点问题都没有
但此时业务逻辑比较耗时,执行时间已经超过redis锁的过期时间,这个时候A就会自动释放锁,然后B线程检查到key不存在,执行setnx命令就也拿到了锁。
当A执行完业务逻辑之后,就会去释放锁,这就导致了B的锁被A给释放了。
解决办法其实很简单,一般我们在每个线程加锁时要带上自己独有的value值来标识,只释放指定value的key,就可以避免上面的情况。
3、数据库事务超时
看下面这段代码:
1 @Transaction
2 public void lock() {
3
4 while (true) {
5 boolean flag = this.getLock(key);
6 if (flag) {
7 insert();
8 }
9 }
10 }
这个方法上面有一个@Transaction注解,当有异常时,就会自动回滚。但是数据库事务是有超时时间限制的,不会无条件的一直等下去。
当我们解析一个大文件1,并且把他写入数据库的时候,如果执行时间太长,就会造成超时,事务就会回滚。
一旦你的key长时间获取不到锁,获取锁等待的时间远超过数据库事务超时时间,程序就会报异常。
这个时候,我们就需要将数据库事务改为手动提交、回滚事务。
1 @Autowired
2 DataSourceTransactionManager dataSourceTransactionManager;
3
4 @Transaction
5 public void lock() {
6 //手动开启事务
7 TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
8 try {
9 while (true) {
10 boolean flag = this.getLock(key);
11 if (flag) {
12 insert();
13 //手动提交事务
14 dataSourceTransactionManager.commit(transactionStatus);
15 }
16 }
17 } catch (Exception e) {
18 //手动回滚事务
19 dataSourceTransactionManager.rollback(transactionStatus);
20 }
21 }
4、锁过期了,业务还没有执行完
这种情况和上面的第二种情况比较相似,但解决思路有点不一样。
同样是redis分布式锁过期,而业务逻辑没执行完的场景,不过,这里换一种思路想问题,把redis锁的过期时间再弄长点不就解决了吗?
那还是有问题,我们可以在加锁的时候,手动调长redis锁的过期时间,可这个时间多长合适?业务逻辑的执行时间是不可控的,调的过长又会影响操作性能。
要是redis锁的过期时间能够自动续期就好了。
为了解决这个问题我们使用redis客户端redisson,redisson很好的解决了redis在分布式环境下的一些棘手问题,它的宗旨就是让使用者减少对Redis的关注,将更多精力用在处理业务逻辑上。
redisson对分布式锁做了很好封装,只需调用API即可。
1 RLock lock = redissonClient.getLock("stockLock");
redisson在加锁成功后,会注册一个定时任务监听这个锁,每隔10秒就去查看这个锁,如果还持有锁,就对过期时间进行续期。默认过期时间30秒。
举例子:假如加锁的时间是30秒,过10秒检查一次,一旦加锁的业务没有执行完,就会进行一次续期,把锁的过期时间再次重置成30秒。
5、redis主从复制的坑
redis高可用最常见的方案就是主从复制(master-slave),这种模式也给redis分布式锁挖了一坑。
redis cluster集群环境下,假如现在A客户端想要加锁,它会根据路由规则选择一台master节点写入key mylock,在加锁成功后,master节点会把key异步复制给对应的slave节点。
如果此时redis master节点宕机,为保证集群可用性,会进行主备切换,slave变为了redis master。B客户端在新的master节点上加锁成功,而A客户端也以为自己还是成功加了锁的。
此时就会导致同一时间内多个客户端对一个分布式锁完成了加锁,导致各种脏数据的产生。
至于解决办法嘛,目前看还没有什么根治的方法,只能尽量保证机器的稳定性,减少发生此事件的概率。