Appearance
Redis 分布式读写锁
项目中常常有使用分布式锁的需求,以 Redis 方式实现最为常见和方便。项目中用到以下两种读写锁:
- Redis 的
setnx方式适用于大部分的写锁,在 Redis 是单实例时,可以读锁 key 和写锁 key 结合起来用 lua 来实现读写锁; - 当线上 Redis 为多实例的集群时,由于不同的 key 可能分布在不同的实例上,没法在 lua 中对不同的 key 进行操作,因此以 hash 这种数据结构来做读写锁;
本文介绍的两种读写锁实现方式比较:
| 单机版 | 集群版 | |
|---|---|---|
| 读锁可重入 | 支持 | 支持 |
| 锁只能被当前线程删除 | 不支持 | 支持 |
| 超时自动释放 | 支持 | 支持 |
| 写锁当前线程可重入 | 不支持 | 不支持 |
单机版是早期项目中使用的方式,后来 Redis 扩容和业务更加复杂后无法满足需求,升级为了集群版,此处推荐直接使用集群版。
下面将这两种方式的实现做详细介绍:
1. 单机版
1.1. 获取读锁
读锁是可重入锁,在写锁不存在时,可以获得读锁。返回 1 表示获取成功,0 表示失败。
| 参数 | 说明 |
|---|---|
| key1 | 读锁 key |
| key2 | 写锁 key |
| ARGV1 | 超时时间(秒) |
Lua
eval "local readKey ,writeKey,timeout = KEYS[1],KEYS[2],ARGV[1]
local wExist = redis.call('EXISTS',writeKey)
if wExist == 1 then
return 0
else
redis.call('SET',readKey,'1','EX',timeout)
end
return 1" 2 readKey writeKey 101
2
3
4
5
6
7
8
2
3
4
5
6
7
8
1.2. 获取写锁
写锁不可重入,当没有读锁,且设置写锁成功时,返回 1 获取成功。
Lua
eval "local readKey ,writeKey,timeout,result = KEYS[1],KEYS[2],ARGV[1]
local rExist = redis.call('EXISTS',readKey)
if rExist == 1 then
return 0
else
result = redis.call('SET',writeKey,'1','EX',timeout,'NX')
if result then
return 1
else return 0
end
end" 2 readKey writeKey 101
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
此种在集群部署的 Redis 中使用存在的问题:
- 读锁用的 key 和写锁用的 key 不在同一个 Redis 分片上时,无法执行 lua 脚本;
- 读线程 A 可能会删除读线程 B 设置的读锁;
2. 集群版
使用 Redis 的 hash 结构,保证同一个 key 在同一个 Redis 实例上。hash 中储存 read 和 write 两个 key,值均为毫秒级时间戳,删除锁的时候对比时间戳是否一致,以保证当前锁只能由当前线程删除或者自动过期。
2.1. 获取读锁

调用时需要传入的参数说明:
| 参数 | 说明 |
|---|---|
| key1 | 锁的 key |
| ARGV1 | key1 的过期时间 |
| ARGV2 | 读锁的超时时间 |
| ARGV3 | 写锁的超时时间 |
| ARGV4 | 当前毫秒级时间戳 |
Lua
local lockKey,timeout,readExpTime,writeExpTime,currentTimeWs = KEYS[1],tonumber(ARGV[1]),tonumber(ARGV[2]),tonumber(ARGV[3]),tonumber(ARGV[4])
local writeValue = redis.call('HGET',lockKey,'write')
local canGet=false
if writeValue then
canGet=((currentTimeWs - tonumber(writeValue))> writeExpTime )
else canGet=true
end
if canGet then
redis.call('HSET',lockKey,'read',currentTimeWs)
redis.call('PEXPIRE',lockKey,timeout)
return 1
else
return 0
end1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
2.2. 删除读锁
删除的时候比较 read 的值
Lua
local lockKey,readTime = KEYS[1],ARGV[1]
local readValue = redis.call('HGET',lockKey,'read')
if readTime==readValue then
redis.call('HDEL',lockKey,'read')
end1
2
3
4
5
2
3
4
5
2.3. 获取写锁

获取写锁时要同时判断读锁和写锁是否存在或者已超时
Lua
local lockKey,timeout,readExpTime,writeExpTime,currentTimeWs = KEYS[1],tonumber(ARGV[1]),tonumber(ARGV[2]),tonumber(ARGV[3]),tonumber(ARGV[4])
local writeValue = redis.call('HGET',lockKey,'write')
local writeValid=false
if writeValue then
writeValid=((currentTimeWs - tonumber(writeValue))> writeExpTime )
else
writeValid=true
end
local readValue = redis.call('HGET',lockKey,'read')
local readValid=false
if readValue then
readValid=((currentTimeWs - tonumber(readValue))> readExpTime )
else
readValid=true
end
if writeValid and readValid then
redis.call('HSET',lockKey,'write',currentTimeWs)
redis.call('PEXPIRE',lockKey,timeout)
return 1
else
return 0
end1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2.4. 删除写锁
同样的,要判断 write 的值是否一致
Lua
local lockKey,writeTime = KEYS[1],ARGV[1]
local writeValue = redis.call('HGET',lockKey,'write')
if writeTime==writeValue then
redis.call('HDEL',lockKey,'write')
end1
2
3
4
5
2
3
4
5
3. 在 java 项目中的调用
Java
private static final String READ_LOCK_SCRIPT = "local lockKey,timeout,readExpTime,writeExpTime,currentTimeWs = KEYS[1],tonumber(ARGV[1]),tonumber(ARGV[2]),tonumber(ARGV[3]),tonumber(ARGV[4]) local writeValue = redis.call('HGET',lockKey,'write') local canGet=false if writeValue then canGet=((currentTimeWs - tonumber(writeValue))> writeExpTime ) else canGet=true end if canGet then redis.call('HSET',lockKey,'read',currentTimeWs) redis.call('PEXPIRE',lockKey,timeout) return 1 else return 0 end ";
private static final String WRITE_LOCK_SCRIPT = "local lockKey,timeout,readExpTime,writeExpTime,currentTimeWs = KEYS[1],tonumber(ARGV[1]),tonumber(ARGV[2]),tonumber(ARGV[3]),tonumber(ARGV[4]) local writeValue = redis.call('HGET',lockKey,'write') local writeValid=false if writeValue then writeValid=((currentTimeWs - tonumber(writeValue))> writeExpTime ) else writeValid=true end local readValue = redis.call('HGET',lockKey,'read') local readValid=false if readValue then readValid=((currentTimeWs - tonumber(readValue))> readExpTime ) else readValid=true end if writeValid and readValid then redis.call('HSET',lockKey,'write',currentTimeWs) redis.call('PEXPIRE',lockKey,timeout) return 1 else return 0 end";
private static final String DEL_READ_LOCK_SCRIPT = "local lockKey,readTime = KEYS[1],ARGV[1] local readValue = redis.call('HGET',lockKey,'read') if readTime==readValue then redis.call('HDEL',lockKey,'read') end";
private static final String DEL_WRITE_LOCK_SCRIPT = "local lockKey,writeTime = KEYS[1],ARGV[1] local writeValue = redis.call('HGET',lockKey,'write') if writeTime==writeValue then redis.call('HDEL',lockKey,'write') end";
private static Integer LOCK_KEY_TIMEOUT = 10 * 1000;
private static Integer LOCK_READ_KEY_TIMEOUT = 1 * 1000;
private static Integer LOCK_WRITE_KEY_TIMEOUT = 3 * 1000;
private String getReadLock(String lockKey,Integer lockTimeOut,Integer readTimeOut, Integer writeTimeOut){
RedisTemplete redis = routeRedis(lockKey);//根据 key 路由出 Redis 客户端
String value = String.valueOf(System.currentTimeMillis());
List<String> args = new ArrayList<>(4);
args.add(lockTimeOut.toString());
args.add(readTimeOut.toString());
args.add(writeTimeOut.toString());
args.add(value);
Long result = (Long) redis.eval(READ_LOCK_SCRIPT, lockKey, args);
if(Long.valueOf(1).equals(result)){
return value;
}
return null;
}
private String getWriteLock(String lockKey,Integer lockTimeOut,Integer readTimeOut, Integer writeTimeOut){
RedisTemplete redis = routeRedis(lockKey);//根据 key 路由出 Redis 客户端
List<String> keys = new ArrayList<>(1);
keys.add(lockKey);
String value = String.valueOf(System.currentTimeMillis());
List<String> args = new ArrayList<>(4);
args.add(lockTimeOut.toString());
args.add(readTimeOut.toString());
args.add(writeTimeOut.toString());
args.add(value);
Long result = (Long) redis.eval(WRITE_LOCK_SCRIPT, keys, args);
if(Long.valueOf(1).equals(result)){
return value;
}
return null;
}
private void delWriteLock(String lockKey,String value){
RedisTemplete redis = routeRedis(lockKey);//根据 key 路由出 Redis 客户端
List<String> keys = new ArrayList<>(1);
keys.add(lockKey);
List<String> args = new ArrayList<>(1);
args.add(value);
redis.eval(DEL_WRITE_LOCK_SCRIPT, keys, args);
}
private void delReadLock(String lockKey,String value){
RedisTemplete redis = routeRedis(lockKey);//根据 key 路由出 Redis 客户端
List<String> keys = new ArrayList<>(1);
keys.add(lockKey);
List<String> args = new ArrayList<>(1);
args.add(value);
redis.eval(DEL_READ_LOCK_SCRIPT, keys, args);
}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
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