问题描述
随着业务的发展,原来的单体架构演变成分布式集群后,由于分布式系统多线程,多进程且分布在不同的机器上,这将使原单机部署情况下的并发控制锁策略失效,所以就需要分布式锁解决。
分布式锁主流的实现方案:
- 基于数据库实现分布式锁
- 基于缓存(redis)
- 基于zookeeper
每一种分布式锁解决方案都有各自的优缺点:
- 性能:redis最高
- 可靠性:zookeeper最高
redis实现分布式锁
基于redis的命令: 不存在才设置并且设置过期时间,原子操作
set key value [EX|PX] [NX|XX]
设置过期时间有两种方式
- 首先通过expire设置过期时间(缺乏原子性,如果在setnx和expire之间出现异常,锁也无法释放)
- 在set时指定过期时间(推荐)
存在问题
可能会释放其他服务器的锁。
场景:如果业务逻辑的执行时间是7s,执行流程如下
- index1业务逻辑没执行完,3秒后锁被自动释放。
- index2获取到锁,执行业务逻辑,3秒后被自动释放。
- index3获取到锁,执行业务逻辑。
- index1业务逻辑执行完成,开始调用del释放锁,这时释放的是index3的锁,导致index3的业务只执行1s就被别人释放,相当于index3没有锁。
此方案存在两个问题:
- 业务还没执行完,锁就已经过期了,
- 可能会释放其他线程的锁。
解决方案:set设置锁的时候把value设置为唯一值(例如uuid),释放前获取这个值,判断是否自己的锁。
优化之UUID防止误删
但还是存在问题,判断是否是自己的锁和删除锁的操作不具有原子性
比如以下场景:
- index1执行删除时,查询到的lock值确实是自己设置的uuid
- index1执行del前,刚好锁过期时间到了,此时redis已经没有锁了
- index2获取到了锁,开始执行方法
- index1开始执行del,此时会把index2的lock删除。因为index1已经判断过uuid并通过了,是具有del删除权限的。
优化之LUA脚本保证删除的原子性
为了确保分布式锁的可用,我们至少要确保锁的实现同时满足一下四个条件:
- 互斥性,在任意时刻,只有一个客户端能持有锁
- 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也会保证后续其他客户端能加锁。
- 加锁和解锁必须是同一个客户端,不能释放其他人的锁。
- 加锁和解锁必须具有原子性