Java里用Redis的读写锁来搞多线程同步,感觉挺实用也有点复杂
- 问答
- 2026-01-16 14:58:58
- 2
在Java多线程编程里,我们经常遇到一个经典问题:多个线程可以同时读一个数据,这没问题;但只要有一个线程想写这个数据,那就得“清场”,不能有其他线程在读或写,简单用synchronized或者ReentrantLock可以保证安全,但代价是即使所有线程都只是读,也得一个个排队,效率太低了,这时候,如果能用上Redis实现的分布式读写锁,就能在分布式环境下解决这个问题,虽然实用,但里面的门道确实不少。
(参考来源:Redis官方文档关于Distributed Locks的说明,以及Java并发编程的常见模式)
得明白读写锁的核心思想:它是两把锁,一把是“读锁”,也叫共享锁;另一把是“写锁”,也叫排他锁,它的规则很简单:
- 当没有线程持有写锁时,任意多个线程都可以同时成功加上读锁。
- 写锁是独占的,一旦某个线程加上了写锁,其他任何线程(无论是想读还是想写)都必须等待。
- 如果一个线程已经持有了读锁,另一个线程再来申请写锁,这个写锁必须等待所有读锁释放,反之,如果一个线程持有了写锁,它后面再来申请读锁,通常是允许的(这叫锁降级,但Redis实现起来要小心)。
在单机Java环境,我们有现成的ReadWriteLock接口和ReentrantReadWriteLock实现,但问题来了,现在都是分布式、微服务架构,你的应用可能部署在多台机器上,它们需要协同操作同一个Redis里的数据,单机的锁只能锁住当前JVM进程内的线程,管不了其他机器上的JVM进程,我们必须用一个所有服务实例都能访问到的中间件——比如Redis——来实现一个分布式的读写锁。
(参考来源:基于Redis的分布式锁实现原理,通常依赖SET命令的NX/PX参数)

用Redis搞一个基本的写锁(排他锁)还算直观,最经典的做法就是用SET命令,带上NX(不存在才设置)和PX(设置过期时间)选项,线程A要写数据,它就在Redis里创建一个特定的键,比如lock:resource_1,值可以是一个唯一标识(比如UUID),并设置一个过期时间(比如10秒),如果设置成功了,就表示线程A抢到了写锁,其他线程再来设置同样的键,会因为NX选项而失败,只能等待,等线程A操作完毕,它需要检查一下这个锁是不是还是自己的(通过比较值),如果是,就删除这个键来释放锁,这里有个关键点,设置值和过期时间必须是一个原子操作,否则如果设置完值还没来得及设过期时间进程就挂了,这个锁就永远不被释放,成“死锁”了,所以直接用SET key random_value NX PX 10000是标准做法。
(参考来源:Redis作者Antirez提出的Redlock算法,以及社区对分布式读写锁的扩展讨论)
但读写锁的复杂之处主要体现在读锁上,怎么实现“多个读锁可以共存,但读锁和写锁互斥”呢?一个常见的思路是:

- 加读锁: 线程想加读锁时,它不能只用一个简单的键,因为它要允许其他读锁共存,它可能要用到一个计数器和一个记录持有者的集合,为资源
resource_1创建两个键:rwlock:resource_1:mode- 这个键的值记录当前锁的模式,是read还是write。rwlock:resource_1:readers- 这个可能是一个Set或者一个计数器,用来记录所有当前持有读锁的线程标识。
- 加锁过程需要Lua脚本来保证原子性,脚本里需要判断:如果当前锁模式是
write(有写锁),那么加读锁失败;如果当前是read模式或者无锁,那么允许加锁,并将自己的标识加入到readers中,同时可能更新一下过期时间。 - 加写锁: 写锁的申请就更严格了,它的Lua脚本需要检查:
readers集合必须是空的(或者计数器为0),其次mode不能是write(表示没有其他写锁),只有条件都满足,才能设置mode为write,并设置自己的标识。
这个过程听起来就比单机锁复杂多了,因为所有的状态判断和修改都必须是原子性的,否则在高并发下会乱套,这就是为什么必须依赖Redis的Lua脚本——它能确保一连串操作在执行时不会被其他命令打断。
(参考来源:实际应用中关于锁续期、避免误释放等问题的讨论)
除了基本逻辑,还有一堆让人头疼的细节:
- 锁续期(Watchdog): 你不可能把锁的过期时间设得特别长,那万一持有锁的线程因为GC或者网络延迟操作超时了,锁自动释放,会导致数据不一致,但如果设短了,可能操作没做完锁就没了,所以通常要有个后台线程给快过期的锁“续命”,这就是看门狗机制,这又增加了实现的复杂性。
- 避免误释放: 释放锁的时候必须核验身份,线程A加的锁,只能由线程A来释放,你不能因为锁的键存在就删,可能你拿到锁的时候已经过期了,已经被线程B占用了,如果你直接删除,就把B的锁给释放了,所以释放时要用Lua脚本比较值是否还是自己当初设置的那个。
- 公平性: 上面这种简单的实现可能是不公平的,比如一堆读锁等着,写锁可能一直抢不到(因为只要一直有新的读请求,读锁计数器就一直不为零),要实现公平的读写锁,可能需要引入队列机制,那就更复杂了,性能开销也更大。
所以说,在Java里用Redis搞读写锁,想法很美好,现实很骨感,它确实解决了分布式同步的核心需求,非常实用,但你要自己从零开始实现一个健壮、高效、公平的分布式读写锁,需要考虑的边界条件和需要实现的细节非常多,稍有不慎就会埋下坑,这也是为什么很多项目会选择直接使用已经经过大量实践检验的第三方库,比如Redisson,它里面就已经封装好了分布式的RReadWriteLock,其内部帮你处理了上述绝大部分复杂问题,让你能用类似单机ReadWriteLock的简单接口,享受到分布式锁的能力,但即使你用的是现成的库,理解它底层可能存在的复杂性和潜在风险,对于正确使用和排查问题也是至关重要的。
本文由雪和泽于2026-01-16发表在笙亿网络策划,如有疑问,请联系我们。
本文链接:https://www.haoid.cn/wenda/81855.html
