redisson version:2.1.4
去查看源码的原因:
以前简单理解的基于redis的锁,就是去setnx一个key,然后设置一个过期时间。多线程的时候,谁能set成功,说明谁获取锁成功。
然后解锁操作就是删除之前的key或者等待过期。
现在用了下redisson的锁,开始也以为是这样的,认为其他线程也能强制解锁。结果测试发现不行。
源码:
获取锁
<span style="font-size:14px;">private Long tryLockInner(final long leaseTime, final TimeUnit unit) {
internalLockLeaseTime = unit.toMillis(leaseTime);
return commandExecutor.evalWrite(getName(), RedisCommands.EVAL_INTEGER,
//step1
"local v = redis.call('get', KEYS[1]); " +
"if (v == false) then " +
//step2
" redis.call('set', KEYS[1], cjson.encode({['o'] = ARGV[1], ['c'] = 1}), 'px', ARGV[2]); " +
" return nil; " +
"else " +
" local o = cjson.decode(v); " +
" if (o['o'] == ARGV[1]) then " +
//step3
" o['c'] = o['c'] + 1; redis.call('set', KEYS[1], cjson.encode(o), 'px', ARGV[2]); " +
" return nil; " +
" end;" +
" return redis.call('pttl', KEYS[1]); " +
"end",
//这个getName()就是加锁的key, 后面有2个参数, 加锁线程的唯一标识 和 锁定时间(毫秒)
Collections.<Object>singletonList(getName()), id.toString() + "-" + Thread.currentThread().getId(), internalLockLeaseTime);
}</span>
一段lua脚本,我加了一点注释.
本来刚开始看的时候,最上面step1通过先比较,在赋值的方式,这会存在竞态条件,不过redis的服务器是单线程执行的,所以没问题。
如果key不存在,到step2,保存了一个json的字符串值,下面是我本机的值。
"{\"o\":\"e0969e37-9520-4ca9-9995-48920756d159-23\",\"c\":1}"
这里取值的时候通过如下方式
eval "local v = redis.call('get', KEYS[1]); return v" 1 redisson.lock.selectId
如果从命令行直接输入 get redisson.lock.selectId, 则返回nil。获取不到值
如果可以存在,进入step3,
这里是可重入锁,如果当前加锁的线程是之前成功获取到锁的线程,则把获取锁的数量+1。
上面脚本的完整版本就是 eval "${上面的脚本}" 1(key的数量) redisson.lock.selectId(锁的key) e0969e37-9520-4ca9-9995-48920756d159-23(锁定线程唯一标识) 300000(我锁定300秒)
解锁
public void unlock() {
Boolean opStatus = commandExecutor.evalWrite(getName(), RedisCommands.EVAL_BOOLEAN_R2,
"local v = redis.call('get', KEYS[1]); " +
//step1,无此key,则不需要其他操作,通知其他等锁的线程
"if (v == false) then " +
" redis.call('publish', ARGV[4], ARGV[2]); " +
" return true; " +
"else " +
" local o = cjson.decode(v); " +
//step2,依然先判断当前想要解锁的线程 是否是之前成功获取到锁的线程
//是,继续往下
//否,进入step6
" if (o['o'] == ARGV[1]) then " +
//step3,锁的次数先-1,对应上面加锁(锁可重入)
" o['c'] = o['c'] - 1; " +
" if (o['c'] > 0) then " +
//step4,说明发生了锁重入,解锁次数不是一次就完成的,更新当前锁的次数
" redis.call('set', KEYS[1], cjson.encode(o), 'px', ARGV[3]); " +
" return false;"+
" else " +
//step5,已完全解锁,删除key
" redis.call('del', KEYS[1]);" +
" redis.call('publish', ARGV[4], ARGV[2]); " +
" return true;"+
" end" +
" end;" +
//step6,对应step2的条件分支
" return nil; " +
"end",
Collections.<Object>singletonList(getName()), id.toString() + "-" + Thread.currentThread().getId(), unlockMessage, internalLockLeaseTime, getChannelName());
if (opStatus == null) {
throw new IllegalStateException("Can't unlock lock Current id: "
+ id + " thread-id: " + Thread.currentThread().getId());
}
if (opStatus) {
stopRefreshTask();
}
}
基本解锁流程见上面注释即可。
完整的执行脚本如下。
eval "${上面的脚本}" 1(key的数量) redisson.lock.selectId(锁的key,即KEYS[1]) b69c1b77-8ff8-4dd3-9c90-012eee74bd34-23(锁定线程唯一标识) 0 300000 redisson__lock__channel__{redisson.lock.selectLoan}
最后4个值就是脚本里面的ARGV[1],ARGV[2],ARGV[3],ARGV[4]
redisson:3.10.2
redisson对应的是redis cluster时,会有专门的线程进行类似的心跳检测,使用的线程的配置在
org.redisson.config.Config --> private int nettyThread = 32;
这个值可以进行调整,在测试环境发现进行检测是一直都在线程序号为2的线程(即一直是一个线程),比如调小,减少资源开销。
欢迎关注微信公众号: 假程序员