Redis-学-01-分布式锁分析

Redis分布式锁方案剖析

参考1
参考2
参考3

分布式锁的原则

  1. 独享:即互斥属性,在同一时刻,一个资源只能有一把锁被一个客户端持有
  2. 无死锁:当持有锁的客户端奔溃后,锁仍然可以被其它客户端获取
  3. 容错性:当部分节点失活之后,其余节点客户端依然可以获取和释放锁
  4. 统一性:即释放锁的客户端只能由获取锁的客户端释放

方案一

使用 Redis 实现分布式锁最简单的方案是获取锁之前先查询该锁为 key 对应的 value 是否存在。如果存在,则说明该锁被其他客户端获取了,否则就尝试获取锁。获取锁的方法是,以该锁为 key,设置一个随机的值就可以。

获取锁的伪代码如下:

public boolean getLock(String key) {
    // 如果 key 存在,返回 false,获取锁失败
    if(existsKey(key)) {
        return false;
    }else {
        // key 不存在,设置 key,返回 true,获取锁成功
        setKey(key);
        return true;
    }
}

释放锁的伪代码如下:

public void bussiness(String key) {
    boolean lock = false;
    try{
        // 获取锁
        lock = getLock(key); 
        if(lock) {
            // 处理业务
            doSomeThing();
        }
    }finally{
        if(lock) {
            // 释放锁,删除对应的 key,value
            releaseLock(key);
        }
    }
}

以上的写法的好处是在使用锁的过程中出现异常,也能顺利释放锁。

风险与不足

  1. 如果代码执行到 doSomeThing() 方法时,服务器宕机,这个时候 finally 代码就无法执行,导致锁无法释放而造成死锁。解决方式为 Redis 设置 key 时可以指定一个过期时间,即使服务器宕机也可以保证锁及时释放。
  2. 获取到的锁不一定是排他锁,也就是说同一把锁同一时间可能被不同客户端获取到。getLock() 方法不是原子性的,当一个客户端检查到锁不存在时,并在执行 setKey() 之前,别的客户端同样检查该锁不存在,也会执行 setKey() 方法。这样就会有两个客户端同时获取锁。

方案二

Redis 提供了一个只有在 key 不存在的情况下才会设置 key 的值的原子命令,该命令也能设置 key 值过期时间,因此使用该命令,不会出现方案一出现的两个问题。

该命令为:
SET key value NX PX milliseconds。 NX 表示只有当键 key 不存在时才会设置 key 的值,PX 表示设置键过期时间,单位是毫秒。

伪代码如下:

public boolean getLock(String key, long timeout) {
    return setKeyOnlyIfNotExists(key, timeout);
    // redisTemplate.opsForValue().setIfAbsent(key,value)
}

其中,setKeyOnlyIfNotExists() 方法表示的是原子命令 SET key value NX PX milliseconds。现在这样获取锁是原子性的操作没问题了,但是释放锁出现了新的问题。

风险与不足

  1. 客户端A 获取锁设置 key 的过期时间 2s,客户端A获取锁后,执行业务 doSomeThing() 方法时间为 3s(大于2s),当执行完业务逻辑后,客户端A获取的锁已经被 Redis 过期机制释放掉了。所以客户端A执行完 doSomeThing() 方法后,执行 releaseLock() 释放锁时,可能释放的是客户端B获取的锁。

方案三

方案二出现客户端A释放客户端B获取的锁的情况,我们可以设置 key的时候,将value设置成一个唯一值,当释放锁,也就是删除key时,先比较key是否为之前设置的值,只有两者相等时才删除key。

伪代码如下:

public void releaseLock(String key, String value) {
    // 比较 value是否一致
    if(getKey(key) == value) {
        // 根据key删除
        deleteKey(key);
    }
}

风险与不足

  1. 上述释放锁的操作不是原子性的,意味着getKey 方法和 deleteKey 方法之间,其他客户端是可以执行其他命令的。假设客户端A执行完 getKey() 方法,并且value 也和之前一致,准备执行 deleteKey 时,假设由于网络或其他原因导致客户端A在执行完 getKey之后过了1s才执行 deleteKey,在这1s中,该key可能因为过期被Redis清除了。这样客户端B,有可能在这期间获取锁,然后客户端A执行 deleteKey 误释放别的客户端的锁。

方案四

方案三的问题是因为释放锁的方法不是原子性操作导致的,那么我们只要保证释放锁的代码是原子性就能解决问题。我们在此使用Lua脚本来保证操作的原子性。

Lua基本伪代码如下:

if redis.call("get", KEYS[1]) == ARGV[1] then 
    return redis.call("del", KEYS[1])
else
    return 0
end

其中 ARGV[1] 表示设置 key时指定的全局唯一的随机值。由于Lua脚本的原子性,在Redis执行该脚本时,其他客户端的命令都需要等待该Lua脚本执行完毕才能执行。所以使用Lua脚本来进行锁的释放就相对完善了。

风险与不足

  1. 上述分布式的实现方案中,都是针对 Redis 单节点而言。在实际生产环境中,通常都是Redis集群和主从节点,由于Redis的主从节点复制是异步的,假设客户端A获取锁后,Redis 数据没有同步到从节点,主节点宕机,从节点变成主节点,这个时候客户端A获取的锁就处于无效状态,客户端B照样可以获取锁。
  2. 当 redis 的架构如上图所示一样是单实例模式时,如果存在主备且可以忍受小概率的锁出错,那么就可以直接使用上述代码,当然最严谨的方式还是使用官方的 Redlock 算法实现。其中 Java 包推荐使用 redisson。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值