读写锁 RReadWriteLock 源码剖析:高并发下读多写少库存查看的锁降级实践

发布时间:2026/10/12 21:54:23
读写锁 RReadWriteLock 源码剖析:高并发下读多写少库存查看的锁降级实践 在双 11 大促的商品链路中读写流量的比例通常呈现极端的“二八原则”甚至“一九九原则”每当有 1 个用户真正执行下单扣减库存写操作时往往有超过1,000 个用户在反复刷新商品详情页查看实时库存与价格读操作。如果此时架构师图省事无论读还是写全部统一使用一把普通的排他独占锁如lock:stock:sku_1001这意味着那 1000 个仅仅是想看一眼库存的只读请求必须老老实实跟在写请求后面串行排队读请求互相之间明明没有任何数据冲突却被强制互斥挂起商品详情页的平均响应时间瞬间从 15ms 暴涨到数秒系统吞吐量遭受灭顶之灾。针对这种典型的“读多写少”高并发场景最优雅的解法是引入读写锁ReadWriteLock允许多个读线程同时并行持有锁只有写线程进入时才执行排他独占。今天我们深度剖析 Redisson 分布式读写锁RReadWriteLock的底层源码揭开其状态管理的精妙设计更要弄懂为什么必须进行锁降级Lock Downgrading以及为什么在多线程环境下绝对不能尝试锁升级。本地 vs 分布式状态管理的异曲同工之妙在 Java 单机环境中ReentrantReadWriteLock底层通过 AQS 的一个 32 位整型state巧妙地进行了高低位拆分高 16 位记录读锁的共享持有计数Read Count低 16 位记录写锁的独占重入计数Write Count。而在分布式环境下Redisson 面对的是跨 JVM、跨物理服务器的集群并发。它利用 Redis 的String 与 Hash 组合数据结构完美复刻了这一语义。Redisson 读写锁的物理存储结构写锁Write Lock在 Redis 中表现为一个 Hash 结构记录当前持有写锁的客户端UUID:ThreadId与重入次数并在 String 键mode中标记当前锁为独占模式Key: lock:stock:sku_1001 Field: mode - write Field: e1a9c3d4-8b9a:142:write - 1读锁Read Lock如果当前没有写锁多个客户端可以同时申请读锁。Redisson 会在 Hash 中为每个读线程记录各自的计数并在全局read字段上进行原子累加Key: lock:stock:sku_1001 Field: mode - read Field: e1a9c3d4-8b9a:142 - 1 Field: f8c2b1e0-7a3d:205 - 1 Field: total_reads - 2逐行拆解读锁与写锁互斥的 Lua 核心判定1. 读锁获取时的 Lua 逻辑简析-- 1. 判断锁是否存在或者当前锁的模式是否为读模式 local mode redis.call(hget, KEYS[1], mode); if (mode false or mode read) then -- 没有写锁占用允许多个读操作并发进入 redis.call(hset, KEYS[1], mode, read); -- 为当前读线程的租约计数 1 redis.call(hincrby, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; -- 加读锁成功 end; -- 2. 如果当前有写锁且持有写锁的恰好就是当前线程自己允许锁降级的前提 if (mode write and redis.call(hexists, KEYS[1], ARGV[3]) 1) then redis.call(hincrby, KEYS[1], ARGV[2], 1); redis.call(pexpire, KEYS[1], ARGV[1]); return nil; -- 同一线程在持有写锁时允许重入获取读锁 end; -- 3. 其他线程正在持有写锁加读锁失败返回等待时间 return redis.call(pttl, KEYS[1]);核心设计亮点当读锁并发进入时Redis 只做简单的原子计数累加网络延迟极低只要没有写操作发生几千个读请求可以百分之百并行通过详情页吞吐量直接提升了数十倍生产级实战锁降级Lock Downgrading的正确姿势在商品库存变更并刷新缓存的场景中经常需要执行这样的逻辑“修改库存数据 $\rightarrow$ 刷新本地与分布式缓存 $\rightarrow$ 随后继续以只读方式使用最新数据”。很多开发者写完修改逻辑后直接writeLock.unlock()然后再去调readLock.lock()。这是极其危险的漏洞写法在释放写锁与重新获取读锁的这几微秒间隙内其他并发写线程可能会瞬间插进来修改数据导致当前线程后续读取到的数据根本不是刚才自己写入的版本。必须执行标准的**锁降级Lock Downgrading**流程获取写锁执行数据变更与写入在不释放写锁的前提下就地直接获取读锁释放写锁此时线程依然安全地持有读锁其他写线程无法插队修改数据执行只读业务操作最终释放读锁。Service public class SafeProductInventoryService { Autowired private RedissonClient redissonClient; Autowired private ProductMapper productMapper; public void updateAndRefreshCache(String skuId, int delta) { RReadWriteLock rwLock redissonClient.getReadWriteLock(rw:stock: skuId); RLock writeLock rwLock.writeLock(); RLock readLock rwLock.readLock(); // 1. 获取写锁 writeLock.lock(); try { // 2. 执行核心写库操作 productMapper.deductStock(skuId, delta); // 3. 核心关键在持有写锁的同时提前获取读锁完成锁降级准备 readLock.lock(); } finally { // 4. 释放写锁此时当前线程依然持有读锁没有任何其他写操作能够侵入 writeLock.unlock(); } try { // 5. 在读锁保护下安全地读取最新落盘数据并刷新各级缓存 Product latest productMapper.selectById(skuId); refreshLocalCache(latest); } finally { // 6. 最终释放读锁 readLock.unlock(); } } }为什么说“锁升级Lock Upgrading”必死无疑很多同学自然会反向思考“既然能从写锁降级为读锁那能不能反过来——先加读锁看一眼如果需要修改再就地升级为写锁”绝对不行在任何多线程或分布式环境下锁升级是导致死锁的头号杀手假设有两个并发线程 A 和 B同时持有了读锁这是完全合法的并发操作线程 A 发现数据需要更新试图在持有读锁的前提下申请写锁写锁要求“全场没有任何读锁”因此线程 A 在等待线程 B 释放读锁恰好线程 B 也发现数据需要更新同样在持有读锁的前提下申请写锁它也在死等线程 A 释放读锁两个线程都在等待对方释放读锁同时自己绝不撒手当场形成不可解的死锁闭环因此不管是 Java 本地的ReentrantReadWriteLock还是 Redisson都严禁锁升级。一旦发现需要写操作必须先老老实实释放读锁再重新去抢写锁。掌握读写锁的边界与降级之道是在读多写少的亿级流量战场上将吞吐量压榨到物理极限的最强利刃。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询