1 基本的实现方式
分布式锁的主要实现方式主要有以下几种:mysql,zookeeper和redis。下面依次介绍这三种组件实现分布式锁的方式。
2 分布式锁的几个问题
在使用分布式锁的时候,需要考虑如下几个问题:假如我们在多线程下拿锁(多线程和多进程是一样的):
- 假如设置了锁的有效期,在有效期内线程没有执行完怎么办。
- 假如不设置锁的有效期,线程异常挂掉,所有线程都拿不到锁了怎么办。
带着这样的问题,看下面的实现。
3 mysql实现分布式锁
先select,如果没有则,insert一个key,insert成功则拿到锁,insert不成功则拿不到锁。如果第一次select到了,那么用select for update操作施加一个x锁,for update拿到数据则拿锁成功。
锁的有效期就是线程的有效期,线程如果中断了或者异常退出了,一般都会捕获异常,回滚事务,释放锁。
4 zookeeper实现分布式锁
zookeeper实现分布式锁是利用的zookeeper可以创建临时节点的机制,同时利用了zk的watcher机制,如果不太熟悉zk的可以看我的zk文章,一个常见的使用zk作为分布式锁的方法为:
- 线程在zk上创建临时节点,如果创建成功,则持有节点,并拿到锁;如果创建失败,则监听这个节点。
- 线程退出,正常退出则删除节点,异常中断则,当SESSIONEXPIRED(可设置)之后,zk会把这个会话建立的临时数据移除。同时,所有等待这个节点的线程会收到通知。
- 收到通知的线程都去拿锁,创建临时节点,但是只有一个可以创建成功。拿到锁。
细心的读者可能发现了问题,所有的线程都去拿锁,这就是惊群效应,那么如何避免呢?zk还提供了一种创建模式,可以创建顺序节点,那么该怎么做呢?
- 所有线程创建顺序节点,并且watch本节点的前一个节点。
- 如果本节点的前一个节点被删除,则拿到锁。
- 本线程做完工作则delete节点,异常退出则等待超时。
zk的实现机制很完美,有什么问题呢?
zk的事务提交流程比较长,在集群下需要非leader转发到leader,leader发起投票,过半提交后leadercommit事务,follower 提交事务。整个流程比较长,有些高并发场景下可能不太适用,而且新增了一个zk组件,不便维护。
3 redis实现分布式锁
redis的分布式锁实现,要依赖于原子操作和setnx命令。setnx拿到锁并同时设置超时时间的原子操作命令为:
SET key value [EX seconds] [PX milliseconds] [NX|XX]
实例:SET resource-name any-string NX EX max-lock-time
这个命令解决了
假如不设置锁的有效期,线程异常挂掉,所有线程都拿不到锁了怎么办。
的问题。但是续期怎么做呢。
有两种做法:
第一种:
- 第一种是每次set key的时候set一个唯一id(可以用时间+mac+线程号)。
- 每次释放的时候看是不是自己的锁,放置过期了被别人拿到了,又把别人的锁释放了。但是这一步需要原子操作,可以用lua脚本:
"if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
"return nil;" +
"end; " +
// 将该客户端对应的锁的 hash 结构的 value 值递减为 0 后再进行删除
// 然后再向通道名为 redisson_lock__channel publish 一条 UNLOCK_MESSAGE 信息
"local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
"if (counter > 0) then " +
"redis.call('pexpire', KEYS[1], ARGV[2]); " +
"return 0; " +
"else " +
"redis.call('del', KEYS[1]); " +
"redis.call('publish', KEYS[2], ARGV[1]); " +
"return 1; "+
"end; " +
"return nil;",
第二种,使用看门狗:
- set key的时候需要set进线程id
- Watch Dog 机制其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到一个 RedissonLock.EXPIRATION_RENEWAL_MAP里面,然后每隔 10 秒 (internalLockLeaseTime / 3) 检查一下,如果客户端 1 还持有锁 key(判断客户端是否还持有 key,其实就是遍历 EXPIRATION_RENEWAL_MAP 里面线程 id 然后根据线程 id 去 Redis 中查,如果存在就会延长 key 的时间),那么就会不断的延长锁 key 的生存时间。
- 锁释放也需要lua脚本。