分布式锁实现
电商场景,当用户下单的时候,redis 里库存只有一件,并发执行的时候可能会造成库存超卖问题
通过在执行第二步加锁,可以保证并发请求在下单的时候操作是串行化的,但是并发增多,增加一台机器
此时还是会造成库存超卖问题。原因是:两个系统运行在两个不同的JVM里面,他们加的锁只对属于自己JVM里面的线程有效,对于其他JVM的线程是无效的。即 Java提供的原生锁机制在多机部署场景下失效了
分布式锁:redis 或 zookeeper
1. Redis 实现方式
思路:在redis中设置一个值表示加了锁,然后释放锁的时候就把这个key删除。
1 | // 获取锁 |
需要注意的地方:
- 一定要用SET key value NX PX milliseconds 命令
如果不用,先设置了值,再设置过期时间,这个不是原子性操作,有可能在设置过期时间之前宕机,会造成死锁(key永久存在) - value 要具有唯一性
这个是为了在解锁的时候,需要验证value是和加锁的一致才删除key。
这是避免了一种情况:假设A获取了锁,过期时间30s,此时35s之后,锁已经自动释放了,A去释放锁,但是此时可能B获取了锁。A客户端就不能删除B的锁了。
这样有可能会有一个问题是:设置了key的过期时间,但是业务处理逻辑的时间可能大于过期时间,这样A获取了锁,但是处理超时了,key被过期,B获取了锁,也有可能会恶性循环
组件 Redission 实现
- redisson所有指令都通过lua脚本执行,redis支持lua脚本原子性执行
- redisson中有一个watchdog的概念,翻译过来就是看门狗,它会在你获取锁之后,每隔10秒帮你把key的超时时间设为30s。这样的话,就算一直持有锁也不会出现key过期了,其他线程获取到锁的问题了。同时也保证了没有死锁的产生,哪怕机器宕机,key也会在时间到了之后自己过期
1 | // 加锁逻辑 |
2. ZooKeeper 实现方式
Zookeeper是一种提供配置管理、分布式协同以及命名的中心化服务。
zk的模型是这样的:zk包含一系列的节点,叫做znode,就好像文件系统一样每个znode表示一个目录,然后znode有一些特性:
- 有序节点
- 假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点
zookeeper提供了一个可选的有序特性,例如我们可以创建子节点“/lock/node-”并且指明有序,那么zookeeper在生成子节点时会根据当前的子节点数量自动添加整数序号
也就是说,如果是第一个创建的子节点,那么生成的子节点为/lock/node-0000000000,下一个节点则为/lock/node-0000000001,依次类推。
- 假如当前有一个父节点为/lock,我们可以在这个父节点下面创建子节点
- 临时节点
- 客户端可以建立一个临时节点,在会话结束或者会话超时后,zookeeper会自动删除该节点。
- 事件监听
- 在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:
- 节点创建
- 节点删除
- 节点数据修改
- 子节点变更
- 在读取数据时,我们可以同时对节点设置事件监听,当节点数据或结构变化时,zookeeper会通知客户端。当前zookeeper有如下四种事件:
实现分布式锁的思路
- 使用zk的临时节点和有序节点,每个线程获取锁就是在zk创建一个临时有序的节点,比如在/lock/目录下。
- 创建节点成功后,获取/lock目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点
- 如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。
- 如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听。
比如当前线程获取到的节点序号为 /lock/003
,然后所有的节点列表为[/lock/001,/lock/002,/lock/003]
,则对/lock/002这个节点添加一个事件监听器。
如果锁释放了,会唤醒下一个序号的节点,然后重新执行第3步,判断是否自己的节点序号是最小。比如/lock/001
释放了,/lock/002
监听到事件,此时节点集合为[/lock/002,/lock/003]
,则/lock/002
为最小序号节点,获取到锁。