分布式锁
为什么需要锁?
在单机多线程环境下,当多个线程需要访问同一个变量或者代码片段(临界资源)时,我们需要控制线程顺序执行,避免并发问题,通常我们会使用堆内存中的一个标志来控制,如果标志没有被占用,则线程可以访问,如果标志被占用则线程阻塞。这个标志也就是我们说的锁。如果不加锁当多个线程同时去操作数据库,可能导致数据错误也可能导致服务宕机。
什么是分布式锁?
在分布式环境下,即多台计算机,那么不同计算机上的线程想访问临界资源怎么办呢?
而像单机环境使用堆内存中的变量的方式肯定不适用了,因为一台计算机的堆内存中的变量对其他计算机上的线程是不可见的。所以我们需要找到所有计算机上的线程都可见的标志来作为锁。这样的锁就是分布式锁。
分布式锁的使用场景
抢票系统,抢票系统一般为适应高并发性,会将服务部署多在多个服务器上(以承载瞬间的并发场景),分布式锁控制不好,往往会出现超卖情况。
分布式锁的实现
数据库、Redis(常用)、Zookeeper
1、数据库乐观锁
一个mysql数据库Order,Order库里有一个Lock表中只有一条记录,该记录有一个字段lock_status,标识锁的标志。
- 如果分布式系统抢到锁后,自己宕机了造成锁无法释放,就会造成死锁。增加过期时间
- 抢到锁的分布式系统,其执行时间长,可能比锁得有效期长,会导致锁过期。可以使用守护线程增加锁的时长;
缺点:实现复杂,性能不高,可靠性低。
2、Redis
适用于千万级系统
通常使用set K V px mill nx, 在多线程环境下只会有一个线程成功,起到了锁的唯一性。(有两个问题:1、当前线程可以释放其他线程持有的锁,2、持有锁的线程还没执行完锁就过期了)、
解锁:1、查询当前锁是否还是我们持有,因为锁存在过期时间。
2、如果还是我们持有,则执行解锁操作,否则,直接返回失败。
由于redis没有原子指令支持这两步操作,所以通常使用Lua脚本解锁
Redis分布式锁过期了,还没处理完怎么办?
在设置过期时间时或结合业务场景去考虑,尽量去设置一个比较合理的值。尽管这样,还需要设置一个兜底策略。
- 守护线程定期检查贤臣是否还持有锁,如果有则延长时间(“看门狗”,每1/3锁过期时间检查一次).
- 超时回滚:当我们结果时发现线程已经被其他线程获取了,说明此时我们执行的操作已经是“不安全的了”,此时需要进行回滚,并返回失败。
守护线程续命的问题:
一个线程A设置了锁,在主机同步到从机之前,主机服务器宕机了,从机变成主机(该主机没有线程A的锁信息),导致了第二个线程B可以获取锁,以至于两个线程获取了锁,导致一致性问题。
解决方案:
RedLock
- 获取当前的系统时间
- 尝试从5个实例,使用相同的key和随机值去获取锁,获取锁会有一个超时时间避免在待机的Redis节点浪费时间。
- 如果获取一半以上节点的锁,则获取锁成功,执行业务逻辑。
- 如果获取一半以上节点的锁失败,则对之前加锁的线程释放锁。
也有可能会出问题:
1、严重依赖系统时间,某个节点的系统时间过快,锁释放后,另一个线程可能获取到一半以上的锁,导致两个线程同时获得锁。
2、某个节点重启,另一个线程可能获取到一半以上的锁,导致两个线程同时获得锁。
Zookeeper实现的分布式锁
3、Zookeeper实现分布式锁
方案:
1、创建一个locks目录
2、想要获取锁的线程都在锁目录下创建一个临时顺序节点
3、获取所目录下所有子节点,对子节点按节点自增序号从小到大排序。
4、判断本节点是不是第一个节点,如果是则成功获取锁,开始执行业务逻辑,如果不是则监听自己上一个节点的删除时间(只有上一个节点释放了锁才有可能到自己)
5、持有所得线程释放锁,只需删除节点即可。
6、当自己监听的节点被删除,监听时间触发,则重新回到第三步,直到获取锁
Redis和Zookeeper的区别:
Zookeeper保证了数据的强一致性,不会出现redis的问题,但是它的问题在于性能不如Redis好,申请锁和释放锁频率较高时,会对集群造成压力。
通常情况下,对于数据安全性要求没那么高时,可以采用redis的方案,对数据安全性要求比较高的可以采用Zookeeper方案。