基于 Redis 和 ZooKeeper 的分布式锁
Redis 锁
SET key value EX 120 NX
该命令是原子操作,表示只有在 key 不存在的情况下,才会赋值成功,并且 120 秒后会自动删除,这样就实现了带超时时间的互斥锁功能,获得锁的程序删除 key 就是释放了锁,如果程序出错退出,达到超时时间后也会保证锁能被释放
这种方法在比较极端的情况下可能失效
程序 A 获取锁
master 将数据同步到 slave 之前 master 宕机了
slave 被选为 master 且没有 A 的锁
程序 B 获取锁成功,这时候程序 A 还在正常运行,导致两个程序同时获得锁
Redisson
https://redisson.org/
Redis Java Client with features of In-Memory Data Grid
Redisson 是基于 redis 的一个 Java 程序,提供了非常丰富的功能,其中就包括了锁
Redisson 提供了多种锁,具体可参考官网
https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers
这里以基本的 Lock 为例子
RLock lock = redisson.getLock("myLock");
// traditional lock method
lock.lock();
// or acquire lock and automatically unlock it after 10 seconds
lock.lock(10, TimeUnit.SECONDS);
// or wait for lock aquisition up to 100 seconds
// and automatically unlock it after 10 seconds
boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS);
if (res) {
try {
...
} finally {
lock.unlock();
}
}
ZooKeeper 锁
ZooKeeper 的节点叫 ZNode,共有 4 种类型
- 持久节点 (PERSISTENT)
- 持久节点顺序节点(PERSISTENT_SEQUENTIAL)
- 临时节点(EPHEMERAL)
- 临时顺序节点(EPHEMERAL_SEQUENTIAL)
通过创建临时节点获取锁
只有节点不存在时才能创建成功
临时节点只有在 client 和 server 保持连接的时候才存在,session 断开后会被 server 删除
阻塞模式下,可以设置获取锁的超时时间,没能成功获取会抛异常
似乎没有可以设置成功获取锁后多长时间内会自动释放
删除节点就是释放锁
存在程序还在跑但 session 断开的异常情况要处理
通过创建临时顺序节点获取锁
同一个节点下的临时顺序节点,会按照创建的先后顺序编号,可以实现排队机制
程序 A B C 先后在同一个节点下创建临时顺序节点
程序在成功创建后会检查所有临时顺序节点,判断自己是不是编号最小的
A 程序发现自己是最小的于是 A 拿到锁
B 程序发现 A 的编号在自己之前,于是等待并通过 watcher 监听 A 创建的节点
C 程序发现 B 的编号在自己之前,于是等待并通过 watcher 监听 B 创建的节点
A 程序删除节点释放锁
B 程序通过 watcher 得知,并再次检查自己是否编号最小,如果是就获取锁
B 程序删除节点释放锁
C 程序通过 watcher 得知,并再次检查自己是否编号最小,如果是就获取锁
ZooKeeper 的性能应该会比 Redis 差些
# coding=utf-8
from kazoo.client import KazooClient
from kazoo.client import KazooState
zk = KazooClient(host="localhost:2181")
zk.start()
def my_listener(state):
if state == KazooState.LOST:
print("KazooState.LOST")
elif state == KazooState.SUSPENDED:
print("KazooState.SUSPENDED")
else:
print("KazooState.CONNECTED")
zk.add_listener(my_listener)
# 通过直接创建节点获取锁
try:
zk.create('/test/lock/resource', b'app name', makepath=True, ephemeral=True, sequence=False)
except:
print("failed to get the lock")
exit()
zk.delete('/test/lock/resource')
# 通过三方包的功能获取锁
lock = zk.Lock('/test/lock/resource', b'app name')
if lock.acquire(blocking=True, timeout=None, ephemeral=True):
# acquire 函数会使用 sequence=True 在 /test/lock/resource 节点下用包含 uuid 的名字创建子节点,这样节点必然创建成功
# 如果子节点序号不是最小且 blocking=True 则注册 watcher 然后等待,否则序号最小则返回 True,不是最小或异常则返回 False
# 如果超过了 timeout 时间还没获取则返回 False
pass
else:
print("failed to get the lock")
exit()
# release 函数会 delete 节点
lock.release()