数据库的"锁",比你想象的更残忍
Site Owner
发布于 2026-06-16
数据库并发控制核心:封锁协议如何解决丢失修改、脏读、不可重复读三大问题,以及两段锁协议的可串行化保证与死锁代价。
数据库的"锁",比你想象的更残忍
你抢到了!——然后系统崩溃了。
2021年,某电商平台双十一当天,一款爆品瞬间被抢购10万件。然而5分钟后,运营后台显示:库存扣了10万件,实际只发出去了3万件。剩下的7万单,要么超卖,要么库存变成负数。
技术团队连夜排查,发现罪魁祸首不是服务器、不是带宽、不是代码bug——而是两个事务同时读取了同一行数据,都以为自己"抢到了",然后各自按自己的逻辑继续执行。
这个场景,几乎每个高并发系统都踩过。而它的解法,写在数据库教科书里一个被大多数人忽略的章节:封锁协议。
一、锁的本质:给数据加一把"安全带"
数据库并发控制的核心问题只有一个:多个事务同时读写同一份数据,谁先谁后,怎么保证不出错?
封锁协议的思路很简单:在对数据进行操作之前,先"加锁",告诉其他事务"这数据我现在在用,你们等一下"。
加的是什么锁?有两种基本类型:
X锁(排他锁):加了X锁,其他事务既不能读也不能写。相当于给数据贴上"专属"标签,只有你一个人能用。
S锁(共享锁):加了S锁,其他事务可以读,但不能写。相当于大家可以围观看,但谁都不能动手改。
这两种锁的组合,就构成了四级封锁协议的底层逻辑。
二、四级封锁协议:数据库的"安全等级"
这四级封锁协议,不是技术专家拍脑袋想出来的,而是针对三类并发问题,逐级打补丁的过程。
第一级:防止"丢失修改"
丢失修改是什么?举个例子:
两个用户同时买最后一件商品。事务A读取库存=1,事务B也读取库存=1。A算完:库存-1=0,写回去。B也算完:库存-1=0,写回去。结果:库存=0,但实际两个人都买到了。
第一级封锁协议的要求:修改数据前,必须加X锁,且事务结束后才释放。 这样,事务A在修改时加了X锁,事务B必须等A完成才能读。
但第一级有漏洞:脏读。
第二级:防止"脏读"
脏读是什么?事务A修改了库存从1改成0,但还没提交。事务B此时来读,读到了0——但如果A最后回滚了呢?B读到的就是"不存在的数据"。
第二级封锁协议加了一条:读数据前,加S锁,读完立刻释放。 这样,事务B在A提交前读的其实是S锁保护下的快照,不会读到未提交的脏数据。
但第二级还有漏洞:不可重复读。
第三级:彻底解决"不可重复读"
不可重复读是什么?事务A在同一个事务里读了两次数据,结果两次读到的值不一样——因为中间有其他事务修改并提交了这条数据。
第三级封锁协议的关键改动:S锁必须持有到事务结束才释放。 这样,在整个事务期间,没人能修改你读过的数据,你每次读到的都是一致的。
| 级别 | X锁要求 | S锁要求 | 防止 |
|---|---|---|---|
| 一级 | 修改前加X锁,事务结束释放 | 无 | 丢失修改 |
| 二级 | 同上 | 读前加S锁,读完释放 | 丢失修改、脏读 |
| 三级 | 同上 | 读前加S锁,事务结束释放 | 丢失修改、脏读、不可重复读 |
所以为什么大多数OLTP数据库默认用三级封锁? 因为到了第三级,事务的隔离性才算完整——你读到的东西,要么是别人没动过的,要么是真实提交过的,不会出现读到一半被改回去的"幻觉"。
三、两段锁协议:给封锁协议套上"可串行化"的枷锁
四级封锁协议解决了"三类并发问题",但还有一个更大的问题:并发事务的执行顺序怎么保证正确性?
两段锁协议(2PL)回答了这个问题。
它的规则只有一条:锁只能先获取、后释放,不能交叉。
具体分成两个阶段:
- 扩张阶段:只加锁,不释放任何锁
- 收缩阶段:只释放锁,不获取任何新锁
这两段,按顺序来,不许乱。
为什么这样设计?因为两段锁协议能保证:所有并发事务的执行结果,等价于某个串行顺序的执行结果。 这叫做"可串行化"——并发执行,和一个人顺序执行,效果一样。
但代价也随之而来:死锁。
四、死锁:锁的"副作用",比你想的更常见
死锁是什么?两个事务互相等待对方持有的锁,谁都推进不下去。
事务A锁住了记录1,等着记录2;事务B锁住了记录2,等着记录1。两边都不放手,系统就卡死了。
两段锁协议允许死锁发生,这是它的理论代价。实际工程中,数据库会配套两个机制:
1. 死锁检测:数据库内部有死锁检测器,定期扫描"等待图",发现环就选择一个"受害者"事务回滚。
2. 超时回滚:设一个阈值,事务等锁超过X秒就自动回滚。简单粗暴,但有效。
Oracle默认超时是约3秒。MySQL InnoDB默认超时是50秒。业务高峰期,50秒的等待意味着什么?意味着用户看到"转圈",然后关掉页面。
五、实际工程:封锁协议不是孤立存在的
教科书里封锁协议是干净的逻辑,但到了真实系统里,工程师要做的选择题远比考试复杂。
选择哪种隔离级别?
隔离级别越高,数据一致性越好,但并发性能越差。Oracle默认是"读已提交"(Read Committed),而不是"可重复读"——因为对大多数业务来说,偶尔的不可重复读是可以接受的,但系统卡死3分钟是不可接受的。
长事务是封锁协议的天敌。
一个事务持续30分钟,期间持有大量S锁。期间所有想修改这些数据的事务都得排队——严重时,一个慢查询拖垮整个系统。所以工程上有个铁律:大事务拆分,批量操作分批提交。
乐观锁是封锁协议的"替代方案"
有些场景下,封锁协议开销太大,工程师会用版本号机制替代:读数据时顺便读版本号,修改时检查版本号有没有变过——变了就重试,没变就修改。这叫乐观锁,适合"冲突少、读多写少"的场景。典型的如库存扣减,高并发下乐观锁会导致大量重试,性能反而不如悲观锁。
所以呢
封锁协议是数据库并发控制的地基。这套理论诞生于1970年代,比大多数互联网公司的历史还长,但它解决的问题,今天依然每天在生产环境里真实发生。
你今天抢到的那张票、下的那单外卖、查的那笔银行余额——背后都有封锁协议在默默运转。它不显眼,不炫酷,但如果它失效,整个系统的一致性就会像沙堡一样崩塌。
理解封锁协议,不是为了考试——是为了在系统出现"明明代码没问题但数据就是不对"的诡异问题时,能从"并发控制"的角度快速定位根因。
下次遇到死锁导致的系统卡顿时,别急着骂数据库。你面对的,可能只是两个事务在互相等锁——而这,恰恰是封锁协议在执行它的本职工作:保证在混乱的并发世界里,每一笔数据操作都是安全的。