在一个分布式系统中,涉及到多个节点访问同一个公共资源的情况,此时就需要通过锁来做互斥控制,避免出现类似于“线程安全”问题。
之前学过的锁本质都只能在一个进程内部生效,分布式系统是有很多进程的(每个服务器都是独立的进程),之前的锁是无法实现进程间的制约。
1.分布式锁场景
两个客户端同时去查询,都查到了余票为1,都执行--操作,最终使得票数剩余-1,这就导致超卖了;
所谓的分布式锁,本质也是一个/一组单独的服务端程序,给其他服务器提供“加锁”这样的服务,给其他服务器提供“加锁”这样的服务(Redis是一种典型实现分布式锁的方案,不是唯一一种);
买票服务器,在进行买票操作过程中,需要先加锁(往redis上设置一个特殊的key-value,完成上述买票操作之后,删除);
setnx可以实现加锁效果,针对解锁,使用del命令完成;
问题:进程内部的锁,进程退了,锁就没了;但是分布式的锁,如果持有锁的服务器掉电,会导致锁无法释放,其他服务器就无法获取到锁了?解决方法:给set的key设置过期时间,一旦时间到,key就会被自动删除;
需要用:set ex nx这种来设置,如果用setnx+expire,这是无法保证原子的;问题:如果一个服务器加锁,另一个给它解了?
解决:引入校验机制
1)给服务器编号,每个服务器有一个自己的身份标识;解锁时先查询编号,相同才del;
2)加锁时,设置key-value,key要对应针对哪个资源加锁,value存储服务器编号;问题:加入了校验机制后,必须要两步才能完成,一步查询、一步del,但这两步操作不能保证是原子的。
虽然看起来这两个服务器执行两次del没有问题,但是如果有第3个服务器,在B执行del前就加锁了,就会把这个锁解了;
解决:引入lua脚本,用lua写一些逻辑,上传到redis服务器上,然后让客户端来控制redis执行上述脚本,redis执行lua的过程,也是原子的(redis官方文档:lua是事务的替代方案);
2.过期时间续约问题
问题:加锁时,设置过期时间多少合适?
设置多了,服务器挂了,很久才释放;
设置少了,服务器还没操作完就自动解锁了;
解决:动态续约,例如:初始设置1s,等到剩余300ms,再把时间续成1s,从此往复;
因此服务器这边需要一个专门的线程来负责续约,这个线程被称为“看门狗”(watch dog);
3.redis挂了
解决:采用主从+哨兵,进行加锁,把key设置到主节点上,主节点挂了,哨兵自动把从节点升级成主节点,进一步保证锁可用;
衍生问题:主从节点数据同步是存在延时的,主节点先收到set还没同步呢就挂了,从节点就没锁了;解决:redlock算法,采用多组(主从+哨兵);
加锁就按照一定顺序,针对这些组redis加锁,写入key成功超过一半,视为加锁成功;
解锁时,上述主节点都进行一遍解锁;