redis分布式锁使用须知

2018-08-26  本文已影响0人  _大豪

最近在规范平台缓存使用时发现,很多业务用到了 reids 分布式锁,但普遍存在一些细节问题,根据这些问题,本文将会尝试去总结分布式锁常见的问题。最后聊聊redis乐观锁。

分布式锁

如果是单机环境,对于并发问题,直接用 java 提供的 synchronized 或 Lock 实现即可,而涉及到多进程环境,那么就需要依赖一个第三方系统来提供锁机制。

redis作为一个缓存中间件系统,就能提供这种分布式锁机制,其本质就是在redis里面占一个坑,当别的进程也要来占坑时,发现已经被占领了,就只要等待稍后再尝试

在java中我们一般这样用:

boolean result = jedis.setnx("lock-key",String.valueOf(System.currentTimeMillis()))== 1L;
if  (result) {
    try {
        // do something
    } finally {
        jedis.del("lock-key");
    }
 }

须知一:潜在死锁

上面程序逻辑存在一个问题,就是如果加锁和解锁中间执行的业务中断,比如服务器挂了,或者线程被杀掉,那么就可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。

那么实际应用中我们应该给锁加上过期时间,比如5秒,这样即使出现上面说的异常,也可以保证5秒后锁会自动释放。所以程序可以优化成如下:

try {
    boolean result = jedis.setnx("lock-key",String.valueOf(System.currentTimeMillis()))== 1L;
    if  (result) {
        jedis.expire("lock-key",5);
        // do something
    }
} finally {
    jedis.del("lock-key");
}

这样写也存在一个问题:由于 setnx 和 expire 非原子性,如果在 setnx 和 expire 之间出现机器挂掉或者是被人为杀掉,就会导致死锁。

加锁正确姿势:

String result = jedis.set("lock-key", String.valueOf(System.currentTimeMillis(), "NX", "PX", 5);
if ("OK".equals(result)) {
    return true;
}
return false;

须知二:超时问题

Redis 分布式锁并不能解决超时问题,其实基于 ZooKeeper 实现的分布式锁也没办法避免超时问题。

考虑如下场景,加锁和解锁之间的业务非常耗时,那么就可能存在:

当然这是 redis 分布式锁在死锁和超时问题之间做出的妥协,没办法完全避免,但是需要业务在使用时,衡量加锁的粒度及过期时间

须知三:可重入性

可重入性是指线程在持有锁的情况下,再次请求持有同一把锁,那么是可以获取到的。在 java 中, synchronized 和 ReentrantLock 都是可重入锁。

redis本身不具备可重入性,如果要支持可重入锁,可以借助 Threadlocal 对请求的 setnx 进行包装,Threadlocal 变量存储当前持有锁的计数。

须知四:集群环境如何保证锁的安全

redis分布式锁在集群环境下,不是绝对的安全的。比如:主节点的锁还没来得及同步到从节点,此时主节点挂了,从节点取而代之。

线程1在主节点已经成功拿到一把锁,此时切到了从节点,这把锁不存在了,此时线程2轻松在从节点取到这把锁,这就导致一把锁被两个线程拿到了。

Redlock 算法

Redlock 算法就是为了解决这个问题,他的原理是在加锁时,向过半节点发送 set 指令,只要过半节点返回成功,那就认为加锁成功。释放锁时,再向所有节点发送 del 指令。

代价也很明显,跟tomcat 的 session 共享机制一样,随着集群机器的增加,势必会有损性能。Redlock 算法还需要考虑出错重试时钟漂移等很多细节问题。

所以一般这种由于主从节点同步时间差导致的锁不安全问题,业务系统一般都是选择忍受的,生产上这种场景发生的概率也不大。

redis乐观锁

以上讨论的 reids 分布式锁本质就是使用 setnx 指令实现占位功能,所以这种分布式锁是一种悲观锁,我们也可以借助 redis 的 watch 指令实现乐观锁。

实现原理

结合 redis 事务,watch 会在事务开始之前盯住某个变量,当事务执行提交执行时,redis 会自动检查被watch的变量,是否被修改过了,如果变量被修改过,事务提交指令 exec 会返回 null 告知客户端事务执行失败。

举例

场景:redis 存储了用户金额,现在有两个并发请求改账户额度,业务实现上需要获取到金额,再修改金额,最后写入 redis 。

> set account 100
> watch account
OK
> set account  50 # 事务执行过程中被修改
OK
> multi # 开始事务
OK
> incr account # 账户额度+1
QUEUED
> exec  # 事务提交,返回失败
(nil)

当 exec 指令返回一个 null 时,客户端知道了事务执行是失败的。

注意

参考:
Redis 分布式锁的正确实现方式( Java 版 )
Redis 设计与实现 -- 事务

上一篇下一篇

猜你喜欢

热点阅读