一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
1 前言
研究源码 是为了解决实际问题 - 雨夜
zl(沈阳)-皓哥 是我目前遇到的最好的老大
先看 思考和应用场景的问题 思考下,然后再继续往下看
如果可以的话 点个赞/评论下 哈哈 好有点动力 有问题 也请留言
2 上节回顾/本节重点/上节思考题
2.1 上节讲了加锁/解锁/watch dog/时间轮等概念
2.2 本节讲解
1. 公平锁原理
2.3 上节 思考题解答
3 思考
4 应用场景
5 公平锁 lock 步骤
5.1 RedissonFairLock 类 tryLockInnerAsync方法
5.1.1 参数
KEYS[1]
实际 lockKey
KEYS[2]
threadsQueueName
实际 redisson_lock_queue:{lockKey}
KEYS[3]
timeoutSetName
实际 redisson_lock_timeout:{lockKey}
ARGV[1]
internalLockLeaseTime
实际 30000
ARGV[2]
getLockName(threadId)
实际 f71f3609-1ec2-40dc-b31b-75aa4b0d7ab6:1
ARGV[3]
threadWaitTime
实际 5000
ARGV[4]
currentTime
实际 当前时间 1650852343221
5.2 加锁步骤
如果我们加锁 先做什么 后做什么
1. 先看等待queue 是否存在等待的lockkey
如果不存在 break
如果存在 看这个元素的timeout 时间是否小于当前时间,如果小于就删除该元素
2 第一次加锁的 需要加锁
3 watch dog执行的逻辑
4 如果抢锁没成功 怎么生成timeout
5.2.1
"while true do " +
//看lock_queue 中是否有元素在等待 如果没有元素 直接退出\
"local firstThreadId2 = redis.call('lindex', ‘redisson_lock_queue:{lockKey}’, 0);" +\
"if firstThreadId2 == false then " +\
"break;" +\
"end;" +\
\
// 获取 第一个等待元素的 时间\
"local timeout = tonumber(redis.call('zscore', redisson_lock_timeout:{lockKey}, firstThreadId2));" +
// 如果这个元素 应该在现在之前执行\
"if timeout <= tonumber(1650852343221) then " +\
// 删除超时的元素\
"redis.call('zrem', redisson_lock_timeout:{lockKey}, firstThreadId2);" +\
"redis.call('lpop', redisson_lock_queue:{lockKey});" +\
"else " +\
"break;" +\
"end;" +\
"end;" +
只有2个情况会break。
1是 等待队列中没有元素
2是等待队列最后一个元素 timeout时间都在当前时间之前,说明所有元素都在当前时间之前
5.2.2
// lockkey不存在占用 并且 (等待队列中不存在 or 等待队列里的就是自身)\
"if (redis.call('exists', lockKey) == 0) " +\
"and ((redis.call('exists', redisson_lock_queue:{lockKey}) == 0) " +\
"or (redis.call('lindex', redisson_lock_queue:{lockKey}, 0) == f71f3609-1ec2-40dc-b31b-75aa4b0d7ab6:1)) then " +\
\
// 该元素已经被处理 需要删除\
"redis.call('lpop', redisson_lock_queue:{lockKey});" +\
"redis.call('zrem', redisson_lock_timeout:{lockKey}, f71f3609-1ec2-40dc-b31b-75aa4b0d7ab6:1);" +\
\
// 其他元素 超时时间都剪掉5s\
"local keys = redis.call('zrange', redisson_lock_timeout:{lockKey}, 0, -1);" +\
"for i = 1, #keys, 1 do " +\
"redis.call('zincrby', redisson_lock_timeout:{lockKey}, -tonumber(5000), keys[i]);" +\
"end;" +\
\
// 加锁成功\
"redis.call('hset', lockKey, f71f3609-1ec2-40dc-b31b-75aa4b0d7ab6:1, 1);" +\
"redis.call('pexpire', lockKey, 30000);" +\
"return nil;" +\
"end;" +
没加锁的 加锁成功
5.2.3
// 如果本线程 加过锁,之后加锁 会每次加一/重置存在时间\
"if redis.call('hexists', lockKey, f71f3609-1ec2-40dc-b31b-75aa4b0d7ab6:1) == 1 then " +\
"redis.call('hincrby', lockKey, f71f3609-1ec2-40dc-b31b-75aa4b0d7ab6:1,1);" +\
"redis.call('pexpire', lockKey, 30000);" +\
"return nil;" +\
"end;" +
watch dog的逻辑
5.2.4
// 获取超时队列的 该线程的超时时间
// 无法获取锁\
//检查线程是否已在队列中\
"local timeout = redis.call('zscore', redisson_lock_timeout:{lockKey}, f71f3609-1ec2-40dc-b31b-75aa4b0d7ab6:1);" +\
"if timeout ~= false then " +\
//真正的超时是前一个线程的超时\
//在队列中,但这大致正确,并且\
//避免遍历队列\
"return timeout - tonumber(5000) - tonumber(当前时间);" +\
"end;" +\
\
// 获取 等待队列中的最后一个插入的元素\
"local lastThreadId = redis.call('lindex', redisson_lock_queue:{lockKey}, -1);" +\
"local ttl;" +
// 最后一个元素不是 自身线程\
"if lastThreadId ~= false and lastThreadId ~= f71f3609-1ec2-40dc-b31b-75aa4b0d7ab6:1 then " +\
"ttl = tonumber(redis.call('zscore', redisson_lock_timeout:{lockKey}, lastThreadId)) - tonumber(当前时间);" +\
"else " +\
"ttl = redis.call('pttl', lockKey);" +\
"end;" +\
"local timeout = ttl + tonumber(5000) + tonumber(当前时间);" +\
"if redis.call('zadd', redisson_lock_timeout:{lockKey}, timeout, f71f3609-1ec2-40dc-b31b-75aa4b0d7ab6:1) == 1 then " +\
"redis.call('rpush', redisson_lock_queue:{lockKey}, f71f3609-1ec2-40dc-b31b-75aa4b0d7ab6:1);" +\
"end;" +\
"return ttl;
8 总结:
9 下节预知
- 加锁运行lua的时候 每个阶段参数都是什么样子 画图展示 2 宕
10 目标:
- 随着学习 把一些坑解决 形成一个 redission的基础组件
代码地址: gitee.com/gf-8/yuye-p…
项目: yuye-test-redission
类地址: 对应的test 包之下FairLockApplicationTests类
11 思考题
怎么验证lua中 每个lua命令返回什么?怎么测试出每行的返回值?