Redission 公平锁(一)

254 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

1 前言

研究源码 是为了解决实际问题 - 雨夜

zl(沈阳)-皓哥 是我目前遇到的最好的老大

先看 思考和应用场景的问题 思考下,然后再继续往下看

如果可以的话 点个赞/评论下 哈哈 好有点动力 有问题 也请留言

2 上节回顾/本节重点/上节思考题

2.1 上节讲了加锁/解锁/watch dog/时间轮等概念

2.2 本节讲解

1. 公平锁原理

2.3 上节 思考题解答

3 思考

image.png

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 总结:

Redission 可重入锁(二).png

9 下节预知

  1. 加锁运行lua的时候 每个阶段参数都是什么样子 画图展示 2  宕

10 目标:

  1. 随着学习 把一些坑解决 形成一个 redission的基础组件

代码地址: gitee.com/gf-8/yuye-p…

项目: yuye-test-redission

类地址: 对应的test 包之下FairLockApplicationTests类

11 思考题

怎么验证lua中 每个lua命令返回什么?怎么测试出每行的返回值?