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 同时点击"预订":

两个事务都读到了"还有 1 间空房",于是各自把total_reserved加 1 并提交——结果卖出了 101 间房,库存被超卖。这就是经典的竞态条件(Race Condition),根源在于"检查库存"和"扣减库存"两步之间没有保护机制。
书中将并发问题分为两类(详见 22. Hotel Reservation System/README.md):
- 同一用户手抖双击"预订"按钮→ 用幂等键(Idempotency Key)+ 唯一约束解决;
- 多个用户同时订同一房间→ 用锁机制解决,这正是本文主角。
二、悲观锁:先加锁再操作,排队串行化 🔒
悲观锁(Pessimistic Locking)的思路是"先假设冲突一定会发生",更新记录时直接给它上一把锁,其他事务必须等待。

在 MySQL 中通过SELECT ... FOR UPDATE实现:User 1 的 Transaction 1 执行该语句后,库存行被锁定直到事务提交;此时 User 2 的 Transaction 2 执行同样的语句时阻塞等待。等 Transaction 1 提交后,Transaction 2 再检查库存,发现已满(total_reserved=100),于是回滚——超卖被彻底避免。
| 维度 | 说明 |
|---|---|
| ✅ 优点 | 实现简单;串行化更新,天然避免冲突;适合数据竞争激烈的场景 |
| ❌ 缺点 | 多资源加锁可能死锁;持有锁期间其他事务全部阻塞,可扩展性差 |
书中作者明确指出:由于扩展性问题,不推荐在酒店预订系统里使用悲观锁。
三、乐观锁:允许并发修改,冲突时再回滚 🚀
乐观锁(Optimistic Locking)的思路相反:"假设冲突很少发生",不加锁,让多个事务自由读写,只在提交时校验是否有人动过数据。

最主流的实现方式是版本号(version)(比时间戳更可靠,因为服务器时钟可能不准):
- 给库存表新增一个
version列; - 用户更新前先读取当前版本号;
- 更新时把版本号 +1 一起写回数据库;
- 数据库校验:如果写入时表里的版本已不是读到的那个,说明期间被别人改过——拒绝本次更新,事务回滚。
左图(无冲突):User 1 写 v2,User 2 读到 v2 再写 v3,顺序衔接成功;右图(有冲突):两个用户都基于 v1 去写 v2,第二个写入因版本不符被拦截。
| 维度 | 说明 |
|---|---|
| ✅ 优点 | 不加锁、不阻塞,性能更高;不会编辑到过期数据;适合冲突较少的低竞争场景 |
| ❌ 缺点 | 数据竞争激烈时大量回滚,性能急剧下降 |
书中结论:酒店预订的 QPS 并不高(估算约 3 次/秒,见下方估算图),冲突率低,因此乐观锁是更优选择。

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

给库存表加一条 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),仅供参考