Redis原理篇——分布式锁
- 分布式锁是什么?
- 分布式锁有哪些特性?
- 分布式锁常用实现方式
- Redis 实现分布式锁
- 一、简单的 Redis 锁
- 二、带过期时间的 Redis 锁
- 三、加上 Owner 的 Redis 锁
- 四、Lua 脚本确保原子性
分布式锁是什么?
分布式锁是在分布式系统中用来确保当多个进程或服务在不同的计算机上同时尝试访问和操作同一份资源时,能够协调一致地进行,避免数据冲突和不一致性的问题。
分布式锁就像是一个网络版的门卫,确保在多台计算机上运行的程序不会同时操作同一个数据。想象一下,每台计算机都要先拿到这个门卫的钥匙,才能操作数据。这样,就能防止数据混乱,确保每次只有一个程序在使用数据。
分布式锁有哪些特性?
- 互斥性:很好理解,确保在任何时刻,只有一个客户端可以持有锁。这意味着当一个进程或线程成功获取到锁时,其他任何尝试获取该锁的进程或线程都会被阻止,直到锁被释放。
老王进去了老李就不能再进了!!
- 锁失效机制:为了 防止死锁,分布式锁应该具有 超时机制 。如果持有锁的进程因崩溃或其他原因未能释放锁,锁将在设定的超时时间后自动释放。
老王在里面干太久了,这不行,挂了都不知道,甭管你挂没挂,不出来拉倒,别人还要进去呐!
- 对称性:加锁的必须和解锁的是 同一个竞争者 ,不能把其他的竞争者持有的锁给释放掉了
跟大爷打招呼,拿着钥匙进去的,得和出来的时候还钥匙的是一个人,不能老王进去老李出来吧,那不见鬼了!!
- 高可用:需要能有一定程度的 异常处理能力 和 容灾能力 ,确保业务不会出现中断
保安大爷不行了,干不动了,换那个新来的大学生顶上啊!!
分布式锁常用实现方式
- 基于缓存(如 Redis):使用缓存系统的原子操作来实现锁。例如,Redis的SETNX命令可以用来设置一个不存在的键,如果键已存在,则操作失败,这可以用来实现锁的互斥性。
- 基于数据库:利用数据库的唯一性约束来实现锁机制。通过向数据库表中插入具有唯一索引的记录来获取锁,操作完成后删除记录来释放锁。
- 基于Zookeeper:Zookeeper提供了一种基于临时节点和顺序节点的锁实现机制。客户端可以创建一个临时顺序节点,并检查是否为最小节点来获取锁,释放锁时删除该节点。
而本篇文章,将基于 Redis 缓存详细探讨分布式锁的使用
Redis 实现分布式锁
一、简单的 Redis 锁
想象一下,我们的系统是一个超级忙碌的酒吧,而分布式锁就是酒吧里唯一的洗手间的钥匙。客户(也就是进程)想要使用洗手间(也就是资源),他们需要先拿到钥匙。
SET restroom_key "occupied" NX
这条命令就像是在问酒保:“嘿,洗手间钥匙在吗?”如果NX(Not eXists)标志起作用,那就意味着洗手间空着,你就可以拿到钥匙(也就是锁)。如果钥匙“不存在”,那就意味着洗手间已经有人在用了,你得等等。最后,在使用完成之后会归还钥匙(delete)
不足之处
但是,如果老王进了洗手间,然后突然不见了(也就是进程崩溃了)或者没卫生纸了需要其他人递过来但又没手机(发生死锁),怎么办?我们的钥匙就永远被锁在里面了。这就引出了我们的第二部分。
二、带过期时间的 Redis 锁
为了防止洗手间被永久占用,我们给钥匙加上了一个计时器。
SET restroom_key "occupied" NX EX 5
现在,不管老王发生了什么,5分钟后钥匙也会自动回到酒保手里,其他客户就可以使用洗手间了。
进一步的不足
但是,如果老王今天身体确实不舒服,窜了太长的时间了,或者因为其他原因接个电话什么( 比如说网络延迟或者 GC卡顿等原因)导致他没消失但一直占着茅坑不拉屎,然后钥匙回到了酒保手上,另一个人老张就拿着钥匙进去了
然后关键来了,这时候老王结束了,然后出来的时候按照流程就把钥匙(也就是锁),但是实际上他还的是老张的钥匙,而这时候老张不就完犊子了,你老王的问题别把我也扯进去!!
三、加上 Owner 的 Redis 锁
分布式锁应该满足, 谁申请谁释放 的原则,不能释放别人的锁
因此我们决定在钥匙(锁)上刻上名字,这样只有名字匹配的人(一般是进程的 uuid )才能归还钥匙。
SET restroom_key “老张” NX EX 5
如果老张有了钥匙,那么只有老张能归还它。如果老王试图归还一个写着“老张”的钥匙,酒保会说:“不行,这不是你的钥匙。”
再进一步的问题
可以看到,解锁是有两个操作,先查看是否是自己的钥匙,是的话再归还钥匙(delete)
但整个解锁操作并不是原子性的,可能检查的时候是自己的,而归还删除的时候已经是别人的了( 比如说你刚获取到是自己的锁,然后处于了一个很长的 GC…)
四、Lua 脚本确保原子性
这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end
这样就通过使用 SET
命令 加上 NX
和 EX
参数 以及 Owner
标志 和 Lua
脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。
通过以上方式,分布式锁的前三个特性 互斥性 、 锁失效机制以及对称性就基本解决了
那么对于高可用,其实,就是基于集群模式,主从复制加上哨兵模式即可
详见之前文章:
Redis实战篇——搭建主从复制
Redis原理篇——哨兵机制