(阅读本文需要有使用redis/lua的基础)
目录
1.单实例redis:
1.1获取读锁
1.2获取写锁
2.redis集群版:
2.1获取读锁
2.2 删除读锁
2.3 获取写锁
2.4 删除写锁
3.在java项目中的调用:
项目中常常有使用分布式锁的需求,以redis方式实现最为常见和方便。项目中用到以下两种读写锁:
1.redis的setnx方式适用于大部分的写锁,在redis是单实例时,可以读锁key和写锁key结合起来用lua来实现读写锁。
2.当线上redis为多实例的集群时,由于不同的key可能分布在不同的实例上,没法在lua中对不同的key进行操作,因此以hash这种数据结构来做读写锁
本文介绍的两种读写锁实现方式比较:
单实例版 | 集群版 | |
读锁可重入 | 支持 | 支持 |
锁只能被当前线程删除 | 不支持 | 支持 |
超时自动释放 | 支持 | 支持 |
写锁当前线程可重入 | 不支持 | 不支持 |
单实例版是早期项目中使用的方式,后来redis扩容和业务更加复杂后无法满足需求,升级为了集群版。此处推荐直接使用集群版
下面将这两种方式的实现做详细介绍:
读锁是可重入锁,在写锁不存在时,可以获得读锁。返回1表示获取成功,0表示失败。
参数说明
key1 读锁key
key2 写锁key
ARGV1 超时时间(秒)
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 10
写锁不可重入,当没有读锁,且设置写锁成功时,返回1获取成功
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 10
此种在集群部署的redis中使用存在的问题:
1.读锁用的key和写锁用的key不在同一个redis分片上时,无法执行lua脚本
2.读线程A可能会删除读线程B设置的读锁
使用redis的hash结构,保证同一个key在同一个redis实例上。hash中储存read和write两个key,值均为毫秒级时间戳,删除锁的时候对比时间戳是否一致,以保证当前锁只能由当前线程删除或者自动过期。
调用时需要传入的参数说明:
key1 锁的key
ARGV1 key1的过期时间
ARGV2 读锁的超时时间
ARGV3 写锁的超时时间
ARGV4 当前毫秒级时间戳
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
删除的时候比较read的值
local lockKey,readTime = KEYS[1],ARGV[1]
local readValue = redis.call('HGET',lockKey,'read')
if readTime==readValue then
redis.call('HDEL',lockKey,'read')
end
获取写锁时要同时判断读锁和写锁是否存在或者已超时
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
同样的,要判断write的值是否一致
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 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 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 keys = new ArrayList<>(1);
keys.add(lockKey);
String value = String.valueOf(System.currentTimeMillis());
List 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 keys = new ArrayList<>(1);
keys.add(lockKey);
List 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 keys = new ArrayList<>(1);
keys.add(lockKey);
List args = new ArrayList<>(1);
args.add(value);
redis.eval(DEL_READ_LOCK_SCRIPT, keys, args);
}