分布式锁
分布式锁解决的是多进程的并发问题。
确保分布式锁可用,至少要确保锁的实现满足以下四个条件:
- 互斥性:在任意时刻,只有一个客户端能持有锁;
- 不会发生死锁:即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁(锁可重入/设置超时时间实现);
- 具有容错性:只要大部分节点正常运行,客户端就可用加锁和解锁;
- 加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
一般的几种实现方式:数据库自己的排他锁,数据库乐观锁,基于Redis等缓存的分布式锁,基于ZK的分布式锁。
1.ZK
获取分布式锁的思路(临时顺序节点+watcher):
- ZK集群下创建一个持久节点locker,获取分布式锁的时候在locker节点下创建临时顺序节点,释放锁的时候删除该临时节点;
- 客户端调用createNode()在locker下创建临时顺序节点,然后调用getChildren(“locker”)来获取locker下面的所有子节点;
- 客户端获取到所有的子节点path之后,如果发现自己创建的子节点序号最小,那么就认为该客户端获取到了锁;
- 如果发现自己创建的节点并不是最小的,说明没有获取到锁,此时客户端需要找到最小的节点,对其调用exist()方法,同时对其注册事件监听器。
- 等到这个最小的节点释放锁,节点被删除,客户端的Watcher会收到相应通知,次吃再判断自己创建的节点是不是最小的,如果是则获取到锁,如果不是再重复上述步骤。
2.Redis
-
加锁:
jedis.set(String key, String value, String nxxx, String expx, int time)
- key:用key当锁,key是唯一的;
- value:传入requestId,这样就可用知道这把锁是哪个请求加的了,在解锁的时候有依据。可用使用
UUID.randomUUID().toString()
来生成。 - nxxx:传入nx,当key不存在时进行set操作,如果已经存在,不做任何操作;
- expx:传图px,给这个key加过期时间;
- time:代表key的过期时间。
-
解锁:
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
使用Lua脚本,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁。可以保证判断和删除两步操作的原子性。
Jedis客户端加锁是不可重入的,所以设置超时时间防止死锁发生。
Redisson客户端可以实现可重入锁。