Redis 实现分布式锁及原理 —— RedisTemplate 、Redission

338 阅读3分钟

实现分布式锁的关键点

  • 互斥

    • 在分布式高并发的条件下,我们最需要保证,同一时刻只能有一个线程获得锁,这是最基本的一点。
  • 原子性

    • 加锁和给锁设置过期时间需要保证原子性,否则可能出现死锁。例如,一个线程获取锁之后由于系统故障等无法执行释放锁的命令,导致其它线程都无法获得锁。
  • 性能

    • 对于访问量大的共享资源,需要考虑减少所等待的时间,避免导致大量线程阻塞。
    • 锁的颗粒度要尽量小。比如你要通过锁来减库存,那这个锁的名称可以设置成是商品的ID,而不是任取名称。
    • 锁的范围尽量要小。比如只要锁2行代码就可以解决问题的,那就不要去锁10行代码了。
  • 重入

    • 同一个线程可以重复拿到同一个资源的锁,重入锁非常有利于资源的高效利用。

分布式锁的实现

RedisTemplate 实现分布式锁

@Autowired
private RedisTemplate<String, Object> redisTemplate;
    
public void test() {
    boolean isSuccess = false;
    try {
        isSuccess = redisTemplate.opsForValue().setIfAbsent(key, "", 30, TimeUnit.SECONDS);
        if (!isSuccess) {
            log.warn("获取锁失败, key: {}", key);
            return;
        }
        logger.info("业务逻辑操作");
    } catch (Exception err) {
        log.error("发生异常: {}", err);
    } finally {
        if (isSuccess) {
            redisTemplate.delete(lockUserBehaviorKey);
        }
    }
}

RedisTemplate 分布式锁的加锁机制:基于 Redis 的 SET key value EX seconds NX 实现原子操作,当key不存在的时候写入value值并设置过期时间。

问题点:当第一个线程业务处理时间超过锁设置的过期时间时,第二个线程也获得锁进入,然后第一个线程结束后还会释放点第二个线程的锁,连锁反应,导致没法达到互斥的效果。不支持重入。

Redission 实现分布式锁

@Autowired
private RedissonClient redissonClient;

public void test() {
    boolean isSuccess = false;
    // 获取指定的分布式锁对象
    RLock lock = redissonClient.getLock(key);
    try {
        // 第一种, 具有 Watch Dog 自动延期机制, 默认每隔30/3=10秒续期, 续到30s
        isSuccess = lock.lock();

        // 第二种, 尝试拿锁10s后停止重试, 返回false, 具有 Watch Dog 自动延期机制, 默认续30s
        // isSuccess = lock.tryLock(10, TimeUnit.SECONDS);
        
        // 第三种, 没有 Watch Dog, 10s后自动释放
        // isSuccess = lock.lock(10, TimeUnit.SECONDS);
        
        // 第四种, 尝试拿锁100s后停止重试, 返回false, 没有 Watch Dog, 10s后自动释放
        // isSuccess = lock.tryLock(100, 10, TimeUnit.SECONDS);
        
        // 第五种, 非阻塞式获取锁
        // isSuccess = lock.tryLock();
        
        if (!isSuccess) {
            log.warn("获取锁失败, key: {}", key);
            return;
        }
        
        logger.info("业务逻辑操作");
    } finally {
        // 释放锁
        lock.unlock();
    }
}

Redission 分布式锁的加锁机制

Redisson 分布式锁的加锁机制.png

如何解决集群情况下分布式锁的可靠性

Redis 集群下,分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点是异步的,如果在 Redis 主节点获取到锁后,在没有同步到从节点时,Redis 主节点宕机了,此时从节点变成新的主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

针对这个问题,Redis 之父 antirez 设计了 Redlock 算法来解决。

Redlock 算法的思想是让客户端向 Redis 集群中的多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

即使部分 Redis 节点出现问题,只要保证 Redis 集群中有半数以上的 Redis 节点可用,分布式锁服务就是正常的。

Redlock 是直接操作 Redis 节点的,并不是通过 Redis 集群操作的,这样才可以避免 Redis 集群主从切换导致的锁丢失问题。

Redlock 实现比较复杂,性能比较差,发生时钟变迁的情况下还存在安全性隐患。

实际项目中不建议使用 Redlock 算法,成本和收益不成正比。

如果不是非要实现绝对可靠的分布式锁的话,其实单机版 Redis 就完全够了,实现简单,性能也非常高。