引入分布式锁
我们之前学过的锁,本质上都是只能在一个进程内部生效
在分布式系统中,是有很多进程的(每个服务器,都是独立的进程)
因此,之前的锁,难以对现在分布式系统中的多个进程之间产生制约
并且分布式系统中,多个进程之间的执行顺序也是不确定的=>随机性
引入"分布式锁"来解决上述问题
分布式锁的基础实现
举个例子:
客户端1,先执行查询余票,发现剩余1张
在即将执行1->0过程之前
客户端2也执行查询余票,发现剩余1张
客户端2也会执行1->0过程
所谓的分布式锁,也是一个/一组单独的服务器程序,给其他的服务器提供"加锁"这样的服务
(redis是一种典型的可以用来实现分布式锁的方案,但不是唯一的一种,也可以使用mysql/zookeeper这样的组件来实现)
买票服务器,在进行买票操作时,就需要先加锁
在redis上设置一个特殊的key-value,完成上述买票操作,再把这个key-value删除
其它服务器像买票时,也在redis尝试设置key-value,如果发现key-value已经存在,就认为"加锁失败"(是放弃/阻塞,看具体实现策略)
这样就可以保证,第一个服务器执行"查询->更新"过程中,第二个服务器不会出现"超卖"问题
setnx命令
回想setnx命令,使用setnx确实可以得到"加锁"效果,针对解锁,使用del命令
假设个服务器,加锁成功了(setnx成功),执行后续逻辑中程序崩溃了,没有执行解锁
那能否使用finally呢?(无论是否出现异常,都会执行到)
不能,这种做法,只是针对进程内(进程异常退出,锁也就随之销毁了)的锁,针对分布式锁无效(redis还是好好的)
这种情况就会导致redis上设置的key无人删除,其它服务器无法获取到锁了
解决:
可以给set的key设置过期时间,一旦时间到,自动删除
引入过期时间
set ex nx 命令
比如说,设置key的过期时间为1000ms,意味着即时出现极端情况,某个服务器挂了,没有正确释放锁,这个锁最多保持1000ms,也就会自动释放
注意:
setnx expire这种设置方式不可以
redis上的多个命令之间,无法保证原子性,此时就可能出现,这两个命令,一个成功,一个失败
相比之下,一条命令更稳妥
引入校验id
所谓的加锁,就是给redis设置一个key-value,解锁,就是给redis这个key-value删除掉
那么是否可能会出现,服务器1执行了加锁,服务器2执行了解锁
正常来说,是不会的,但是代码总会有bug,不小心就执行了解锁,此时就会进一步给系统带来严重的后果
为了解决上述问题,引入校验机制:
①给服务器编号,每个服务器有自己的身份标识
②进行加锁的时候,设置key-value,key对应着要针对哪个资源加锁,value存储服务器的编号,标识着当前这个锁是哪个服务器加上的
后续在解锁的时候,就可以进行校验了
解锁的时候,先查询一下这个锁对应的服务器编号,然后判定一下这个编号是否就是当前执行解锁的服务器编号,如果不是,就失败
当然,在解锁的时候,先查询判定,再进行del,此处是两步操作,就可能会出现问题
如果说:
一个服务器内部,也可能是多线程的
此时,就可能同一个服务器,两个线程都在执行上述解锁操作
看起来好像重复DEL好像问题不大,不是!!!
如果再引入一个新的服务器2,执行加锁操作,就可能出现问题了(在线程A执行完DEL之后,B执行DEL前)
服务器2的线程C执行加锁,此时由于A已经把锁释放的,C的加锁是能够成功的
但是紧接着,线程B的del到来了,就把刚刚服务器2的加锁操作给解锁了
服务器1和2进行加锁,key是资源的编号,服务器id是value
这个归根结底,都是因为get和del不是原子产生的问题
引入lua
使用事务,能解决上述问题(redis事务避免插队)
但是实际中往往采用更好的方案,lua脚本
lua脚本是一个编程语言,作为redis内嵌的脚本
lua语言特别轻量,实现一个lua解释器,销毁的体积非常小
可以使用lua编写一些逻辑,把这个脚本上传到redis服务器,就可以让客户端来控制redis执行上述脚本了
redis执行lua脚本的过程,也是原子的,相当于执行一条命令(实际上多条)
redis官方文档,也明确了lua是事务代替方案
引入watch dog(看门狗)
过期时间的续约问题:
在加锁的时候,给key设置过期时间
设置多少时间呢?
- 设置过短,就可能业务逻辑还没有执行完,就释放锁了
- 设置过长,就会导致"锁释放不及时"问题
更好的方式,就是"动态续约"
比如,初始情况下,设置一个过期时间(比如设置1s),提前在还剩300ms的时候,如果当前任务还没执行完,就把过期时间再续上1s,等到时间又快到了,但是任务还没执行完,就再次续约...
如果服务器中途崩溃了,就没人续约了,此时,锁就在较短的时间内被自动释放
"动态续约",续约服务器这边有一个专门的线程,负责续约这个事情=>看门狗
引⼊Redlock算法
那么使用redis作为分布式锁,redis本身有没有可能挂了呢?
当然有
要想保证"高可用",就需要通过这样一系列的"预案演习"
哨兵方式:
进行加锁,就是把key设置到主节点上
如果主节点挂了,有哨兵自动吧从节点升级为主节点,进一步保证刚才的锁仍然可用
主节点和从节点之间的数据同步,是存在延时的!!
可能主节点收到了set请求,还没来得及同步给从节点,主节点就先挂了
即使从节点升级成了主节点,但是刚才的加锁对应的数据也是不存在的
分布式系统,需要随时考虑某个节点挂了的情况,需要保证某个节点挂不会影响到大局
为了解决这个问题,Redis的作者提出了Redlock算法
引⼊⼀组Redis节点.其中每⼀组Redis节点都包含⼀个主节点和若⼲从节点.并且组和组之间存储的数据都是⼀致的,相互之间是"备份"关系
此处加锁,是按照一定的顺序,针对这些组redis都进行加锁操作
如果某个节点挂了(加不上锁),继续给下一个节点加锁即可
如果写入key成功的节点个数超过总数的一半,视为加锁成功
同理,解锁的时候,也就会把上述节点都设置一遍解锁
总之,Redlock算法的核⼼就是,加锁操作不能只写给⼀个Redis节点,⽽要写个多个!!
分布式系统 中任何⼀个节点都是不可靠的.最终的加锁成功结论是"少数服从多数的".
由于⼀个分布式系统不⾄于⼤部分节点都同时出现故障,因此这样的可靠性要⽐单个节点来说靠谱不 少
上述内容,只是一个简单的"互斥锁"
还涉及其它情况:
读写锁,公平锁(遵守先来后到),可重入锁...