• Redis典型应用之分布式锁


    目录

    前言

     分布式锁的基础实现

     引入过期时间:

     引入校验ID:

    引入lua

    lua的简介:

    引入看门狗 (watch dog)

     引入Redlock算法


    前言

    在一个分布式系统中,也会涉及到多个节点同时去访问一个公共资源的时候,此时就需要通过锁来做互斥控制,避免出现类似于“线程安全”的问题~~

    本质上来说就是使用一个公共的服务器,用来记录加锁状态

    当然这个服务器也可以是Redis,也可以是其他的组件(MySQL、ZooKeeper等),自己手搓也行

     分布式锁的基础实现

    其实就是通过一个键值对来标识锁的状态

    例如:买票的时候,存在多个能够购票的软件(12306、携程等)但是票的总数是固定的,所以我们都需要先查询该票的余票是否大于0,如果成立那么票数--

    但是上述的情况会出现“线程安全”问题(访问临界资源)

    在上述的场景的时候就会出现“超卖”的现象~~

    可以在购票服务器中加一个Redis来作为分布式锁的管理器

     

    此时如果买票服务器尝试买票,就需要先访问Redis,在Redis上设置一个键值对。比如key就是车次,value就随意

    如果当前设置成功,就是为当前没有节点对001车次加锁,那么就可以对数据库进行写操作,操作完成之后再把Redis上的键值对给删除掉

    如果在买票服务器1买001车次的票的时候, 买票服务器2也要买001车次的票,此时买票服务器2也会想向Redis中写入key 为 001的键值对,但是此时该key已经存在,那么就会设置失败,那么我们就认为此时其他服务器持有锁,那么买票服务器2就应该等待或放弃~~

    Redis提供了setnx操作,即:key不存在就设置,存在直接失败

    看起来这样就已经实现了分布式锁了吗?? 

     如果此时买票服务器1宕机了怎么办,话句话说,我忘记给del这个key了!!

     引入过期时间:

    为了解决上述的问题,我们可以对key引入一个过期时间,即这个锁有一个默认释放形式

    注意:此处的过期时间的设置务必在设置锁的同时设置上! 

     使用set ex nx 的方式,在设置锁的时候设置上过期时间,不要分成set 与 expire 两个指令~~

    这样已经大致完善了分布式锁的安全使用,但我们仍要考虑一些特殊情况~~

    即购票服务器2的失误操作,将redis中已存在的key删除了~~

     引入校验ID:

    为了解决上述的问题,可以引入校验ID 

    即设置key的时候对其value的值设置成为服务器的编号,比如:key:"001" , value:"服务器1"

    这样就可以在删除key的时候检测一下value是否对应当前执行删除操作的服务器 

     那么伪代码应该如下:

    1. String key = [要加锁的资源 id];
    2. String serverId = [服务器的编号];
    3. // 加锁, 设置过期时间为 10s
    4. redis.set(key, serverId, "NX", "EX", "10s");
    5. // 执⾏各种业务逻辑
    6. // ...
    7. // 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配.
    8. if (redis.get(key) == serverId) {
    9. redis.del(key);
    10. }

    很明显此时get和del这两步操作不是原子的,即使有过期时间也不应该就这样,过期时间只是最后防线 

     

    引入lua

     为了使解锁操作原子,可以使用Redis支持的Lua脚本功能

    lua的简介:

    Lua也是一门编程语言,语法类似JS,是一个动态弱类型语言,Lua的解释器一般用C语言实现,Lua的语法精炼速度快,解释器轻量(200K左右)

    因此Lua经常作为其他程序的内嵌脚本语言,Redis本身就支持Lua作为内嵌脚本

    使用Lua脚本完成上述的解锁功能

    1. if redis.call('get',KEYS[1]) == ARGV[1] then
    2. return redis.call('del',KEYS[1])
    3. else
    4. return 0
    5. end;

     上述代码可以编写成一个.lua后缀的文件,由redis-cli 或者 redis-plus-plus 或者jedis 等客户端加载,并发送给Redis服务器,由Redis服务器来执行这段逻辑

    一个Lua脚本会被Redis服务器以原子的方式来执行

     


    引入看门狗 (watch dog)

     上述方案仍然存在一个重要问题:在过期时间内我们的服务仍未执行完成,也就是事没办完你把锁给解了

    那么是不是直接加长我们的过期时间就可以了么?

    如果这样,服务器1真挂了的话这个锁会僵住很长一段时间,不合理的,只能动态调整~~

    所谓的watch dog ,本质就是在加锁的服务器上的一个单独的线程,通过对这个线程的加锁过期时间进行“续费”

    举个例子:

    初始情况下设置过期时间为10s.同时设定看⻔狗线程每隔 3s 检测⼀次.
    那么当3s时间到的时候,看⻔狗就会判定当前任务是否完成

    • 如果任务已经完成,则直接通过 lua 脚本的⽅式,释放锁(删除key)
    • 如果任务未完成,则把过期时间重写设置为10s

     

     引入Redlock算法

     实践中的 Redis ⼀般是以集群的⽅式部署的(⾄少是主从的形式,⽽不是单机).

     

    例如:

    服务器1向master节点进⾏加锁操作.这个写⼊key的过程刚刚完成,master挂了;slave节点升级成了新的master节点.但是由于刚才写⼊的这个key尚未来得及同步给slave呢,此时就相当于 服务器1 的加锁操作形同虚设了,服务器2仍然可以进⾏加锁(即给新的 master 写⼊ key. 因为新的 master 不包含刚才的 key)

     为了解决这个问题,Redis提出了Redlock算法

    引入一组Redis节点,其中每一组Redis节点都包含一个主节点和若干个从节点,并且组与组之间存储的数据都是一致的,相互之间是“备份关系”

    加锁的时候,按照一定顺序,写多个master节点,在写锁的时候就需要设定“超时时间”。比如30ms。如果超过了30ms没有成功,就视为加锁失败

     

    | 当加锁成功的节点数超过总结点数的一半,才视为加锁成功

    | 同理,释放锁的时候,也需要把所有节点都进⾏解锁操作.(即使是之前超时的节点,也要尝试解锁,尽量保证逻辑严密).

    在分布式系统中,不能让一台机器" 独断专行 ", 不能过度相信这一台机器不会宕机,最终加锁成功的结论就是“少数服从多数”

  • 相关阅读:
    Q弹的 肉丸教程
    EtherCAT转Modbus-TCP协议网关与DCS连接的配置方法
    springboot+vue企业销售人员培训报名系统java+ssm
    《linux程序设计》第二章笔记
    nginx部署问题集合
    MySQL的常用聚合函数
    工程师总结:PCB设计中降低噪声与电磁干扰的方法
    分布式一致性算法Raft-理论篇
    knative入门指南
    开发微信支付服务复盘
  • 原文地址:https://blog.csdn.net/Obto_/article/details/136194702