前言 默认的锁定逻辑是不公平的。 当锁失败时,线程会进入while循环,不断尝试获取锁。这时候,多个线程就会竞争。也就是说,谁抢到了就属于谁。 Redisson提供了公平锁机制,使用方法如下: RLock fairLock = redisson.getFairLock("anyLock"); // 最常见的使用方法 fairLock.lock(); 我们来看看公平锁是如何实现的? 1个公平锁 相信小伙伴们看过前面的文章,已经很熟悉了。他们可以直接定位到源码方法:RedissonFairLock#tryLockInnerAsync。 大家好,这么大的代码我都截图不了,那我们直接分析Lua脚本吧。 PS:虽然我们不懂lua,但是这些一堆if else我们大概也能看懂。 因为debug发现command == RedisCommands.EVAL_LONG,所以直接看下面的部分。 这么久,我喊了好几声“好人”! 我们先来看看参数是什么? KEYS[1]:锁的名称,anyLock; KEYS[2]:锁等待队列,redisson_lock_queue:{anyLock}; KEYS[3]:等待队列中线程锁时间的集合,redisson_lock_timeout:{anyLock},根据锁时间戳存入集合中; ARGV[1]:锁定超时30000; ARGV[2]: UUID:ThreadId 组合 a3da2c83-b084-425c-a70f-5d9a08b37f31:1; ARGV[3]:threadWaitTime默认300000; ARGV[4]:currentTime 当前时间戳。 锁定队列和集合是包含大括号的字符串。 {XXXX}表示这个key只使用XXXX来计算slot位置。 2Lua脚本分析 上面的Lua脚本分为几个部分。我们从不同角度看一下上面代码的执行过程。 第一个锁(线程1) 第一部分,因为是第一次加锁,所以等待队列为空,直接跳出循环。这部分执行完毕。 第二部分: 当锁不存在、等待队列为空或者队列头为当前线程,并且两个条件都满足时,进入内部逻辑;从等待队列和超时设置中删除当前线程。此时等待队列和超时设置为空,无需进行任何操作; 减少队列中所有等待线程的超时且无需执行任何操作; 锁定并设置超时。 执行后返回这里。所以我现在不会读接下来的几部分。 相当于下面两个命令(整个Lua脚本是原子的!): > hset anyLock a3da2c83-b084-425c-a70f-5d9a08b37f31:1 1 > pexpire anyLock 30000 线程2锁 Thread1加锁后,Thread2加锁。 Thread2可以是本实例的其他线程,也可以是其他实例的线程。 第一部分中,虽然锁被Thread1占用,但是等待队列为空,直接跳出循环。 第二部分,锁就在那里,直接跳过。 第三部分是线程是否持有锁。如果没有持有锁,则直接跳过。 第四部分是Thread2如果线程在等待队列中是否会锁定该线程。如果不在等待队列中,则直接跳过。 Thread2 将在这里结束: 从线程等待队列中获取最后一个线程 redisson_lock_queue:{anyLock}; 由于等待队列为空,直接获取当前锁的剩余时间ttl anyLock; 组装超时timeout = ttl + 300000 + 当前时间戳,这个300000就是默认的60000*5; 使用zadd将Thread2放入等待线程有序集合中,然后使用rpush将Thread2放入等待队列中。 zadd KEYS[3] 超时 ARGV[2] 这里使用zadd命令放置redisson_lock_timeout:{anyLock}、超时时间戳(1624612689520)、线程(UUID2:Thread2)。 超时时间戳作为分数,用于在有序集合中排序,表示锁定的顺序。 线程3锁 线程1持有锁,线程2正在等待,线程3来了。 获取第一个ThreadId2。此时队列中有线程UUID2:Thread2。 判断firstThreadId2(超时时间戳)的分数是否小于当前时间戳: 如果小于等于超时,则移除firstThreadId2;如果大于该值,则进入后续判断。 第二、三、四部分不符合条件。 Thread3 也将在这里结束: 从线程等待队列中获取最后一个线程 redisson_lock_queue:{anyLock}; 最后一个线程存在并且不是它自己,那么ttl=lastThreadId超时时间戳——当前时间戳是看最后一个线程还有多长时间才超时; 组装超时timeout = ttl + 300000 + 当前时间戳。这个300000就是默认的60000*5。最后一个线程的超时时间加上300000和当前时间戳,即为Thread3的超时时间戳。 使用zadd将Thread3放入等待线程有序集合,然后使用rpush将Thread3放入等待队列。 3 总结 本文主要总结一下公平锁的加锁逻辑。其中涉及到很多Redis操作。以下是一个简短的总结: Redis Hash数据结构:存储当前锁,Redis Key是锁,Hash字段是加锁线程,Hash值是重入次数; Redis List数据结构:充当线程等待队列。新的等待线程将使用rpush命令放置在队列的右侧; Redis Sorted Set有序集数据结构:存储等待线程的顺序,得分作为等待线程的超时时间戳。 需要理解的是,这里会额外增加一个等待队列和有序集合。 本文转载自微信公众号“程序员小航”,可以通过以下二维码关注。转载本文请联系程序员小航公众号。