用户在“星河剧场”点击一次“确认购票”,页面上看起来只有一个动作,数据库里却要完成一串彼此依赖的修改:确认座位仍可售、冻结座位、扣减用户余额、增加商户待结算款、创建订单,再把出票任务交给消息系统。只完成其中一半,结果就会变成“扣了钱却没有票”或“出了票却没收钱”。
事务把这串操作声明为一个逻辑工作单元。它不是让每条语句瞬间完成,而是让外部最终只能观察到两种结果:整组操作成功,或者整组操作没有留下效果。系统还必须处理另一类风险——许多用户同时抢票时,各事务的读写会交错;每个事务单独看都正确,交错后却可能超卖。

图:一次购票横跨座位、账户、订单与出票任务;事务把它们收束为同一个可提交或可撤销的边界。
事务是一次程序执行中访问并可能修改若干数据项的单位。边界内可以只有一条 SQL,也可以包含多条 SQL 和应用计算。数据库关心的是:这组读写何时开始、何时提交、失败时该撤销哪些效果,以及并发时允许别人看到什么。
以购买座位 A-08 为例,业务不变量可以写成:
还可以写成订单与资金的对应关系:每个状态为“已支付”的订单都必须有一笔等额扣款和一笔等额商户入账。数据库的一致状态,就是这些完整性约束与业务不变量都成立的状态。
原子性、隔离性和持久性主要由数据库系统的恢复与并发控制机制承担。一致性则需要数据库约束和事务程序共同保证:如果程序把票价算错了,数据库无法仅凭“事务已提交”推断出正确票价。更准确的说法是:从一致状态开始,一个逻辑正确的事务在隔离环境中运行,结束后仍应得到一致状态。
ACID 不是“所有中间状态从未存在”。扣减余额已经写入、商户入账尚未写入的瞬间,内部可能暂时不满足资金守恒。原子性与隔离性要求这种中间状态不能在失败后残留,也不能被不该看到它的并发事务观察到。
为了分析并发,我们把复杂 SQL 暂时压缩成两种数据库操作:read(X) 把数据库中的数据项 X 复制到事务自己的内存变量;write(X) 把该内存变量的当前值交回数据库。两次操作之间的算术只影响事务的私有副本。
假设 A 是可售座位数,B 是已预订座位数,售出 1 张票的事务可以写成:
T_buy:
read(A)
A := A - 1
write(A)
read(B)
B := B + 1
write(B)这段程序保持 不变,但执行到 write(A)、尚未执行 write(B) 时,数据库里的两项之和暂时少 1。也要注意,模型里的 write(A) 代表逻辑写入数据库缓冲体系,不必等同于物理磁盘立即落盘;真正的落盘时机由缓冲管理和恢复协议决定。

图:read 把数据库值带入事务私有副本,计算在本地完成,write 才形成数据库层面的修改。
“最后一条 SQL 执行完成”不等于“已经提交”。在部分提交状态,结果可能仍只在易失性内存里;系统必须先保存足够的恢复信息,才能宣布提交。已中止和已提交都是终止状态。一次因死锁而中止的事务可以作为一个新事务重新开始;若失败来自余额不足或参数错误,机械重试没有意义。
外部动作还要额外小心。短信、邮件、页面提示、打印票据和闸机放行都不能像数据库行那样回滚。常见做法是先在事务内写入“待发送事件”,提交后再由独立工作进程投递;即使投递进程崩溃,重启也能从持久化事件继续。已经提交的业务若要撤回,应执行退款、释放座位等补偿事务,而不是把原事务“改成未发生”。
内存和 CPU 缓存速度快,但进程崩溃、操作系统故障或断电可能使其中的信息消失,它们属于易失性存储。SSD、磁盘、光盘和磁带在系统崩溃后通常仍保留数据,属于非易失性存储;但设备损坏、控制器故障或介质老化仍可能造成数据丢失。
稳定存储是一个理想目标:信息永不丢失。工程上无法证明“永不”,只能让丢失概率足够低。实现手段包括把同一份关键信息复制到具有独立故障模式的多个非易失介质,并谨慎安排副本更新顺序。对普通业务,单盘可能被当作足够可靠;对票务清算或支付账务,则可能要求多副本、异地容灾和离线归档。

图:访问速度与故障存活能力并不相同;事务提交依赖的不是“写过内存”,而是恢复所需信息已进入足够可靠的存储层。
修改数据项时,恢复系统通常先记录事务标识、数据项标识、旧值和新值,再允许对应数据页以受控顺序写出。旧值能支持撤销未提交事务,新值能支持重做已提交事务。核心关系可以概括为:
日志先安全保存 → 数据页才可以写出该项变更
提交所需日志安全保存 → 才可以向调用方确认成功若 T_buy 在扣款后中止,系统根据旧值把用户余额恢复;若提交记录已保存但新版订单页还留在内存,重启后根据新值重做。原子性和持久性并不是两套完全独立的设施,它们都依赖可验证、可重放的修改历史。
已提交事务不能通过普通回滚抹去。退款是新的补偿事务,它也要独立满足 ACID。补偿能够抵消业务效果,却不删除原来的支付事实和审计轨迹。
串行执行容易推理,却会浪费资源。一个购票事务等待存储 I/O 时,CPU 可以处理座位查询;一个长报表在扫描历史订单时,不应让毫秒级的验票查询全部堵在后面。并发带来两项直接收益:增加单位时间完成的事务数,提高 CPU 与存储利用率;减少短事务被长事务阻塞造成的平均等待时间。
代价是读写会交错。单独运行都正确的事务,可能观察到另一个事务的中间结果,或者基于已经过时的值覆盖更新。

图:异常的关键不是“两个请求同时到达”,而是它们对相同数据项或相同谓词范围的读写顺序产生了依赖。
脏写是更基础的危险:T2 覆盖 T1 尚未提交的写入。若 T1 随后回滚,系统很难只撤销 T1 而不破坏 T2 的结果,因此常见 SQL 隔离级别都会禁止脏写。
隔离的理想目标不是让事务真的停止交错,而是让每个事务看到的效果等价于某个串行顺序。至于哪些较弱保证可以接受,要看业务是否能在应用层检测冲突并重试。例如选座页面允许显示稍旧的可售快照,但最终确认座位的数据库事务必须再次检查唯一约束。
调度是多个事务操作在时间轴上的总顺序。一个合法调度必须包含参与事务的全部操作,并保持每个事务内部原有顺序;它只决定不同事务之间如何穿插。
设 T1 售出固定 1 张票,T2 把当前可售票的 10% 划入渠道预留。初始 、。若先执行 T1 再执行 T2,结果是 、;若顺序反过来,结果是 、。两者都保持:
结果不同不代表错误,因为两个不同串行顺序本来就可能产生不同的正确结果。对于 个事务,共有 个串行顺序。

图:并发调度允许两条泳道交错,但每条泳道内部的 read、计算、write 顺序不能被打乱。
某个并发调度如果与至少一个串行调度具有相同的可观察效果,就称它可串行化。这样既保留并发带来的资源利用率,又把正确性归约为已经容易理解的串行执行。
反例是丢失更新调度:T1 读到 后尚未写回,T2 也读到 100 并写回 90;随后 T1 写回 99。最终 ,T2 的 10% 划拨消失,这个结果无法由 T1→T2 或 T2→T1 得到。
我们只比较不同事务的操作。若两项操作访问不同数据项,交换顺序不会影响彼此;若访问同一数据项且两项都是读,顺序也无关紧要。其余三类都冲突:读后写、写后读、写后写。
若一个调度能通过反复交换相邻的非冲突操作变成串行调度,它就是冲突可串行化的。手工交换对短调度直观,长调度则适合使用优先图。

图:先发生的冲突操作给出事务间的先后边;无环图的拓扑序就是一个等价串行顺序。
优先图的顶点是事务。若 Ti 的某个操作在 Tj 的冲突操作之前,就添加边 。具体有三种来源:
write_i(Q) 在 read_j(Q) 之前;read_i(Q) 在 write_j(Q) 之前;write_i(Q) 在 write_j(Q) 之前。图中有环,说明存在互相矛盾的先后要求,调度不是冲突可串行化;图无环,调度就是冲突可串行化,任一拓扑序都给出等价串行顺序。图可能有多个拓扑序,这说明不冲突的事务之间没有唯一先后要求。
有些调度不能靠交换非冲突操作变成串行调度,却仍可能得到相同读来源与最终写入。视图等价关注三件事:每个读取初始值的操作仍读取初始值;每个读取其他事务写入值的操作仍由同一事务供值;每个数据项的最终写入仍由同一事务完成。

图:视图等价比较“谁读到谁、谁留下最终值”,比逐对保留所有冲突顺序更宽松。
若一个调度与某个串行调度视图等价,它就是视图可串行化。每个冲突可串行化调度一定视图可串行化,反向不一定成立。典型差别来自“盲写”:事务没有先读就覆盖某项数据,某些写写顺序虽然造成冲突图成环,却没有改变任何事务实际读到的值,最终写入者也相同。
冲突判定只需检查读写操作并对图做环检测;视图可串行化的通用判定代价很高,也难以在事务运行时高效维护。更宽松的“最终结果碰巧相同”若依赖理解应用计算,例如加法交换律,还要求系统分析任意 SQL 或程序逻辑,现实中更困难。
因此,视图可串行化帮助我们理解“冲突可串行化不是并发正确性的唯一数学定义”,而常见并发控制协议仍主要以可高效保证的冲突顺序为基础。不要看到冲突图有环就断言最终值必然错误;准确结论是:该调度不满足冲突可串行化,至于是否满足更宽松定义还需另行分析。
可串行化讨论正常完成时的并发结果,故障又增加了一条要求:若 Tj 读取了 Ti 写入的值,那么 Tj 依赖 Ti。如果 Tj 先提交,随后 Ti 中止,系统已经无法再中止 Tj,恢复就陷入矛盾。
可恢复调度要求:只要 Tj 读过 Ti 的写入,就必须等 Ti 提交后,Tj 才能提交。用事件顺序表示为:
这仍允许 Tj 提前读未提交值。若 Ti 最终失败,所有直接或间接依赖它的事务都要回滚,形成级联回滚。
无级联调度把等待点提前到读取:Tj 只能在 Ti 提交后读取 Ti 写过的值。
严格调度再进一步:只要 Ti 写过 Q 且尚未结束,其他事务既不能读也不能写 Q。这同时避免脏读和脏写,让撤销未提交事务时不必处理别人建立在其值上的读写。

图:严格调度满足无级联,无级联调度必然可恢复;反向关系不成立。
三者关系是:
严格并不自动等于可串行化。恢复性质约束提交、读取与覆盖未提交写入的顺序;可串行化约束整个并发执行是否等价于某个串行顺序。一个生产调度通常要同时满足并发正确性和故障恢复要求。
所有这些级别都应禁止脏写。级别名称描述的是最低保证,但具体数据库可能通过锁、多版本或额外冲突检测实现,边界细节并不完全相同。尤其“可重复读”对幻读和快照范围的处理,在不同产品中可能比最低标准更强;设计事务时要查明目标系统的真实语义,而不是只记一张通用表。

图:级别越强,允许的调度越少;实际产品可能提供强于表中最低要求的保证。
选择较弱级别不是无条件“用一致性换性能”。应先说明哪些不一致可接受、怎样检测竞争、失败后怎样重试。选座页面可以使用快照展示;真正占座时依赖唯一约束或条件更新:
UPDATE seats
SET status = 'held', holder_id = :user_id
WHERE show_id = :show_id
AND seat_no = :seat_no
AND status = 'available';应用检查受影响行数是否为 1。若为 0,说明座位已被他人抢先占用,应该刷新选择,而不是继续创建订单。这个局部原子更新比“先查可售,再无条件更新”更能抵抗竞态。
最简单的方案是事务开始前锁住整个数据库,提交后释放。它只产生串行调度,容易保证正确,却几乎没有并发价值。细粒度锁只保护实际访问的数据项:共享锁用于读,多事务可以同时持有;排他锁用于写,与其他共享锁或排他锁互斥。
两阶段锁把事务分成增长阶段和收缩阶段:增长阶段只获取锁,不释放;收缩阶段只释放锁,不再获取。实践中常把写锁持有到提交或中止,以获得严格调度。锁粒度越细,并发潜力越大,管理开销与死锁风险也越高。
时间戳方法在事务开始时分配一个顺序标识,并为数据项维护最近读时间戳和写时间戳。若某次读写会违反既定时间顺序,系统不让它“倒着发生”,而是中止相关事务并以新时间戳重试。它用拒绝和重试代替部分锁等待,但高冲突事务可能频繁中止。
多版本并发控制保留数据的多个版本。事务读取适合自身快照的已提交版本,而不必等待后来事务的未提交写入。快照隔离下,事务从开始时的快照读取;自己的更新先留在私有视图里,提交时检查是否有并发事务修改了它也要写的数据,冲突则中止。
快照隔离特别适合读多写少的负载:只读事务通常不用等待也不会因写写冲突中止。但它不天然等于可串行化。两个事务可能各自读取对方将要修改的数据,却更新不同的行,因此都通过“同一行写冲突”检查,形成写偏差。例如两个售票员都看到“至少两名值班人员”,分别把自己设为离岗;两者更新不同记录,提交后却无人值班。
许多数据库连接默认自动提交:每条语句成功后形成自己的事务。要让多条语句成为一个工作单元,需要显式开始事务,再以 COMMIT 或 ROLLBACK 结束:
START TRANSACTION;
UPDATE seats
SET status = 'sold', buyer_id = :buyer_id
WHERE show_id = :show_id
AND seat_no = :seat_no
AND status = 'held'
AND holder_id = :buyer_id;
INSERT INTO orders(order_id, show_id, seat_no, buyer_id, amount, status)
VALUES (:order_id, :show_id, :seat_no, :buyer_id, :amount, 'paid');
INSERT INTO outbox_events(event_id, event_type, aggregate_id, payload)
不同产品可能使用 BEGIN、BEGIN TRANSACTION 等同义形式。连接 API 也通常能关闭自动提交、提交、回滚并设置隔离级别。隔离级别应在事务的最前面设置;连接池复用连接时,还要在归还前恢复自动提交、隔离级别等会话状态,避免下一个请求继承前一个请求的设置。
数据库事务不应跨越用户思考时间。让用户在选座页面停留几分钟,同时一直持有数据库锁,会阻塞其他买家。更好的做法是把交互分成“查看快照”和“短事务确认”两段;确认事务再次验证座位,失败时返回明确的重新选择结果。
简化模型给 read(Q) 一个确定数据项,SQL 查询却通过谓词动态决定行集合:
SELECT seat_no
FROM seats
WHERE show_id = :show_id
AND zone = 'A'
AND status = 'available';另一个事务插入一条新的 A 区可售座位,或把某行从 held 改成 available,会改变查询结果。第一次执行时那条记录并不存在于结果中,却在第二次查询时“出现”,这就是幻读。只锁住第一次返回的行不足以表达“所有满足这个谓词的记录范围”。
并发控制还要保护寻找记录所用的信息:关系中的范围、索引节点、键区间或等价的谓词。谓词锁直接把查询条件视为受保护对象;实际系统常通过索引范围锁、下一键锁或可串行化冲突检测来近似实现。更新搜索键也可能使一行移入或移出谓词范围,因此同样需要参与冲突判断。
设计事务时可以按三步检查:先写出业务不变量,再列出会影响不变量的行与谓词范围,最后选择能保护这些读写关系的数据库约束、SQL 写法和隔离级别。只给代码包一层 BEGIN/COMMIT,并不会自动修复逻辑上遗漏的并发条件。