system-design-notes:乐观锁 vs 悲观锁,酒店预订并发冲突的2大武器详解
2026/9/17 19:52:19 网站建设 项目流程

system-design-notes:乐观锁 vs 悲观锁,酒店预订并发冲突的2大武器详解

【免费下载链接】system-design-notesNotes of the book System Desgin Interview - An Insider's Guide项目地址: https://gitcode.com/GitHub_Trending/sy/system-design-notes

在系统设计的经典开源笔记 system-design-notes 中,酒店预订是并发冲突的代表性场景:旺季多个用户同时抢订同一批房间,如何避免"超卖"?本文基于该项目的第 22 章,图解**乐观锁(Optimistic Locking)与悲观锁(Pessimistic Locking)**两大并发控制方案的原理、优缺点与适用场景,帮助你在 10 分钟内掌握防止酒店预订"双订问题"的核心武器。

一、酒店预订为什么会有并发冲突?🏨

想象一个场景:某酒店还剩最后 1 间房total_inventory=100, total_reserved=99),此时 User 1 和 User 2 同时点击"预订":

![酒店预订多用户并发预订的竞态条件时序图](https://raw.gitcode.com/GitHub_Trending/sy/system-design-notes/raw/9d8388721e7231442763ad37398b8d82224aa68f/22. Hotel Reservation System/images/double-booking-multiple-users.png?utm_source=gitcode_repo_files)

两个事务都读到了"还有 1 间空房",于是各自把total_reserved加 1 并提交——结果卖出了 101 间房,库存被超卖。这就是经典的竞态条件(Race Condition),根源在于"检查库存"和"扣减库存"两步之间没有保护机制。

书中将并发问题分为两类(详见 22. Hotel Reservation System/README.md):

  • 同一用户手抖双击"预订"按钮→ 用幂等键(Idempotency Key)+ 唯一约束解决;
  • 多个用户同时订同一房间→ 用锁机制解决,这正是本文主角。

二、悲观锁:先加锁再操作,排队串行化 🔒

悲观锁(Pessimistic Locking)的思路是"先假设冲突一定会发生",更新记录时直接给它上一把锁,其他事务必须等待。

![悲观锁SELECT FOR UPDATE串行化酒店预订事务的时序图](https://raw.gitcode.com/GitHub_Trending/sy/system-design-notes/raw/9d8388721e7231442763ad37398b8d82224aa68f/22. Hotel Reservation System/images/pessimistic-locking.png?utm_source=gitcode_repo_files)

在 MySQL 中通过SELECT ... FOR UPDATE实现:User 1 的 Transaction 1 执行该语句后,库存行被锁定直到事务提交;此时 User 2 的 Transaction 2 执行同样的语句时阻塞等待。等 Transaction 1 提交后,Transaction 2 再检查库存,发现已满(total_reserved=100),于是回滚——超卖被彻底避免。

维度说明
✅ 优点实现简单;串行化更新,天然避免冲突;适合数据竞争激烈的场景
❌ 缺点多资源加锁可能死锁;持有锁期间其他事务全部阻塞,可扩展性差

书中作者明确指出:由于扩展性问题,不推荐在酒店预订系统里使用悲观锁。

三、乐观锁:允许并发修改,冲突时再回滚 🚀

乐观锁(Optimistic Locking)的思路相反:"假设冲突很少发生",不加锁,让多个事务自由读写,只在提交时校验是否有人动过数据。

![乐观锁版本号v1到v2到v3冲突检测流程图](https://raw.gitcode.com/GitHub_Trending/sy/system-design-notes/raw/9d8388721e7231442763ad37398b8d82224aa68f/22. Hotel Reservation System/images/optimistic-locking.png?utm_source=gitcode_repo_files)

最主流的实现方式是版本号(version)(比时间戳更可靠,因为服务器时钟可能不准):

  1. 给库存表新增一个version列;
  2. 用户更新前先读取当前版本号;
  3. 更新时把版本号 +1 一起写回数据库;
  4. 数据库校验:如果写入时表里的版本已不是读到的那个,说明期间被别人改过——拒绝本次更新,事务回滚。

左图(无冲突):User 1 写 v2,User 2 读到 v2 再写 v3,顺序衔接成功;右图(有冲突):两个用户都基于 v1 去写 v2,第二个写入因版本不符被拦截。

维度说明
✅ 优点不加锁、不阻塞,性能更高;不会编辑到过期数据;适合冲突较少的低竞争场景
❌ 缺点数据竞争激烈时大量回滚,性能急剧下降

书中结论:酒店预订的 QPS 并不高(估算约 3 次/秒,见下方估算图),冲突率低,因此乐观锁是更优选择

![酒店预订系统QPS估算漏斗图](https://raw.gitcode.com/GitHub_Trending/sy/system-design-notes/raw/9d8388721e7231442763ad37398b8d82224aa68f/22. Hotel Reservation System/images/qps-estimation.png?utm_source=gitcode_repo_files)

四、Plan B:用数据库约束兜底 🛡️

除了两大锁方案,书中还给出了第三种思路——数据库约束,实现上和乐观锁很像,但"护栏"交给数据库:

![数据库CHECK约束防止酒店预订超卖的时序图](https://raw.gitcode.com/GitHub_Trending/sy/system-design-notes/raw/9d8388721e7231442763ad37398b8d82224aa68f/22. Hotel Reservation System/images/database-constraint.png?utm_source=gitcode_repo_files)

给库存表加一条 CHECK 约束:

CONSTRAINT check_room_count CHECK (total_inventory - total_reserved >= 0)

如上图:User 1 先提交成功;User 2 晚一步执行扣减时,total_reserved将超过total_inventory约束被违反,数据库直接拒绝写入

  • ✅ 优点:实现极其简单;低竞争场景表现好;
  • ❌ 缺点:高竞争下同样性能差;约束不像应用代码容易做版本管理;并非所有数据库都支持 CHECK 约束。

五、怎么选?一张表看懂三大方案 ⚖️

方案机制适合场景主要风险
悲观锁SELECT ... FOR UPDATE行锁阻塞高竞争、必须强串行死锁、吞吐量瓶颈
乐观锁版本号校验,冲突回滚低竞争、读多写少高冲突时回滚风暴
数据库约束CHECK 约束兜底任何场景的最后一道防线方言支持不一

实战建议:酒店预订这类"低写入 QPS + 偶发热点"的系统,首选乐观锁,并可叠加数据库约束做兜底;悲观锁仅在竞争极激烈且必须严格串行化时考虑。

六、延伸阅读:项目中的相关资料 📚

  • 完整章节(含 API 设计、数据模型、分库分表与缓存方案):22. Hotel Reservation System/README.md
  • 改进后的库存数据表结构(room_type_inventory):updated-schema.png
  • 微服务架构总览:high-level-design.png
  • 数据库分片扩展方案:database-sharding.png

掌握悲观锁与乐观锁的原理和取舍,是系统设计面试中"并发控制"环节的高频考点。建议结合上文的时序图反复推演"两个用户抢最后一间房"的全过程,把每一种方案如何拦截冲突讲清楚,你就已经超过了大多数候选人。

【免费下载链接】system-design-notesNotes of the book System Desgin Interview - An Insider's Guide项目地址: https://gitcode.com/GitHub_Trending/sy/system-design-notes

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询