关键点
有四个关键点:
- 原子性
- 过期时间
- 锁续期
- 正确释放锁
原子性
按照分布式锁的实现比如用两个命令:1
2redis.set(key,value);
redis.expire(key, time, unit);
但是这个不是原子性的。比如设置了key和value,但是没有设置失效时间,则会出现死锁的问题。
过期时间
这个没啥可说的,分布式锁防止死锁的做法。
锁续期
当业务执行时间超过超时时间,则可能出现其他客户端获取锁从而并发执行,失去了分布式锁的意义。这时候就是锁续期。
开辟另外一个线程,专门用于锁续期,加锁的时候就起个线程进行死循环续期,核心流程就是判断锁的时间过了三分之一则就重新续期为上锁时间。 比如,加锁时间是3s,执行1s没有释放锁之后,会为这个key的锁重新设置超时时间为3s。
正确释放锁
释放锁可能有问题:
- 可能释放别人的锁。其实还是上边提到的问题,比如锁设置了3s的超时时间,执行了4s,但是这时别的客户端或者线程获取了锁,所以可能删除了其他客户端的锁。
- 而且别的客户端可能上来就是搞破坏,可能有代码实现的问题,未加锁就解锁。
这两个问题总结起来就是要正确的释放锁。
- 利用锁续期机制,防止业务没执行完成就释放锁的情况
- 释放的时候要判断是否为自己加的锁,避免释放别人的锁。
关于锁续期的伪代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39// 标识当前锁是否在运行中
private volatile boolean isRunning;
// 抢锁成功
if (RESULT_OK.equals(client.setNxPx(key, value, ttl))) {
// 启动续期线程
renewalTask = new RenewTask(new IRenewalHandler() {
@Override
public void callBack() throws LockException {
// 刷新值
client.expire(key, ttl <= 0 ? 10 : ttl);
}
}, ttl);
renewalTask.start();
}
// 省略释放锁的代码 里面也要维护isRunning
// 续期线程的逻辑
@Override
public void run() {
while (isRunning) {
try {
// 1、续租,刷新值
call.callBack();
LOGGER.info("续租成功!");
// 2、三分之一过期时间续租
TimeUnit.SECONDS.sleep(this.ttl * 1000 / 3);
} catch (InterruptedException e) {
close();
} catch (LockException e) {
close();
}
}
}
public void close() {
isRunning = false;
}
另一个是要在删除时判断出是否为此次加锁,因为是分布式的,线程id可能重复,所以不能单单用线程id作为value,这里建议是用当前业务上下文中的userId或者其他随机数。但这里也要注意一下线程安全问题,因为在解锁(删除锁)时如果是先判断再删除,则有原子性问题:1
2
3
4// 代码有原子性问题
if (userId).equals(redis.get(lockKey)) {
redis.delete(key);
}
此原子性问题可能是:比如在get之后,del之前锁超时自动释放,那么可能在执行del的时候还是删除了刚刚加锁的其他客户端。当然如果保证了锁续期,那么这里不会出现此原子性问题,但是还是建议这里判断删除是用lua脚本提供原子性:1
2
3
4
5
6// 如果get的值等于传进来的值,就给它del
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
Redis的部署方式
- 单机
单机模式下面就是一台redis为分布式锁的存储,优点是方便易部署,缺点是存在单机故障。 - 哨兵(Sentinel)
有单点故障,会搞几个slave从节点做备份,redis很好支持了sentinel模式。
但是这里涉及master和slave的数据一致性问题。锁写到master,没有同步到slave,slave选举为master之后slave中没有锁,相当于此时其他客户端也能加锁。 - 集群
集群只是做了slot分片,降低了一定概率,锁还是只写到一个master上,和哨兵一样存在数据一致性的问题。 - 红锁(RedLock)
红锁的思路是:搞几个独立的master节点,比如5个,然后挨个加锁,只要超过一半以上(这里是3个)就代表加锁成功,这样一台master成功,还有其他的master节点,不耽误。虽然解决了上面集群模式的问题,但是性能影响很大,且集群维护也需要部署成本。