美文网首页
redis分布式锁使用须知

redis分布式锁使用须知

作者: _大豪 | 来源:发表于2018-08-26 21:23 被阅读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 时,客户端知道了事务执行是失败的。

    注意

    • 上面例子中的 multi/exec 对应数据库中的 begin/commit,discard 对应 rollback
    • Redis 禁止在 multi 和 exec 之间执行 watch 指令,而必须在 multi 之前做好盯住关键变量,否则会出错。

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

    相关文章

      网友评论

          本文标题:redis分布式锁使用须知

          本文链接:https://www.haomeiwen.com/subject/dxcaiftx.html