文章目录
数据库保证事务正确性,依靠的是一组相互配合的机制:用事务边界把相关修改放在一起,用约束和业务判断检查结果,用版本与锁协调并发,再用日志和恢复机制处理失败。
理解它们,可以沿着一次下单走一遍:扣减库存、创建订单,这两项修改怎样一起成功,又怎样在出错时一起放弃?
本文基于普通持久化行存表,采用读已提交隔离级别,持久化示例假设启用可靠刷盘、提交等待本机 WAL 持久化,存储设备正确履行持久化要求。
一、先确定:哪些修改必须一起完成?
假设商品 7 的可用库存为 10,客户准备购买 2 件。数据库里需要完成两项修改:
- 在库存表
inventory中,把可用数量从 10 改为 8。 - 在订单表
orders中,新增订单 1007,记录商品 7、购买数量 2。
业务希望得到的是“库存扣了 2 件,对应订单也存在”。如果只完成一半,就可能出现库存被占用却找不到订单,或订单已经成立但没有扣减库存。
事务就是把这组数据库操作作为一个整体处理的单位。 应用通过 BEGIN 或 START TRANSACTION 开始一个事务,最后选择 COMMIT 提交,或用 ROLLBACK 放弃本次事务的修改。
下面是应用控制流程的伪代码,重点看分支关系:
在同一个数据库连接中开始事务
尝试扣减商品 7 的 2 件库存
如果实际更新行数不是 1:回滚,结束本次下单
新增订单 1007
如果发生 SQL 错误或业务检查失败:回滚,结束本次下单
所有必要步骤通过后:提交事务

图 1:两个操作共用一个提交结果。回滚取消的是本次事务的数据库修改,其他事务已经完成的业务仍然保留。
在自动提交模式下,独立执行的语句通常各自构成事务。如果第一条库存修改已经提交,随后订单插入才报错,后一个事务的回滚无法撤销前一个已经提交的事务。因此,事务边界需要由应用或框架明确管理,连接池场景也要确保这些操作使用同一事务连接。
二、执行 UPDATE 后,数据库里发生了什么?
扣库存可以把数量要求直接写进修改条件:
UPDATE inventory
SET available = available - 2
WHERE product_id = 7
AND available >= 2;
这里假设 product_id 唯一标识一行库存,available 是非空的库存数量,表上还可以用 CHECK (available >= 0) 约束防止负库存。应用必须检查受影响行数:更新 1 行表示完成了本次扣减;更新 0 行表示商品不存在或数量条件未满足,应按业务规则结束本次下单。
更新 0 行通常是合法的执行结果,而不是 SQL 错误。这说明正确性需要两方配合:数据库负责执行条件与约束,应用负责判断“这次业务要求是否达成”。如果应用忽略行数,仍然插入订单,数据库不会凭空知道业务步骤被遗漏了。
找到目标记录后,数据库会协调必要的写入锁,更新内存中的相关数据页,维护需要变动的索引,并生成用于恢复的 WAL。WAL 是预写式日志,记录数据库内部变化,不是把整条业务 SQL 当作文本存起来。
此时,数据可能已经在内存里发生变化,但事务仍处于进行中。同一事务后续的语句可以看到自己已经完成的修改;其他事务则按隔离规则判断能够读取哪个版本。
数据库会把数据版本与事务状态联系起来。可以先把相关状态理解为“进行中、已提交、已中止”:新版本是否可以被某次查询使用,需要同时看它来自哪个事务、该事务的状态,以及查询所使用的快照。
快照描述本次读取能够看到的数据范围。它通常通过事务与版本信息实现,数据库不需要在每次查询时复制整张表。保留多个版本、让查询按规则选择版本的机制,称为 MVCC,多版本并发控制。
三、两个人同时修改,怎样避免互相覆盖?
继续看库存 10 的例子。事务 A 买 2 件,同时事务 B 想买 9 件。为了看清过程,假设 A 先获得这行库存的修改机会。
A 执行扣减,得到尚未提交的库存 8。此时,另一个会话的普通查询可以依据自己的快照读取已提交的旧版本 10;B 若要修改同一行,则需要等待 A 的相关写锁释放。
在本例的读已提交级别下,A 提交后,B 对更新后的记录继续判断自己的条件。库存已经是 8,无法满足 available >= 9,所以这次扣减更新 0 行。B 的应用流程据此回滚,并告知库存不足。

图 2:版本机制帮助普通读取选择可见数据,锁协调同一行的竞争写入。图中省略其他事务与锁类型。
如果 A 最终回滚,B 等待结束后面对的则是没有被 A 成功扣减的库存状态。可见,等待结果与前一个事务的结局有关。
这个例子也解释了为什么“先 SELECT 得到 10,在应用里算出 8,再把 8 写回去”需要额外谨慎。读取与修改之间,其他事务可能已经更新了库存。如果直接写回先前计算的固定值,可能覆盖本应保留的变化。将扣减与数量条件放在同一条 UPDATE 中,是让本例的判断和修改共同接受数据库并发控制的一种做法。
隔离性决定并发事务如何观察和影响彼此,但业务仍要选择适合的读写方式。 普通读和写之间可以通过版本减少阻塞;同一行的竞争写入,以及显式加锁读取等场景,仍需要锁或冲突处理。
读已提交下,同一事务中前后两次普通查询可能看到其他事务在此期间提交的新结果。需要跨多次读取维持更稳定视图时,还要结合隔离级别和业务规则设计,本篇先掌握版本与锁各自解决的问题。
四、COMMIT 为什么不用等所有数据页都写回磁盘?
假设库存扣减成功,订单也插入成功,应用现在请求提交。提交阶段需要把事务确认为成功,并按照配置满足相应的日志持久化和复制等待条件。
前面的修改可能涉及库存表、订单表及多个索引,它们分散在不同数据页中。如果每次提交都等待所有相关页面分别写回,写入成本会很高。WAL 提供了另一条路径:先可靠保存恢复所需的变更和提交信息,数据页可以按缓冲区和检查点等机制推进写回。
其中有一条关键顺序:某个修改后的数据页写入持久化存储前,相应 WAL 要先获得持久化保障。 这就是“预写”的含义。

图 3:提交确认依赖必要日志得到可靠保存。数据页写回可以发生在提交前或提交后,始终受相应 WAL 先持久化的约束。
在本文的本机持久化前提下,事务所需 WAL,包括提交记录,可靠保存之后,数据库才按相应流程向应用确认成功。即使这时某些数据页还没写回,重启恢复仍能利用已有页面和 WAL 重建成功提交的修改。
提交确认的等待条件受配置影响。synchronous_commit = local 等待本机 WAL 持久化;如果配置了相应同步备库,on 还涉及备库日志持久化的确认。off 可以在本机日志尚未刷盘时返回,因此故障后可能失去近期已确认的事务。本篇用可靠本机持久化说明机制,不假定实际集群采用哪一种安装默认值。
本机崩溃恢复与切换到另一台备库,还涉及不同的数据存活条件。若本机存储一起损坏,能否保留最新事务,需要看其他副本或恢复材料是否已经保存相应日志。
五、执行到一半失败,或者提交后立即崩溃,会怎样?
先看正常回滚。假设扣减库存后,订单插入因编号重复而失败,应用放弃这次事务。数据库将其按失败事务处理,确保这次扣减和订单修改不会作为成功提交的结果保留下来。
这里恢复的是本次事务对业务数据的影响。内部可能仍存在等待清理的版本、日志和空间占用,回滚不要求把每个物理字节都立即改回原样。
再看突然断电。故障前,部分 WAL 或数据页可能已经写入存储,数据库需要在恢复过程中结合日志、事务状态和版本规则,确定哪些修改属于成功事务,哪些修改应当被排除或撤销。

图 4:数据库是否保留事务结果,取决于有效恢复信息;客户端有没有收到成功响应,是另一个观察角度。
如果没有形成可恢复的有效提交结果,恢复后的业务读取应排除这次事务的未提交修改。即使其中某些数据页曾经写盘,也要遵守事务状态与可见性规则。
如果所需日志和提交记录已经可靠保存,只是数据页还没有全部写回,恢复机制可以重建这次事务。这个过程让日志持久化与事务状态共同保障“成功提交的内容能够恢复”。
还有一个应用必须处理的情况:数据库已经完成提交,但成功响应在网络中丢失了。客户端看到连接断开,无法单凭这个现象确定事务是否成功。可靠的业务流程会利用稳定的订单号或请求编号核对结果,并设计幂等处理,让同一个请求重复到达时不会再次扣库存、重复生成订单。
这里的幂等需要完整的事务与业务设计。仅仅生成一个编号,或者在重试时换一个新编号,都不足以自动解决重复执行问题。
六、进阶补充:把这些机制对应到 ACID
现在再认识 ACID,四个字母便有了对应的过程。
| 特性 | 在本例中解决什么问题 | 主要依靠什么 |
|---|---|---|
| 原子性 Atomicity | 本次扣库存与建订单整体成功或整体放弃 | 事务边界、状态管理、回滚和恢复机制 |
| 一致性 Consistency | 库存与订单满足事先定义的规则 | 约束、正确的业务步骤,以及其他事务保障的配合 |
| 隔离性 Isolation | 并发读取和修改按约定规则进行 | 版本、快照、锁和冲突处理 |
| 持久性 Durability | 成功提交的修改在约定故障条件下可以保留 | WAL、可靠持久化、崩溃恢复及适当的副本保护 |
一致性需要先定义“正确”。非负库存、唯一订单号可以由相应约束检查;“只有扣减成功才创建订单”还需要应用正确组织流程。数据库不能从一段合法 SQL 自动推断全部业务意图。
原子性的实现也与存储引擎有关。Astore 采用追加版本的方式,失败事务的版本可通过事务状态和可见性规则排除,之后清理无用版本;Ustore 则结合 Undo 保存历史信息,支持旧版本读取和回滚。Undo 处理历史版本与撤销,WAL 支撑持久化恢复,两者职责不同,也会在整体恢复机制中配合。
这些保障针对纳入当前事务的数据库操作。发送短信、调用外部接口等动作,需要额外设计与数据库提交之间的协调;序列取号等机制也有专门语义,回滚后允许出现编号空缺。实际系统还可能因为死锁或并发冲突终止某个事务,应用应按错误类型决定是否重试整个业务事务。
关于隔离级别,openGauss 6.0 文档中的 SERIALIZABLE 功能上等价于 REPEATABLE READ,不能据名称直接套用其他数据库的严格可串行化承诺。
沿着这次下单回看,正确性来自清楚的分工:应用界定业务整体和成功条件,数据库协调执行、观察与冲突,日志和恢复机制再把事务结局延续到故障之后。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/taiyang3285/article/details/166442989




