Redis -- 分布式锁

方案

setnx + del

1
2
3
4
5
> setnx lock:user true
OK
# 执行业务逻辑
> del lock:user
(integer) 1

存在的问题:如果逻辑执行过程中出现异常,可能会导致del指令没有被调用,陷入死锁,锁永远不会被释放

setnx + expire + del

1
2
3
4
5
6
7
> setnx lock:user true
OK
> expire lock:user 10
(integer) 1
# 执行业务逻辑
> del lock:user
(integer) 1

存在的问题:如果setnxexpire之间的服务器进程突然挂掉,会导致expire无法执行,也会造成死锁
问题根源:setnxexpire是两条独立的指令,而不是原子指令

set扩展参数

1
2
3
4
5
> set lock:user true ex 5 nx # setnx和expire是组合在一起的原子指令
OK
# 执行业务逻辑
> del lock:user
(integer) 1

超时问题

Redis的分布式锁不能解决超时问题,因此Redis分布式锁不要用于较长时间的任务

Lua脚本

Lua脚本可以保证连续多个指令原子性执行,但该方案并不完美,只是相对安全一点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
tag = random.nextint()  # 随机数
if redis.set(key, tag, nx=True, ex=5):
do_something()
# 释放锁时先匹配随机数是否一致,然后再删除Key
# 这样可以确保当前线程占有的锁不会被其它线程释放,除非这个锁是过期了被服务器自动释放
# 如果真的超时,当前线程的逻辑没有执行完成,其它线程也会趁虚而入
redis.delifequals(key, tag)

# delifequals
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end

可重入性

Redis分布式锁如果要支持可重入,需要对客户端的set方法进行包装,使用线程的ThreadLocal变量存储当前持有锁的计数
不推荐使用可重入锁,因为加重了客户端的复杂性,可以通过在业务层面调整来避免使用可重入的Redis分布式锁

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class RedisWithReentrantLock {
private final ThreadLocal<Map<String, Integer>> lockers = new ThreadLocal<>();
private Jedis jedis;

public RedisWithReentrantLock(Jedis jedis) {
this.jedis = jedis;
}

private boolean doLock(String key) {
// TODO 可以进一步细化考虑过期时间,代码复杂度将进一步提升
return jedis.set(key, "", "nx", "ex", 5L) != null;
}

private void doUnlock(String key) {
jedis.del(key);
}

private Map<String, Integer> currentLockers() {
Map<String, Integer> refs = lockers.get();
if (refs != null) {
return refs;
}
lockers.set(Maps.<String, Integer>newHashMap());
return lockers.get();
}

public boolean lock(String key) {
Map<String, Integer> refs = currentLockers();
Integer refCnt = refs.get(key);
if (refCnt != null) {
refs.put(key, refCnt + 1);
return true;
}
boolean ok = this.doLock(key);
if (!ok) {
return false;
}
refs.put(key, 1);
return true;
}

public boolean unlock(String key) {
Map<String, Integer> refs = currentLockers();
Integer refCnt = refs.get(key);
if (refCnt == null) {
return false;
}
refCnt -= 1;
if (refCnt > 0) {
refs.put(key, refCnt);
} else {
refs.remove(key);
this.doUnlock(key);
}
return true;
}

public static void main(String[] args) {
String lockName = "zhongmingmao";
RedisWithReentrantLock redis = new RedisWithReentrantLock(new Jedis("localhost", 16379));
System.out.println(redis.lock(lockName));
System.out.println(redis.lock(lockName));
System.out.println(redis.unlock(lockName));
System.out.println(redis.unlock(lockName));
}
}
0%