[Redis][典型运用][分布式锁]详细讲解


0.什么是分布式锁

  • 在⼀个分布式的系统中,也会涉及到多个节点访问同⼀个公共资源的情况,此时就需要通过锁来做互斥控制,避免出现类似于"线程安全"的问题
  • C++的std::mutex只能在当前进程中⽣效,在分布式的这 种多个进程多个主机的场景下就⽆能为⼒了,此时就需要用到分布式锁
  • 分布式锁本质使用一个公共的服务器,来记录加锁状态
    • 这个公共的服务器可以是Redis,也可以是其他组件(MySQL、ZooKeeper等),或是自己写的服务

1.分布式锁的基础实现

  • 思路⾮常简单,本质上就是通过⼀个键值对来标识锁的状态

  • 以经典的买车票为例

    • 下图是必然有线程安全问题的,需要用锁来控制
      请添加图片描述

    • 在上述架构中,引入一个Redis,作为分布式锁的管理器

      • 此时,如果买票服务器1尝试买票,就需要先访问Redis,在Redis上设置⼀个键值对
        • 例如key就是⻋次,value随便设置个值(⽐如1)
      • 如果这个操作设置成功,就视为当前没有节点对该001⻋次加锁,就可以进⾏数据库的读写操作,操作完成之后,再把Redis上刚才的这个键值对给删除掉
      • 如果在买票服务器1操作数据库的过程中,买票服务器2也想买票,也会尝试给Redis上写⼀个键值对,key同样是⻋次,但是此时设置的时候发现该⻋次的key已经存在了,则认为已经有其他服务器正在持有锁,此时服务器2就需要等待或者暂时放弃
        请添加图片描述
  • Redis中提供了setnx操作,正好适合上述场景

  • 但是上述方案并不完整


2.引入过期时间

  • 问题情景:当服务器1加锁之后,开始处理买票的过程中,如果服务器1意外宕机了,就会导致解锁操作(删除该key)不能执⾏,就可能引起其他服务器始终⽆法获取到锁的情况
  • 为了解决这个问题,可以在设置key的同时引⼊过期时间,即这个锁最多持有多久,就应该被释放
    • 使用set ex nx的方式,在设置锁的同时把过期时间设置进去
    • 例如:设置key的过期时间为1000,那么意味着即使出现极端情况,某个服务器挂了,没有正确释放锁,这个锁最多保持1000ms,也就会自动释放了
  • 此处的过期时间只用了一个命令,分开来用set nxexpire可以吗
    • Redis上的多个命令之间,是无法保证原子性的
    • 由于Redis的多个指令之间不存在关联,并且即使使⽤了事务也不能保证这两个操作都⼀定成功,因此就可能出现setnx成功,但是expire失败的情况
    • 此时仍然会出现⽆法正确释放锁的问题
    • 相比之下,使用一条命令,是更加稳的

3.引入校验ID

  • 问题情景对于Redis中写⼊的加锁键值对,其他的节点也是可以删除的
    • 例如
      • 服务器1写⼊⼀个"001":1这样的键值对,服务器2是完全可以把"001"给删除掉的
      • 当然,服务器2不会进⾏这样的"恶意删除"操作,不过不能保证因为⼀些bug导致服务器2把锁误删除
  • 为了解决上述的问题,可以引入一个检验ID
    • 例如:可以把设置的键值对的值,不再是简单的设为⼀个1,⽽是**设成服务器的编号**,如"001":"服务器 1"
    • 这样就可以在删除key(解锁)的时候,先校验当前删除key的服务器是否是当初加锁的服务器,如果是,才能真正删除,不是,则不能删除
  • 逻辑用伪代码
    String key = [要加锁的资源id];
    String serverId = [服务器的编号];
    
    // 加锁, 设置过期时间为10s
    redis.set(key, serverId, "NX", "EX", "10s");
    
    // 执⾏各种业务逻辑, ⽐如修改数据库数据
    doSomeThing();
    
    // 解锁, 删除key, 但是删除前要检验下serverId是否匹配
    if (redis.get(key) == serverId)
    {
    	redis.del(key);
    }
    
  • 问题又来了,解锁逻辑是两步操作getdel,这样做并非是原子的
  • 此时就会可能这样的情况
    • 服务器2的线程C进行加锁,但是服务器1的线程B就把线程C刚刚加的锁解锁了
    • 因为线程B确实也是在服务器1中成功进行了校验,有权利解锁
      请添加图片描述

4.引入Lua

  • 为了使解锁操作原子,可以使用Redis的Lua脚本功能
  • Lua脚本
    • 类似于JS,是一个动态弱类型的语言
    • Lua语法简单精炼,执行速度快,解释器也比较轻量(200KB左右)
    • Lua经常作为其他程序内部嵌入的脚本语言,Redis则也支持
  • 使用Lua脚本完成上述解锁功能
    if redis.call('get',KEYS[1]) == ARGV[1] then
    	return redis.call('del',KEYS[1])
    else
    	return 0
    end;
    
  • 上述代码可以编写成一个.lua后缀的文件,由redis-cli或者redis-plus-plus等客户端加载,并发送给Redis服务器,由Redis服务器来执行这段逻辑
  • 一个Lua脚本会被Redis服务器以原子的方式来执行
  • 使用事务就能解决上述问题,为何要用Lua脚本呢?
    • 首先明确,Redis事务虽然弱,但是避免插队还是能做到的
      • 所以说,Redis事务确实能解决上述问题
    • 但是实践中使用的往往是更好的方案
      • 有上等马为啥用下等马嘞:)
    • Redis官方文档:Lua属于是事物的替代方案

5.引入Watch Dog(看门狗)

  • 上述方案仍然存在一个重要问题,如果设置了key过期时间之后,仍然存在一定的可能性,当任务还没执行完,key就先过期了,这就导致锁提前释放
  • 如果把这个过期时间设置的足够长,比如30s,是否能解决这个问题?
    • 很明显,设置多少时间合适,是无止境的,即使设置再长,也不能完全保证就没有提前失效的情况
    • 而且如果设置的太长了,万一对应的服务器挂了,此时其他的服务器也不能及时获取到所
    • 因此相比于设置一个固定的定长时间,不如动态的调整时间更合适
  • Watch Dog本质上是加锁服务器上的一个单独的线程,通过这个线程来对锁过期时间进行“续约”
    • 注意:这个线程是业务服务器上的,不是Redis服务器的
  • 举例助解
    • 初始情况下设置过期时间为10s,同时设定看门狗线程每隔3s检测一次
    • 那么当3s时间到的时候,看门狗就会判定当前任务是否完成
      • 如果任务已经完成,则直接通过Lua脚本的方式,释放锁(删除key)
      • 如果任务未完成,则把过期时间重写设置为10s(即”续约”)
  • 此时就不担心锁提前失效的问题了,另一方面,如果该服务器挂了,看门狗线程也就随之挂了,此时无人续约,这个key自然就可以迅速过期,让其他服务器能够获取到锁了

6.引入Redlock算法

  • 问题情景:实践中的Redis一般是以集群的方式部署的(至少是主从的形式,而不是单机),那么就可能出现以下比较极端的冤种情况
    • 服务器1向master节点进行加锁操作,这个写入key的过程刚刚完成,master挂了,slave升级成了新的master
    • 但是由于刚才写入的这个key尚未来得及同步给slave,此时就相当于服务器1的加锁操作形同虚设了
    • 服务器2仍然可以进行加锁(即给新的master写入key),因为新的master不包含刚才的key
  • 为了解决这个问题,Redis作者提出了Redlock算法
    • 引入一组Redis节点,其中每一组Redis节点都包含一个主节点和若干从节点,并且组和组之间存储的数据都是一致的,互相之间是”备份关系

      • 并非是数据集合的一部分,这点有别于Redis Cluster
    • 加锁的时候,按照一定顺序,写多个master节点,在写锁的时候需要设定操作的”操作时间”

      • 例如:50ms,即如果setnx操作超过了50ms,还没有成功,就视为加锁失败
        请添加图片描述
    • 如果某个节点加锁失败,就立即再尝试下一个节点

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

      • 如上图,一共五个节点,三个加锁成功,两个失败,此时视为加锁成功
      • 这样的话,即使有某些节点挂了,也不影响锁的正确性
    • 同理,释放锁的时候,也需要把所有节点都进行解锁操作

      • 即使是之前超时的节点,也要尝试解锁,尽量保证逻辑严密
  • 此时还可能出现上述节点同时都遇到了”大冤种”情况呢?
    • 理论上这件事是可能发生的,但是概率太小了,工程上就可以忽略不计了
  • Redlock算法核心:加锁操作不能只写给一个Redis节点,而要写多个,需要冗余
    • 分布式系统中,任何一个节点都是不可靠的
    • 由于一个分布式系统不至于大部分节点都同时出现故障,因此这样的可靠性要比单个节点来说靠谱不少
    • 最终的加锁成功结论是:少数服从多数

7.其他功能

  • 上述描述中解释了基于Redis的分布式锁的基本实现原理
  • 上述锁只是一个简单的互斥锁,但是实际上在一些特定场景中,还有一些其他特殊的锁,例如:
    • 可重入锁
    • 公平锁
    • 读写锁
    • ……
  • 实际开发中,也不会真的自己实现一个分布式锁,已经有很多现成的库帮我们封装好了,直接拿来s使用即可
    • 例如:C++中的redis-plus-plus
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DieSnowK

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值