美文网首页
Redis - 应用1 分布式锁

Redis - 应用1 分布式锁

作者: 欢喜的看着书 | 来源:发表于2023-06-12 20:24 被阅读0次

    已经好久没有更新过简书了,摸鱼了好久,不行,咱得进步, 把印象笔记的存货发一下。

    分布式应用进行逻辑处理时经常会遇到并发问题。

    比如一个操作要修改用户的状态,修改状态需要先读出用户的状态,在内存里进行修

    改,改完了再存回去。如果这样的操作同时进行了,就会出现并发问题,因为读取和保存状

    态这两个操作不是原子的。(Wiki 解释:所谓原子操作是指不会被线程调度机制打断的操

    作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch 线程切换。)

    这个时候就要使用到分布式锁来限制程序的并发执行。Redis 分布式锁使用非常广泛,

    它是面试的重要考点之一,很多同学都知道这个知识,也大致知道分布式锁的原理,但是具

    体到细节的使用上往往并不完全正确。

    分布式锁

    分布式锁本质上要实现的目标就是在Redis 里面占一个“茅坑”,当别的进程也要来占 时,发现已经有人蹲在那里了,就只好放弃或者稍后再试。

    占坑一般是使用setnx(set if not exists) 指令,只允许被一个客户端占坑。先来先占, 用 完了,再调用del 指令释放茅坑。

    // 这里的冒号:就是一个普通的字符,没特别含义,它可以是任意其它字符,不要误解

    > setnx lock:codehole true OK

    ... do something critical ... > del lock:codehole (integer) 1

    但是有个问题,如果逻辑执行到中间出现异常了,可能会导致del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。

    于是我们在拿到锁之后,再给锁加上一个过期时间,比如5s,这样即使中间出现异常也 可以保证5 秒之后锁会自动释放。

    > setnx lock:codehole true OK

    > expire lock:codehole 5 ... do something critical ... > del lock:codehole (integer) 1

    但是以上逻辑还有问题。如果在setnx 和expire 之间服务器进程突然挂掉了,可能是因 为机器掉电或者是被人为杀掉的,就会导致expire 得不到执行,也会造成死锁。

    这种问题的根源就在于setnx 和expire 是两条指令而不是原子指令。如果这两条指令可 以一起执行就不会出现问题。也许你会想到用Redis 事务来解决。但是这里不行,因为expire 是依赖于setnx 的执行结果的,如果setnx 没抢到锁,expire 是不应该执行的。事务里没有if- else 分支逻辑,事务的特点是一口气执行,要么全部执行要么一个都不执行。

    为了解决这个疑难,Redis 开源社区涌现了一堆分布式锁的library,专门用来解决这个问题。实现方法极为复杂,小白用户一般要费很大的精力才可以搞懂。如果你需要使用分布式锁, 意味着你不能仅仅使用Jedis 或者redis-py 就行了,还得引入分布式锁的library。

      

    业务服务器宕机无法触发执行

    为了治理这个乱象,Redis 2.8版本中作者加入了 set指令的扩展参数,使得 setnx和 expire指令可以一起执行,彻底解决了分布式锁的乱象。从此以后所有的第三方分布式锁 library可以休息了。
    > set lock:codehole true ex 5 nx OK ... do something critical ... > del lock:codehole
    上面这个指令就是 setnx和 expire组合在一起的原子指令,它就是分布式锁的 奥义所在。

    超时问题

    Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行的太长,以至于超出了锁的超时限制,就会出现问题。因为这时候锁过期了,第二个线程重新持有了这把锁, 但是紧接着第一个线程执行完了业务逻辑,就把锁给释放了,第三个线程就会在第二个线程逻 辑执行完之间拿到了锁。

    为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了,数据出现的小波错乱可能需要人工介入解决。

    有一个更加安全的方案是为set 指令的value 参数设置为一个随机数,释放锁时先匹配 随机数是否一致,然后再删除key。但是匹配value 和删除key 不是一个原子操作,Redis 也 没有提供类似于delifequals 这样的指令,这就需要使用Lua 脚本来处理了,因为Lua 脚本可以保证连续多个指令的原子性执行。

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

    可重入性

    可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加 锁,那么这个锁就是可重入的。比如Java 语言里有个ReentrantLock 就是可重入锁。Redis 分布式锁如果要支持可重入,需要对客户端的set 方法进行包装,使用线程的Threadlocal 变量 存储当前持有锁的计数。

    相关文章

      网友评论

          本文标题:Redis - 应用1 分布式锁

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