Redis分布式锁

什么是分布式锁

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

多个线程,并发执行的时候,执行先后顺序,是不确定的->随机性->需要保证程序在任意执行顺序下执行逻辑都是ok的

⽽ java 的 synchronized 或者 C++ 的 std::mutex, 这样的锁都是只能在当前进程中⽣效, 在分布式的这种多个进程多个主机的场景下就无能为力了

此时就需要使⽤到分布式锁

本质上就是使⽤⼀个公共的服务器, 来记录 加锁状态. 这个公共的服务器可以是 Redis, 也可以是其他组件(⽐如 MySQL 或者 ZooKeeper 等), 还可以 是我们⾃⼰写的⼀个服务.

分布式锁的基础实现

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

举个例⼦: 考虑买票的场景, 现在⻋站提供了若⼲个⻋次, 每个⻋次的票数都是固定的

现在存在多个服务器节点, 都可能需要处理这个买票的逻辑: 先查询指定⻋次的余票, 如果余票 > 0, 则设置余票值 -= 1.

显然上述的场景是存在 "线程安全" 问题的, 需要使⽤锁来控制:客户端1先执行查询余票,发现剩余1张,在执行1->0过程之前,客户端2也执行查询余票,发现,也是剩余1张,客户端2也会执行1->0操作

否则就可能出现 "超卖" 的情况

此时如何进⾏加锁呢? 我们可以在上述架构中引⼊⼀个 Redis , 作为分布式锁的管理器

此时, 如果 买票服务器1 尝试买票, 就需要先访问 Redis, 在 Redis 上设置⼀个键值对. ⽐如 key 就是⻋ 次, value 随便设置个值 (⽐如 1)

如果这个操作设置成功, 就视为当前没有节点对该 001 ⻋次加锁, 就可以进⾏数据库的读写操作. 操作完 成之后, 再把 Redis 上刚才的这个键值对给删除掉

引入setnx

如果在 买票服务器1 操作数据库的过程中, 买票服务器2 也想买票, 也会尝试给 Redis 上写⼀个键值对, key 同样是⻋次. 但是此时设置的时候发现该⻋次的 key 已经存在了, 则认为已经有其他服务器正在持 有锁, 此时 服务器2 就需要等待或者暂时放弃.

Redis 中提供了 setnx 操作, 正好适合这个场景. 即: key 不存在就设置, 存在则直接失败

使用setnx确实可以得到“加锁”效果,针对解锁,就可以使用“del”来完成

Question:某个服务器,加锁成功了(setnx成功),执行后续逻辑过程中,程序崩溃了(没有执行到解锁),为了保证解锁操作能够执行到,是否可以把解锁放到finally中?


Answer:在进程间是有效果的,但是在分布式中无效(服务器掉电),这种情况会导致redis上设置的key无人删除,也就导致其他服务器无法获取到锁了

买票场景,使用MySQL的事务,也可以批量执行查询+修改操作,但是分布式系统中,要访问的共享资源,不一定是MySQL,也可能是其他的存储介质,没有事务,也可能是执行一段特定的操作,是通过统一的服务器完成执行动作

引入过期时间

可以给set的key设置过期时间的,一旦时间到,key就会自动被删除掉了,set ex nx这样的命令来完成设置,比如设置key的过期时间为1000ms,那么意味着即使出现极端情况,某个服务器挂了,没有正确释放锁,这个锁最多保持1000ms,也就会自动释放了

setnx
expire

Question:这种设置方式是否可行?


Answer:务必要使用set ex nx这样的方式来设置!redis上的多个命令之间,无法保证原子性的,此时可能会出现两个命令,一个成功,一个失败的情况,相比之下,使用单个命令设置,更加稳妥!

引入校验id

  • 所谓的加锁,就是给redis上设置一个key-value
  • 所谓的解锁,就是给redis上这个key-value删除掉

Question:是否可能出现:服务器1执行了加锁,服务器2执行了解锁(正常来说,肯定不是故意的,但是代码总会有bug)?


Answer:所谓的锁,就是redis的普通键值对!不小心执行到了解锁操作,因此就可能进一步的给整个系统带来更严重的问题(超卖问题),引入校验id

  1. 给服务器编号,每个服务器有一个自己的身份标识
  2. 进行加锁的时候,设置key-value,key对应着要针对哪个资源加锁(比如车次),value就可以存储刚才服务器的编号,标识出当前这个锁是哪个服务器加上的
  3. 后续解锁的时候,可以进行校验了;解锁的时候,先查询这个锁对应的服务器编号,然后判定一下这个编号是否就是当前执行解锁的服务器编号,如果是,才能执行得了,如果不是,就是白

引入lua脚本

在解锁的时候,先查询判定,在进行del->此处是两步操作(不是原子的,就可能会出现问题),一个服务器内部,也可能是多线程的,此时,就可能同一个服务器,两个线程都在执行上述解锁操作,此时del就会被重复执行

若是del和del之间,有其他服务器来执行一些操作(set nx ex加锁操作),上述情况,看起来重复执行del好像问题不大,实则不然,主要是引入一个新的服务器,执行加锁,就可能出现问题了

归根结底,都是因为get和del不是原子产生的问题;使用redis的事务可以解决上述问题,但是我们使用lua脚本

  • Lua 也是⼀个编程语⾔. 读作 "撸啊". 是葡萄⽛语中的 "⽉亮" 的意思. (出⾃于 Lua 官⽅⽂档 https://www.lua.org/about.html)
  • Lua 的语法类似于 JS, 是⼀个动态弱类型的语⾔. Lua 的解释器⼀般使⽤ C 语⾔实现. Lua 语法 简单精炼, 执⾏速度快, 解释器也⽐较轻量(Lua 解释器的可执⾏程序体积只有 200KB 左右)
  • 因此 Lua 经常作为其他程序内部嵌⼊的脚本语⾔. Redis 本⾝就⽀持 Lua 作为内嵌脚本
  • 很多程序都⽀持内嵌脚本, ⽐如 MySQL 8 ⽀持 JS 作为内嵌脚本, ⽐如 Vim ⽀持 VimScript 和 Python 作为内嵌脚本.... 通过内嵌脚本来实现更复杂的功能, 提供更强的扩展性
  • Lua 除了和 Redis 搭伙之外, 在很多场景也会作为内嵌脚本. ⽐如在游戏开发领域常常作为 编写逻辑的语⾔. (⽐如魔兽世界, ⼤话西游等)
  • redis执行lua脚本的过程中,也是原子的,相当于执行一条命令一样,实际上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 或者 jedis 等客⼾端加载, 并发送给 Redis 服务器, 由 Redis 服务器来执⾏这段逻辑
  • ⼀个 lua 脚本会被 Redis 服务器以原⼦的⽅式来执⾏

引入WATCH DOG

过期时间的续约问题

Question:要在加锁的时候,给key设定过期时间,过期时间设置多少合适?

Answer:

  • 如果设置短的话,就可能在你的业务逻辑还没执行完,就释放锁了
  • 如果设置的长的话,就会导致“锁释放不及时”的问题
  • 更好的方式,是”动态续约“-往往需要服务器专门线程负责续约这个事情-WATCH DOG,初识情况下,设置一个过期时间(比如设置1s)就提起在还剩300ms(灵活设置)的时候,如果当前任务还没执行完,就把过期时间再续上1s,等到时间又快到了,任务还没执行完,就再续
  • 如果服务器,中途崩溃了,自然就没人负责续约了,此时,锁就能够在较短时间内被自动释放了
  • 所谓 watch dog, 本质上是加锁的服务器上的⼀个单独的线程,通过这个线程来对锁过期时间进行 "续约"
  • 注意, 这个线程是业务服务器上的, 不是 Redis 服务器的

引入redlock算法

使用redis作为分布式锁,redis本身有没有可能挂了呢?

要想保证“高可用”就需要通过这一系列“预案演习”

  • 主从复制
  • 哨兵-解决高可用
  • 集群-解决存储空间不足

使用哨兵节点搭配redis服务器

主节点和从节点之间的数据同步,是存在延时的,可能主节点收到了set请求,还没来得及同步给从节点呢,主节点就先挂了,即使从节点升级成了主节点,但是,刚才的加锁对应的数据,也是不存在的,这里引入了redlock算法(冗余)

此处加锁,就是按照一定的顺序,针对这些组redis都进行加锁操作,如果某个节点挂了(某个节点加不上锁,没关系,可能是redis挂了),继续给下一个节点加锁即可-如果写入key成功的节点个数超过总数的一半,就视为加锁成功;同理,进行解锁的时候,也就会把上述节点都设置一边解锁

小结

上述描述中我们解释了基于 Redis 的分布式锁的基本实现原理

上述锁只是⼀个简单的互斥锁. 但是实际上我们在⼀些特定场景中, 还有⼀些其他特殊的锁,比如

  • 可重入锁
  • 公平锁-遵守先来后到
  • 读写锁
  • ......

基于 Redis 的分布式锁, 也可以实现上述锁的特性

此处我们不做过多讨论了。实际开发中,我们也并不会真的⾃⼰实现⼀个分布式锁。已经有很多现成的库帮我们封装好了,我们直接 使⽤即可,⽐如 Java 中的 Redisson,C++ 中的 redis-plus-plus

  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值