关于springboot使用redisTemplate实现分布式锁的问题。

关于springboot使用redisTemplate实现分布式锁的问题。

img

img

代码如上,通过redisTemplate的api里的setIfAbsent给redis加锁,然后用delete去解锁。在buy方法中先加锁然后阻塞十秒模拟业务流程,业务流程以后再解锁。然后打开浏览器,开俩个窗口,模拟俩个线程去调用buy(俩个线程参数key是一样)。理想中的过程是,第一个线程调用的时候,加锁以后然后阻塞的十秒之内,再用第二个窗口模拟第二个线程去调用,然后setIfAbsent返回false,buy方法直接返回lock来表示本次调用失效。想实现这么一个接口幂等性校验的场景。
但是这个场景没有实现,发现了一个问题,在第一个线程调用setIfAbsent以后,然后再阻塞的10秒之内, 第二个线程再用相同的key去调用setIfAbsent,会一直等待第一个线程完全执行完以后,setIfAbsent才会返回。但是再第一个线程执行完以后,已经将该key进行了delete,所以第二个线程调用setIfAbsent的结果也是true,俩个线程都返回了buy,导致无法实现接口幂等性校验的场景。
个人对redis了解的比较少,以前都是用来存放用户的token用,以我个人的直觉猜测可能是redis的事务问题,在第一个线程执行setIfAbsent后,阻塞的十秒时间内,redis一直是事务未提交的状态,然后第二个线程再接着去调用setIfAbsent发现该key未提交事务,会一直等待第一个事务提交完事务以后,再进行setnx操作。但是这个也只是本人对关系型数据库的了解作出的猜测,对非关系行数据库确实了解了解较少,所有在此希望CSDN各位可以为指教一下出现这个问题的原因和解决方案。

您的猜测是正确的。redis的setIfAbsent操作是原子操作,但是如果多个操作需要同时执行才能完成一个完整的事务,那么就需要用到redis的事务管理机制——multi/exec。在您的代码中,没有使用multi/exec将setIfAbsent和delete操作包含在一个事务中,导致第二个线程在等待第一个线程的事务提交后才能获取到锁,然而第一个线程已经将锁释放了,导致第二个线程仍然获取到了锁,并对业务进行了处理。

解决方案是将setIfAbsent和delete操作包含在同一个事务中执行,这样就可以保证原子性,避免出现锁被重复获取的情况。代码示例如下:

public void buy(String key) {
    String lock = "lock:" + key;
    if (redisTemplate.opsForValue().setIfAbsent(lock, "value")) {
        redisTemplate.expire(lock, 10, TimeUnit.SECONDS);
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 业务处理代码
    } else {
        // 获取锁失败,处理重复请求的逻辑
        redisTemplate.watch(lock);
        redisTemplate.multi(); // 开启事务
        redisTemplate.delete(lock);
        redisTemplate.exec(); // 提交事务
    }
}