简介
分布式锁实现方案
数据库实现分布式锁
- 增加锁表(主要记录那个机器那个服务那个方法,获取锁时,则进行插入,如果插入成功则加锁成功,否则失败;释放锁删除记录即可)
- 乐观锁(条件,比如库存>0的时候才修改;版本号,如果版本号一直则修改,修改后版本号+1)
- 悲观锁(select for update,正常情况下是行锁,如果没有正确使用,比如没有索引或没有记录或隔离级别是串行化则会升级成表锁!)
Zookeeper实现分布式锁
主要通过znode节点和watch机制实现的;
znode节点有四种:
- 持久节点:一旦创建,永久存在zookeeper中,除非手动删除
- 持久有序节点:持久节点增加序号,并且是有序的;
- 临时节点:节点创建后,如果服务重启或宕机,则被删除;
- 临时有序节点:临时节点增加序号,并且是有序的;
watch机制: 监听节点变更,比如B节点监听A节点,当A节点状态发生变更或删除,则B节点就会收到通知!
实现流程:
- 当首次对共享资源访问的时候,zookeeper会创建一个持久节点作为父节点
- 当有其他应用进程访问的访问的时候,会在父节点后面依次创建临时有序节点
- 如果临时有序节点是最小的,则可以获取到锁
- 没有获取到锁的进程会对前一个节点进行监控,一旦就会依次唤醒
所以可以理解为zookeeper是公平锁,但是可以是实现非公平锁,比如只创建一个临时有序节点,其他进程进行争抢!
Redis实现分布式锁
- setnx(如果key不存在则设置成功) + expire(设置过期时间,避免死锁) + delete (释放锁)
-
- setnx + expire可以使用一条命令来实现保证原子性;
- 加锁时使用requestId避免其他线程释放锁,比如A获取锁后因业务逻辑没有执行完就到了过期时间释放了锁,B获取到锁后刚开始执行,A执行结束就把锁释放了,增加requestId后,可以先查询在判断是否可以释放,因为分了两步所以需要使用lua脚本保证原子性;
- 过期时间设置问题,如果A没有执行结束就释放锁,如果B还是和A执行相同的业务逻辑就会造成重复执行,可以在创建锁的时候增加守护线程来通过定时任务增加过期时间,释放的时候删除守护线程;
- 使用三方类库Redisson
-
- 上面的requestId以及过期时间问题,Redisson早就解决了,requestId在lua脚本中有判断、过期时间是通过看门狗机制实现的
- 加解锁实现原理,请看图:
- RedLock(红锁)
-
- 解决集群部署时,如果主节点挂了,但是没有把锁信息同步给从节点而出现问题
- 原理:为每个节点加锁,如果加锁的节点大于一般则获取锁成功,释放锁也是同理
- 在业界很有争议,redison的作者Antirez和Martin(分布式大佬)battle,Martin说并没有保证并发安全,分布式环境可能会出现线程暂停,比如A在加锁后进程暂停了,超过过期时间进行释放,B获取锁执行中,A进程开始执行就会认为获取到了锁会继续执行从而出现问题;而Antirez会说会检测到从而停止执行!
具体区别以及适用场景
- 数据库实现分布式锁性能比较差,有较大的代码改动;
- Redis中更多的采用的是Redisson方案,简单,并且性能是三种方案中最好的,可以抗住高并发的加解锁,但是在集群部署的收也会偶尔出现数据不正确的情况;
- Zookeeper保证强一致性,如果加解锁频繁Zookeepre服务器压力会很大
具体选择那个根据实际开发环境选择,比如我们系统已经使用了Redis做缓存,也能容忍极少数情况下数据不正确就选择了Redisson;如果你们系统已经使用了Zookeeper并且数据要求非常严格,就是用Zookpeer实现分布式锁!