深夜的急诊诊所一片忙碌。墙上挂着一块值班白板,李医生和赵医生的名字下都写着“在岗”,诊所铁律只有一条:任何时刻,急诊至少有一位医生留守。今晚两人都想早点下班。李医生看了一眼白板——赵医生还在岗,于是放心地擦掉了自己的名字。几乎同一瞬间,赵医生也看了一眼白板——李医生还在岗,也擦掉了自己的名字。等两人都走远,白板上一片空白,急诊室空无一人。两人都“看”过白板,都做了“正确”判断,却共同酿成事故——这就是并发事务里最阴险的陷阱之一:写倾斜。而另一个叫幻读的陷阱更诡异:护士长明明数过候诊名单上有 3 位病人,接个电话再回头,名单上竟多出 1 位——一个“凭空出现”的新行。这两起事故,都根植于数据库底层的“隔离级别”和 MVCC 机制。如果你管理过任何一块“白板”——库存表、排班表、余额表、任务表——这篇文章就是为你写的。
理解写倾斜与幻读的成因与对策,是后端开发与 DBA 设计高并发事务、快速定位数据不一致的必备内功,也能让你在面试中聊出真正的深度。
目录
可视化演示
展示写倾斜异常产生原理、幻读范围查询不一致、四种隔离级别异常防范矩阵、SELECT FOR UPDATE等工程防范方案,交互式理解MVCC并发陷阱与隔离级别选择。
本节配有交互动效(进入视口后自动加载,约 3KB)。
写倾斜:快照隔离下的隐蔽异常 两个并发事务读取同一快照后各自写入不同行,各自合法但组合违反业务约束 修改不同行不触发写写冲突检测,快照隔离无法防范 T1: read → write(A); T2: read → write(B); A+B violates constraint 写倾斜是快照隔离的固有缺陷。
并发与隔离:白板的看与写规则
诊所的每一次看白板、写白板,都对应数据库的一次读、一次写。而“隔离级别”就是诊所制定的观看规则:你是能凑到白板前看别人写到一半的草稿(读未提交),还是只能看已经誊写好的最终结果(读已提交),还是进门时给白板拍一张照、之后全程只看这张照片(可重复读),还是干脆规定所有人必须排成单列、一个操作完下一个才能开始(串行化)。
数据库这门“看与写”的学科,围绕一个叫 ACID 的核心承诺展开:原子性、一致性、隔离性、持久性。其中隔离性(Isolation)决定并发时你能看到多少别人的“半成品”。隔离越严格,看到的越真实,但并发能力越差——就像排队看病虽然公平,效率却低。
四类并发异常按严重程度排列:脏读、不可重复读、幻读、写倾斜。隔离级别也分四档阶梯:读未提交(RU)、读已提交(RC)、可重复读(RR)、串行化(SR)。RC 防住脏读,RR 让快照读稳定,SR 则防住一切异常。这四档的关系,决定了后面两个陷阱出现的舞台。
规则越严,白板前越安全,但排队的队伍也越长。值得记住的是:MySQL InnoDB 默认 RR,PostgreSQL 默认 RC——默认档位不同,踩坑的概率也不同。理解这四档,是通向后面两个陷阱的入场券。
- RU 读未提交
\mathrm{RU}是最宽松的一档,医生可以直接看别人写到一半的白板,可能看到“脏”内容。 - RC 读已提交
\mathrm{RC}只能看到已提交的结果,解决脏读,但同一页白板每次看都可能不同。 - RR 可重复读
\mathrm{RR}给事务一张固定的快照照片,同一事务内重复读结果一致。 - SR 串行化
\mathrm{SR}是最高档,并发效果等价于某种串行顺序,所有异常都消失。
写倾斜原理:两个医生同时下班
回到开头那场事故。李医生的操作能拆成三步:看一眼白板(读);判断“赵医生还在岗,我可以走”(计算);擦掉自己的名字(写自己的行)。赵医生也一模一样。问题出在第一步:两人看的都是“进门时”的信息——在可重复读/快照隔离下,他们各拿着一张旧照片,照片上对方都还在岗。
数据库术语里,这叫作写倾斜(Write Skew):两个事务读了相同的行集,却写了不相交的行集,最终联合破坏了跨多行的业务约束。最阴险的是,全程没有任何锁冲突——两人锁的是不同的行,谁也不等谁,所以没有死锁、没有超时、没有报错,双方都愉快地提交了。等到应用层再去查“为什么没人值班”,事故早已发生。
写倾斜的配方恰好三个要素:共同的读集、不相交的写集、全表性的约束。快照隔离保证了“读一致”,却管不住“基于旧数据做出的判断”。它不像脏读那样明显,也不像死锁那样会报错,是并发数据库里最隐蔽的坑之一。
两人都拿着同一天拍的合影,照片里对方还在值班室,于是各自放心地关灯走人——白板最终空无一人。要阻止这场事故,光靠“发照片”的隔离级别不够,必须改变“读判断”与“写”之间的锁联动方式。
- 相同的读集
R_1 = R_2 = \mathcal{D}表示李和赵都读了整张值班表,都看到“对方在岗”。 - 不相交的写集
W_1 \cap W_2 = \varnothing表示李只写李的行,赵只写赵的行,行锁互不干扰。 - 约束被破坏
C(\mathcal{D}) = \text{假}表示提交后“至少一人在岗”这条全表约束不再成立,而数据库毫无察觉。
幻读原理:候诊名单上多出的病人
诊所角落里,护士长正在做重要统计:今晚 3 点以后挂号的病人有多少?她第一次数,候诊名单上有 3 位。正要录入,急救电话响起,新病人被推进来挂号——名字被写进名单。护士长回头再数一遍:4 位。名单上的旧行一行动过,却凭空多出一行。这个“多出来的幻影”,就是幻读(Phantom Read)。
幻读的本质是:同一事务内,同一条谓词查询(WHERE 条件),两次执行的结果集变了。它和不可重复读的区别在于:不可重复读是“同一行的内容变了”,幻读是“满足条件的行数变了——多出来或消失了”。引发幻读的操作几乎总是 INSERT 或 DELETE,而不是 UPDATE。
为什么快照读能防住它?在可重复读的快照机制下,护士长第二次数的确实还是旧名单。但如果她用的是“当前读”(加锁读),比如 SELECT …… FOR UPDATE 或 UPDATE …… WHERE,数据库必须看最新数据,此时并发插入的新行就像幽灵一样混了进来。MySQL InnoDB 用“间隙锁”给谓词区间筑墙;没有间隙锁的数据库,在 RR 下仍可能被幻读偷袭。
名单上每条记录都真实存在,但“两次统计结论不同”这件事本身,会让护士长做出错误的排班决策。幻读最危险的地方在于:它骗过你的眼睛,让你基于错误的总数做判断。
- 谓词定义结果集
Q(t_1)是第一次查询按条件筛出的行集合,共 3 行。 - 并发插入新行
r_{\text{新}}是另一事务在两次数数之间插入的新行,也满足谓词。 - 结果集变大
|Q(t_2)| = 4,行数从 3 变 4,幻影出现。
两者区别:擦名字 vs 加名字
写倾斜和幻读,就像白板上的两类破坏:一类是“把已有的名字擦掉”(更新行值),一类是“悄悄添上新的名字”(插入新行)。一个改变字的颜色,一个改变名单的长度。很多初学者把两者混为一谈,其实它们发生的路径和防护手段完全不同。
从数据库角度看有三点区别。第一,操作类型:写倾斜是两个 UPDATE 作用于不同的行;幻读是一个 INSERT 引入了新行。第二,读取方式:写倾斜依赖快照读的旧数据做判断;幻读出现在对谓词区间重复执行查询时。第三,约束性质:写倾斜破坏的是“跨多行的业务约束”,幻读破坏的是“查询结果集的稳定性”。
正因为不同,防护手段也不同:写倾斜需要把“读判断”变成加锁读写,或锁住一条物化的控制行;幻读需要锁住谓词对应的整个值域区间(间隙锁),或者直接上串行化。用错手段会事倍功半——例如只给写倾斜加行锁完全无效,因为你锁的是自己正在改的行,而对方改的是另一行。
给白板“每一行”上锁(行锁),管得住有人偷偷改你的名字,却管不住护士在名单末尾新登记一位病人。想同时防住两件事,得把“行”和“范围”两个维度都锁住。
- 写倾斜改值不改数
UPD(r_1) \times UPD(r_2)两个更新作用在不同行,行的个数不变,但取值翻转导致约束失效。 - 幻读增行
INS(r_{\text{新}})一次插入直接让谓词结果集增加一行。 - 对比结论一个是取值变化,一个是行数变化,因此“行锁”只对前者有间接帮助、对后者几乎无效,防幻读必须上范围锁。
隔离防护:从窗帘到铁门
诊所可以选择不同防护等级。RC 像半透明的窗帘,能拦住别人写到一半的草稿,但挡不住你看完一遍后内容又变。RR 像进门时发一张照片,看照片稳定,但照片总会过时。SR 则是铁门加登记处:所有人排成单列,一个接一个操作白板,任何并发事故都无从发生。
串行化的定义很漂亮:并发执行的效果,等价于把事务按某种顺序一个个执行。它不要求数据库真的一个一个来,而是保证“结果像排过队”一样。PostgreSQL 的 SERIALIZABLE 采用 SSI(可串行化快照隔离),通过检测读写依赖并主动中止一个事务来拦下写倾斜;MySQL InnoDB 的 RR 则用“间隙锁 + 行锁”组成 next-key 锁,在加锁读时锁住整个谓词区间。
间隙锁的精髓在于:它锁的是一个值区间 (a,b),即使区间里现在一行数据都没有,也能阻止未来插入。这就像护士长在“3 点到 5 点”的时段格里插了一面小旗:新病人的挂号单若落在这时段,必须等小旗撤掉。而写倾斜的防护,要靠显式地把“读+判断”变成加锁的原子步骤,或用版本号做乐观锁重试。
隔离级别不是越高越好,而是越贴合业务风险越好。高风险规则用铁门,普通查询用窗帘——取舍之道,在于理解每一档真正防住了什么、漏掉了什么。
- 可串行化定义
SR ⟺ 并发效果 ≡ 串行效果,只要结果等价于排队执行,怎么实现都行。 - 间隙锁定义
X-LOCK(a,b)锁定的是从 a 到 b 的取值范围,而不是某一行——空白区间也能锁。 - 组合拳行锁加间隙锁组成 next-key 锁,既防同行改写,又防区间插入,这正是 InnoDB 在 RR 下抑制幻读的机制。
MVCC 支撑:白板背后的便利贴层
为什么快照读能这么稳定?因为数据库并没有真的把旧字擦掉。想象白板背后藏着一摞便利贴:每次有人改名,只是把一张新便利贴贴上去,旧的还压在下面。每张便利贴都标着“谁贴的、第几张”。医生进门时,护士长递给他一副特殊眼镜——戴上后,他只看得见“贴得早于他进门”的便利贴,后来的新贴纸统统隐形。这副眼镜,就是 MVCC(多版本并发控制)。
MVCC 的表里,每一行都维护着多个历史版本,串成一条“版本链”。新增的隐藏字段包括事务 ID(trx_id)和回滚指针(roll_pointer)。每次 UPDATE 不是覆盖,而是生成新版本,通过回滚指针指向前一版,形成链表。事务做“快照读”时,按自己的快照 ID 沿链找第一个可见版本;做“当前读”时,绕过快照直接读最新已提交版本并加锁。
可见性判定的核心就一句话:只要这个版本的事务 ID 不晚于你的快照点,且没有删除标记,你就看得见。正是这种“按版本筛选”的机制,让快照读永远稳定——但代价是,一个事务永远感知不到快照之后别人提交的新版本,写倾斜的温床就此形成。MVCC 是利也是弊:它让读写互不阻塞、读性能极高,但“基于旧判断的写”可能悄悄破坏约束。
便利贴层让“看”飞快且稳定,但也意味着你看到的世界永远滞后一步。写倾斜的根,就埋在“滞后”这两个字里——而理解这一点,是选择防护方案的前提。
- 可见性判定
trx_id ≤ snapshot_id表示“这个版本是快照点之前写的”,只有这样的版本才进入你的视野。 - 删除标记过滤
v.deleted = 假排除已删除的行,保证看不到“尸体”。 - 版本链结构
r新 → r旧 → r更旧,一次更新贴一层,链头永远是最新版本。
实战对策:让值班表不再失控
护士长决定改革,拿出三样工具:第一样是“手按白板”——医生做判断时,必须用手按住相关区域,别人只能等他写完再看(悲观锁);第二样是“带编号的便利贴”——每次写都要先核对编号,发现编号被别人改过就撕掉重来(乐观锁);第三样是“值班哨岗”——把“至少一人在岗”这条规则物化成哨岗登记表,谁进谁出都必须先动哨岗那行记录。
三种工具对应数据库世界的经典解法。悲观锁:SELECT …… FOR UPDATE 把“读判断”变成加锁的当前读,命中行被加排他锁直到事务结束。乐观锁:加 version 字段,UPDATE 时带 WHERE version = 读到的值,影响行数为 0 说明冲突,重试即可。物化冲突:把多行约束浓缩成一行“控制行”,锁这行就等价于锁整个规则。
工程实践还要注意:加了锁可能死锁(互相等待形成环),数据库靠等待超时和死锁检测处理,被牺牲的事务抛出异常,应用层要设计重试;锁粒度太粗会拖垮并发,需要监控锁等待时间、死锁次数、冲突率。SQL 层面,EXPLAIN 能确认 UPDATE 是否走索引——锁的范围随索引而定,全表扫描会锁全表,这也是排查“锁风暴”的钥匙。
白板的秩序,终究要靠“看”与“写”的纪律来保证。选择悲观锁还是乐观锁,本质是选择“宁可排队”还是“宁可重试”——没有银弹,只有对业务并发模式的清醒认知。
- 显式加锁的当前读
SELECT ... FOR UPDATE让这次“读判断”带上排他锁,判断结果在提交前对别人既不可见也不可改。 - 死锁形成
T1 → T2 ∧ T2 → T1表示形成等待环,数据库必须打破僵局。 - 牺牲者回滚死锁检测挑一个事务回滚放行另一个,应用层捕获异常后重试。
AI 应用场景
AI 数据标注平台的并发认领
阶段:数据准备 / 标注任务管理
解决的问题:标注平台维护一张“待标注样本池”,多个标注员并发“领取”任务。传统实现里每人先查询剩余样本数,再更新自己的任务行——这正是写倾斜:各改各的“认领行”,合起来却让样本池出现重复绑定或负数余量。
具体案例:某 CV 标注团队 20 个标注员同时领取新一批图片,任务表突然出现同一条 task_id 被两个人认领,剩余量计数变成 -3。排查后发现是两个事务基于同一份快照做判断,各写各的认领记录后都成功提交。
特征平台的训练与线上一致性
阶段:特征工程 / 离线-在线一致性保障
解决的问题:离线特征表做“按用户维度的聚合统计”时,实时链路并发插入新事件行,导致同一条 SQL 跑两次结果不同——幻读让训练样本的特征快照和线上推理时对不上,模型效果评估失真。
具体案例:推荐系统每天凌晨跑特征拼接任务,统计用户点击量时用 SELECT COUNT(*) 校验数据,恰好与实时写入的日志事务交错,两次统计差了 1 万行,最终训练样本和线上特征错位,A/B 实验指标出现“神秘波动”。
模型灰度发布与回滚
阶段:模型部署 / MLOps
解决的问题:两个发布器同时检查“集群中是否还有旧版本实例”,都决定下线自己负责的旧实例——写倾斜导致新旧比例瞬间失衡,流量全部打到尚未就绪的新模型。
具体案例:双机房并行灰度时,两个发布进程同时读到“旧实例还有 2 台”,各自下掉 1 台后,集群里 0 台旧实例、新实例健康检查还没通过,线上服务直接降级。修复方式是把“新旧实例比例”物化成一行控制记录,切换前先锁这行。
Python 演示
import threadingimport time # 值班白板: 模拟数据库中的两行数据roster = {'李': True, '赵': True} def transaction(doctor, use_lock=False, lock=None): """模拟一个事务: 快照读 -> 业务判断 -> 写自己的行。""" if use_lock: lock.acquire() # 悲观锁: 把判断和写变成临界区 try: snapshot = dict(roster) # 事务开始时的快照读 time.sleep(0.02) # 放大并发窗口, 让两个事务互相交错 other_on = [d for d, v in snapshot.items() if d != doctor and v] if other_on: # 快照里还有别人在岗 roster[doctor] = False # 只写自己的行 return doctor + ' 看到' + other_on[0] + '在岗, 已下班' return doctor + ' 发现无人留守, 留下值班' finally: if use_lock: lock.release() def run_demo(use_lock=False): global roster roster = {'李': True, '赵': True} lock = threading.Lock() if use_lock else None t1 = threading.Thread(target=transaction, args=('李', use_lock, lock)) t2 = threading.Thread(target=transaction, args=('赵', use_lock, lock)) t1.start() t2.start() t1.join() t2.join() on_call = [d for d, v in roster.items() if v] if on_call: return '最终在岗: ' + '、'.join(on_call) return '最终在岗: 空无一人!' if __name__ == '__main__': print('[不加锁: 快照隔离下的写倾斜]') print(run_demo(False)) print() print('[加锁: 判断与写入被串行化]') print(run_demo(True))
snapshot = dict(roster)事务开始瞬间把整张表复制成“快照”,之后的判断全部基于这份旧数据,这正是写倾斜的温床。time.sleep(0.02)人为放大两个事务的交错窗口,让现实中一闪而过的竞态稳定复现。roster[doctor] = False每个事务只写“自己的行”,互不冲突,因此两个写都能成功提交。lock.acquire()悲观锁把“读 + 判断 + 写”变成临界区,李的判断结果立刻对赵可见。run_demo(use_lock=False)对照组:无锁复现写倾斜,加锁后约束恢复,输出对比一目了然。
常见误区
误区“写倾斜和丢失更新不都是并发写搞出来的吗?反正都是后写覆盖先写呗。”很多人一看到“两个事务并发修改”就归类为丢失更新。
正解丢失更新指两个事务改同一行,后提交的覆盖先提交的,本质是“同一行的值被吞掉”;写倾斜是两个事务改不同的行,各自都成功提交,没有任何值被覆盖,但联合破坏了跨行约束。用白板比喻:丢失更新是两人抢同一支笔改同一行名字,后到者覆盖先到者;写倾斜是两人各擦各的名字,结果一栏全空。
为何常见:两者都表现为“并发下的数据不对”,且常出现在快照隔离的讨论里,初学者习惯按“结果异常”归类,而不是按“行集关系”归类。
误区“我都设成可重复读了,快照读那么稳,幻读哪还有机会?”不少开发把 RR 当成万能安全档。
正解快照读确实不会看到新插入的行,但“当前读”(加锁读)必须读最新数据,若没有间隙锁保护谓词区间,幻读依然可能出现;而且可重复读防不住写倾斜——照片不会变,白板本身在变。InnoDB 在 RR 下用 next-key 锁把加锁读也保护住,但这依赖实现细节,并非隔离级别的必然承诺(PostgreSQL 的 RR 就没有间隙锁)。
为何常见:很多人把“事务内重复读结果一致”等同于“任何读都一致”,忽略了加锁读与快照读的本质差异。
误区“写之前 SELECT …… FOR UPDATE 把要改的行锁住,不就安全了吗?”这是最常见的“想当然”。
正解行锁只锁你实际命中的行;写倾斜中两人锁的是各自不同的行,互不等待、都成功提交,约束照样被破坏。正确做法是锁住判断所依赖的整个范围(如锁谓词区间),或把“至少一人在岗”物化成一行控制记录,锁这一行即可。锁的粒度必须覆盖约束作用的范围,否则防线必然漏风。
为何常见:直觉上“锁住数据就安全”,但行锁的粒度小于约束作用的范围,粒度错配导致看似上了锁、实则形同虚设。
误区“既然串行化防一切,那所有事务都设成串行化不就好了?”课堂示例只看正确性,没见过生产环境的锁风暴。
正解串行化通过全局锁、谓词锁或 SSI 依赖检测来保证等价串行,高竞争场景下吞吐和响应时间会明显恶化,甚至引发大面积锁等待。正确姿势是:能优化并发模式就先优化(物化、乐观锁、显式范围锁),串行化作为特定风险的兜底而不是默认。
为何常见:安全感和性能的权衡常被忽略,因为初学者只在示例里看正确性,没在压测环境里看吞吐曲线。
面试题
Q1. 写倾斜的核心特征是下列哪一项?
- A.两个事务改写同一行,后写覆盖先写
- B.读同一快照各写不同行,合破约束← 正确答案
- C.事务读到了另一个事务的脏数据
- D.事务删除了另一事务刚插入的行
答案:B。写倾斜的配方是“相同读集 + 不相交写集 + 跨行约束被破坏”。A 是丢失更新,C 是脏读,D 与写倾斜无关。就像两个医生各擦各的名字,白板空了。
Q2. 幻读最典型的触发操作是?
- A.两个事务并发修改同一行
- B.并发插入满足查询谓词的新行← 正确答案
- C.并发删除已经读到的数据行
- D.并发执行同一条聚合统计
答案:B。幻读的“幻影”来自新插入(或删除)后满足谓词的行,使同一查询两次结果集不同。B 最典型;A 导致的是不可重复读或丢失更新。
Q3. 在快照隔离(如 PostgreSQL 的 REPEATABLE READ)下,下列哪项仍然可能发生?
- A.读取尚未提交的脏数据
- B.两个事务对同一行的丢失更新
- C.读同快照写不同行的写倾斜← 正确答案
- D.快照读重复执行出现新行
答案:C。快照隔离消除了脏读和快照读下的不可重复读,但著名的漏洞正是写倾斜:两人基于同一份旧快照做判断,各写各的行,约束被破坏。D 在快照读下不会发生。
Q4. 下列哪些是写倾斜发生的必要条件?
- A.两事务都读到同一份旧快照← 正确答案
- B.两事务修改的行互不相交← 正确答案
- C.两事务都对同一行加行锁
- D.存在跨多行的业务约束被破坏← 正确答案
答案:ABD。写倾斜需要“同快照、异行写、共约束”三要素。C 是写冲突/死锁的场景,与写倾斜的“互不等待”相反。
Q5. MySQL InnoDB 在可重复读下抑制幻读的主要机制是?
- A.对整张表加全局排他锁
- B.行锁配间隙锁的当前读机制← 正确答案
- C.完全依赖无锁的快照读
- D.临时禁止并发插入操作
答案:B。InnoDB 对当前读使用 next-key 锁(行锁+间隙锁),把谓词区间也锁住,新行无法插入。这就像护士长在时段格里插小旗。
Q6. 关于写倾斜与死锁的关系,下列正确的是?
- A.写倾斜出现时数据库必然报死锁
- B.死锁靠互相等待识别而写倾斜不等待← 正确答案
- C.写倾斜必须回滚其中一个才能解决
- D.写倾斜可全部提交成功却破坏约束← 正确答案
答案:BD。死锁是“我等你、你等我”的等待环;写倾斜中两人各写各的行,全程无等待,所以没有死锁、没有回滚、没有报错,两个事务都成功提交——这正是它最隐蔽的原因。
Q7. 快照隔离下解决写倾斜的“物化冲突”做法是?
- A.将约束物化为一行并锁定这行← 正确答案
- B.给每行添加更新前的触发器
- C.把隔离级别调低到读已提交
- D.只对各自修改的行加行锁
答案:A。物化冲突是把“至少一人在岗”这类跨行约束浓缩成一行控制记录,锁这行就等价于串行化所有决策。只锁各自修改的行(D)正是写倾斜防不住的原因。
Q8. 关于 MVCC 中“快照读”与“当前读”,正确的是?
- A.快照读和当前读看到的数据一致
- B.当前读基于事务起点版本不感知更新
- C.当前读加锁取最新快照读看起点版本← 正确答案
- D.当前读只能配合插入语句一起使用
答案:C。快照读沿版本链找“快照点之前”的版本,稳定但滞后;当前读直接读最新已提交版本并加锁。前者是写倾斜的温床,后者是加锁防护的基础。
Q9. 排查线上“数据不一致”问题时,哪些手段值得采用?
- A.复核事务隔离级别与多次读的语义← 正确答案
- B.用执行计划确认行锁实际锁定的范围← 正确答案
- C.压测还原并发窗口并分析死锁日志← 正确答案
- D.把隔离级别降到读未提交以提速
答案:ABC。排查并发问题要同时看“隔离级别语义、锁范围、并发窗口”。降到读未提交只会引入脏读,不可能解决问题。
Q10. 关于“可串行化”的保证,下列表述最准确的是?
- A.可串行化等价于所有事务都加全表锁
- B.并发结果等价于某串行顺序逐个执行← 正确答案
- C.可串行化之下不可能发生锁等待
- D.可串行化不适用于 MVCC 数据库
答案:B。可串行化承诺的是“结果等价于某种串行顺序”,而非实现方式必须是锁;SSI、谓词锁都能实现它。实现过程中仍可能有锁等待与牺牲回滚。