目录
前言
在一个分布式系统中,也会涉及到多个节点同时去访问一个公共资源的时候,此时就需要通过锁来做互斥控制,避免出现类似于“线程安全”的问题~~
本质上来说就是使用一个公共的服务器,用来记录加锁状态
当然这个服务器也可以是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
即设置key的时候对其value的值设置成为服务器的编号,比如:key:"001" , value:"服务器1"
这样就可以在删除key的时候检测一下value是否对应当前执行删除操作的服务器
那么伪代码应该如下:
- String key = [要加锁的资源 id];
- String serverId = [服务器的编号];
-
- // 加锁, 设置过期时间为 10s
- redis.set(key, serverId, "NX", "EX", "10s");
-
- // 执⾏各种业务逻辑
- // ...
-
-
- // 解锁, 删除 key. 但是删除前要检验下 serverId 是否匹配.
- if (redis.get(key) == serverId) {
- redis.del(key);
- }
很明显此时get和del这两步操作不是原子的,即使有过期时间也不应该就这样,过期时间只是最后防线
引入lua
为了使解锁操作原子,可以使用Redis支持的Lua脚本功能
lua的简介:
Lua也是一门编程语言,语法类似JS,是一个动态弱类型语言,Lua的解释器一般用C语言实现,Lua的语法精炼速度快,解释器轻量(200K左右)
因此Lua经常作为其他程序的内嵌脚本语言,Redis本身就支持Lua作为内嵌脚本
使用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)
上述方案仍然存在一个重要问题:在过期时间内我们的服务仍未执行完成,也就是事没办完你把锁给解了
那么是不是直接加长我们的过期时间就可以了么?
如果这样,服务器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没有成功,就视为加锁失败
| 当加锁成功的节点数超过总结点数的一半,才视为加锁成功
| 同理,释放锁的时候,也需要把所有节点都进⾏解锁操作.(即使是之前超时的节点,也要尝试解锁,尽量保证逻辑严密).
在分布式系统中,不能让一台机器" 独断专行 ", 不能过度相信这一台机器不会宕机,最终加锁成功的结论就是“少数服从多数”