一、Redis分布式锁存在的问题

在我的这篇文章中,提到了使用Redis的setnx的原理,来实现分布式锁,但其实这种方式依旧存在着一些隐患

1.1 误删锁

逻辑说明:

当线程1持有锁并且在进行业务时,业务出现了长时间的阻塞以致于超过了锁的有效时间,导致锁自动释放了。

然后其他线程(假设为线程2)就趁虚而入,获取到了锁并执行业务,执行完业务后,就吧锁给释放了。

这就出现了其他线程释放了原来线程的锁的情况

分析:

出现这种问题的原因是,获取锁和释放锁的时候,我们都是根据锁的key来找到锁,然后释放的。

而我们命名key的时候,只是根据“lock:”+业务名称来命名的,而锁的value,我们是用1来表示锁被占用的。

所以在释放锁的时候,只要是其他线程执行同样的业务时根据一样的key获取到了当前的被超时释放的处于空闲状态的锁,那么他就能释放当前的锁。

所以我们要把value的值,改为线程的ID,然后在释放锁的时候,判断一下根据key获取到的value是否跟当前线程的ID一样

一样:释放锁

不一样:不能释放锁

那么就有人要问了:为什么不是把key 改为“lock:“+业务名称+线程id 呢?

你这样改了,你这个锁还有什么意义,一个锁跟一个线程一一对应,一个锁永远只有一个线程来获取,然后其他线程来执行这个业务的时候又会获得一个新的锁,这样子这个把锁有没有都没什么区别了。因为同一时间内都可以有很多线程来执行这个业务。

这样就完了吗? 在单机环境下,每个线程的ID确实都会不一样,这样确实可以避免误删锁的情况。但是,如果是分布式的环境下呢?

机子1的JVM产生的线ID绝对不会相同,机子2的JVM产生的线程ID绝对不会相同,但是机子1产生的线程ID可能会和机子2的线程ID相同,从而又会导致误删锁的情况发生

这时候我们还可以在用一个UUID来拼接value,UUID的生成跟机器码还有时间有关,所以不同机子上产生的UUID绝对不一样。

解决问题:

引入工具依赖以便于产生UUID

         <dependency>
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-all</artifactId>
             <version>5.7.17</version>
         </dependency>

加锁:

 private static final String KEY_PREFIX="lock:"
 private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";//用toString是因为redis里的value存的就是String类型的,所以要转换
 @Override
 public boolean tryLock(long timeoutSec) {
    // 获取线程标示
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    // 获取锁
    Boolean success = stringRedisTemplate.opsForValue()
                 .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
    return Boolean.TRUE.equals(success);
 }

释放锁:

 public void unlock() {
     // 获取线程标示
     String threadId = ID_PREFIX + Thread.currentThread().getId();
     // 获取锁中的标示
     String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
     // 判断标示是否一致
     if(threadId.equals(id)) {
         // 释放锁
         stringRedisTemplate.delete(KEY_PREFIX + name);
     }
 }

1.2 原子性问题

问题说明:

在某种极端的情况下,假设线程1获取了锁,并执行完了业务,在判断value标示和自身线程标示一致后,准备要释放锁前,此时锁突然过期了,此时其他线程(假设为线程2)就会趁虚而入,获取到锁,随后执行线程2的业务,然后线程1又会继续释放锁的代码,这样也会出现误删锁的情况

问题分析:

出现这种问题的主要原因,就是在释放锁的时候,我们没有做到同时判断同时执行释放操作,没有保持原子性。

我们上面的代码中,是先判断value和当前线程的标示是否一致,再释放锁的。有一个先后的顺序,那么就有可能出现其他线程趁虚而入的问题。

那么可能就会有人说,要不然试试@Transactional,不就可以保证一致性了吗。首先,@Transactional并不能保证多条操作同时成功,还是有先后顺序的,事务只是保证了一系列操作要么都成功要么都失败,但是在成功的时候并不会保证都是同时成功,其次,一般@Transactional只用于关系型数据库,redis对事务的支持也不是特别完善。而且这里redis中出现的问题并不是因为redis出现了错误,只是操作之间有间隙,导致其他线程趁虚而入了。

所以我们一般使用lua脚本保证redis的多条命令的一致性

解决问题:

一般我们使用lua脚本来保证redis多条命令的原子性。即把多条命令当成一个整体执行,只要一条执行不成功那么所以命令都不会成功。所有命令都能执行成功的时候,那么所有命令就能同时成功。


文章作者: 落叶知秋
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 落叶知秋
喜欢就支持一下吧