一文读懂数据库事务

发布时间:2026/9/6 14:51:25
一文读懂数据库事务 文章目录1.什么是事务2.生活中的事务3.数据库事务与四大特性4.事务并发问题脏读不可重复读幻读5.事务隔离级别读未提交读已提交可重复读串行化6.MVCC6.1 简介6.2 原理版本链读视图6.3 举例说明6.4 小结7.更新丢失8.数据库事务的使用参考文献1.什么是事务首先说一下什么是事务。事务Transaction指一个操作由多个步骤组成要么全部成功要么全部失败。比如我们常用的转账功能假设 A 账户向 B 账号转账那么涉及两个操作从 A 账户扣钱。往 B 账户加入等量的钱。因为是独立的两个操作所以可能有一个成功一个失败的情况。但是因为在这种场景下不能存在从 A 账户扣钱成功往 B 账户加入等量钱失败这种情况要么同时成功要么同时失败一个失败需要回滚即必须要保证事务。2.生活中的事务事务不止存在于数据库中生活中处处存在事务只要是涉及多个步骤来完成一件事情时就涉及到事务。比如彩礼三金和结婚是一个事务男方给了价值几十万的彩礼三金女方会答应如期将女儿嫁出。如果女方毁约一般会如数退还彩礼三金。如果遇到蛮横无理的女方那么就破坏了事务男方会采取法律或特殊手段要回彩礼三金强制达成事务。再如菜市场买东西一手交钱一手交货。购买机票到最后完成乘机或退还机票2021 年春节因疫情尚未结束倡导就地过年出现大面积免手续费退票的情况。网购满意确收货或不满意退款退货。等等。3.数据库事务与四大特性什么是数据库事务数据库事务Database Transaction是指对数据库的一系列操作组成的逻辑工作单元。并非任意的数据库操作序列都是数据库事务。数据库管理系统DBMS在写入或更新资料的过程中为保证事务Transaction正确可靠必须具备四个特性ACID。原子性Atomicity事务作为一个整体被执行包含在其中的对数据库的操作要么全部被执行要么都不执行。一致性Consistency一致性确保事务将数据库从一种一致的状态转换为另一种一致的状态。换句话说事务在执行前后数据库必须满足一些预定义的一致性规则如约束、触发器、级联等。如果事务执行后数据库不满足这些规则整个事务将被回滚。隔离性Isolation多个事务并发执行时一个事务的执行不影响其他事务的执行。持久性Durability已被提交的事务对数据库的修改应该永久保存在数据库中。我们还是用上面“A账户向B账号汇钱”的例子来说明如何通过数据库事务保证数据的正确性。熟悉关系型数据库事务的都知道从帐户 A 转帐到账户 B 需要 6 个操作。1 从 A 账户中把余额读出来500 2 对 A 账户做减法操作500-100 3 把结果写回 A 账号中400 4 从 B 账户中把余额读出来500 5 对 B 账户做加法操作500100 6 把结果写回 B 账户中600原子性保证1-6所有过程要么都执行要么都不执行。一旦在执行某一步骤的过程中发生问题就需要执行回滚操作。 假如执行到第五步的时候B账户突然不可用比如被注销那么之前的所有操作都应该回滚到执行事务之前的状态。一致性在转账之前A和B的账户中共有5005001000元钱。在转账之后A和B的账户中共有4006001000元。也就是说数据的状态在执行该事务操作之后从一个状态改变到了另外一个状态两个状态数据总额是一致的不能凭空变多或变少。隔离性在 A 向 B 转账的整个过程中只要事务还没有提交commit查询 A 账户和 B 账户的时候两个账户里面的钱的数量都不会有变化。如果在 A 给 B 转账的同时有另外一个事务执行了 C 给 B 转账的操作那么当两个事务都结束的时候B 账户里面的钱应该是 A 转给 B 的钱加上 C 转给 B 的钱再加上自己原有的钱。持久性一旦转账成功事务提交两个账户的里面的钱就会真的发生变化会把数据写入数据库做持久化保存。事务是个好多西因为它符合我们的预期。但是很多场景下很难保证事务或者说保证事务需要付出很大的成本。此时需要我们权衡利弊设计出低成本又符合实际应用场景的方案。MySQL InnoDB 引擎通过什么技术来保证事务的这四个特性的呢原子性是通过 undo log回滚日志 来保证。持久性是通过 redo log 重做日志来保证。隔离性是通过 MVCC多版本并发控制 和锁机制来保证。一致性则是通过持久性原子性隔离性来保证。4.事务并发问题数据库会被广大用户共享访问那么在数据库并发操作过程中可能会出现一些不确定的情况。脏读Dirty Read不可重复读Non-repeatable Read幻读Phantom Read这三个现象的严重性排序如下脏读读取未提交数据。事务 A 读取事务 B 尚未提交的数据此时如果事务 B 发生错误并回滚那么事务 A 读取到的数据就是脏数据。不可重复读前后多次读取数据内容不一致。事务 A 在事务 B 开始前读和事务 B 结束后读的数据不一样因为数据被事务 B 修改了。幻读当同一个查询在不同时间产生不同的结果集时称之为幻读。比如事务 A 在读取某个范围内的记录时事务 B 在该范围内插入了新记录或删除了旧记录事务 A 再次读取该范围内的记录时前后获取的结果集不同产生了幻读。幻读比不可重复读取更难防范因为锁定第一个查询结果集中的所有行并不能阻止导致幻像出现的更改。为了解决上面的事务并发问题于是有了事务隔离。5.事务隔离级别事务隔离有多个级别每个隔离级别都有不同的特点和能力以解决并发访问数据库时可能出现的不同问题。SQL:1992 标准定义了四个隔离级别及其解决的问题隔离级别越高性能效率越低。读未提交Read Uncommitted读已提交Read Committed可重复读Repeatable Read串行化Serializable按隔离水平高低排序如下针对不同的隔离级别并发事务可能发生的现象也会不同。读未提交允许脏读、不可重复读和幻读。最低的隔离级别事务可以读取其他事务尚未提交的数据虽然拥有超高的并发处理能力及很低的系统开销但很少用于实际应用因为可能导致数据不一致性。读已提交不允许脏读但允许不可重复读和幻读。事务只能读取已经提交的数据避免了脏读问题但可能导致不可重复读和幻读。这是大多数数据库系统的默认隔离级别如 Oracle 和 SQL Server但不是 MySQL 的默认。可重复读不允许脏读、不可重复读但允许幻读。事务在整个事务期间保持一致的快照其他事务的修改不会影响正在运行的事务从而防止不可重复读问题。这是 MySQL InnoDB 默认的事务隔离级别。串行化解决所有事务并发问题。最高的隔离级别通过强制事务排序使之不可能相互冲突从而防止所有并发问题。虽然这个隔离级别可以解决上面提到的所有并发问题由于事务是串行执行所以效率会大大下降应用程序的性能会急剧降低。最直观的体现就是当数据库隔离级别设置为串行化后A 事务在未提交之前B 事务对 A 事务数据的操作都会被阻塞。通常不会使用这个隔离级别我们需要其他机制来解决这些问题比如乐观锁和悲观锁。下面表格总结了事务并发问题和四大隔离级别的关系。隔离级别脏读不可重复读幻读读未提交✓✓✓读已提交x✓✓可重复读xx✓串行化xxx每个隔离级别都在一定程度上解决了事务并发问题但隔离级别越高并发性能越低因为更高级别的隔离通常需要更多的锁和资源开销。因此在选择隔离级别时需要根据应用场景平衡一致性和性能选择合适的隔离级别。四种隔离级别具体是如何实现的呢对于「读未提交」隔离级别的事务来说因为可以读到未提交事务修改的数据所以直接读取最新的数据就好了。对于「读已提交」和「可重复读」隔离级别的事务来说它们是通过多版本并发控制MVCC来实现的。对于「串行化」隔离级别的事务来说通过加读写锁的方式来避免并行访问。6.MVCC6.1 简介MVCCMulti-Version Concurrency Control是多版本并发控制以乐观锁为理论基础。通过对数据行的多个版本管理来实现数据库的并发控制。这样我们就可以通过比较版本号决定数据是否显示出来读取数据的时候不需要加锁也可以保证事务的隔离效果以此提高数据库并发性能。MVCC 没有固定的实现规范不同数据库一般会有不同的实现方式。MySQL 中 InnoDB 采用了 MVCC 来实现“读已提交“和“可重复读”两个隔离级别。其他两个隔离级别和 MVCC 不兼容因为“读未提交”总是读取最新的数据行不需要进行版本控制而“串行化”则会对所有读取的行加锁。6.2 原理MVCC 的核心实现主要基于两部分版本链和读视图。为了方便描述首先我们创建一个表 book就三个字段分别是主键 book_id名称 book_name 和库存 stock。然后向表中插入一些数据INSERTINTObookVALUES(1,数据结构,100);INSERTINTObookVALUES(2,C指南,100);INSERTINTObookVALUES(3,精通Java,100);版本链对于使用 InnoDB 存储引擎的表其聚簇索引记录包含了两个重要的隐藏列事务IDDB_TRX_ID每当事务对聚簇索引中的记录进行修改时都会把当前事务的 id 记录到 DB_TRX_ID 中。回滚指针DB_ROLL_PTR每当事务修改聚簇索引中的记录时都会把该记录的旧版本写到 undo 日志通过 DB_ROLL_PTR 指针可以获取该记录的旧版本。如果一个事务多次修改记录则每次修改都会生成 undo 日志并且这些 undo 日志通过 DB_ROLL_PTR 指针串联成一个版本链。版本链的头结点是该记录的最新值尾结点是事务开始时的初始值。例如我们在表 book 中做以下修改BEGIN;UPDATEbookSETstock200WHEREid1;UPDATEbookSETstock300WHEREid1;COMMIT;那么 id1 的记录此时的版本链如下图所示读视图读视图Read View是实现 MVCC 的关键部分用于管理事务的可见性以确保每个事务在读取数据时只看到在事务开始之前已经提交的数据版本。「读已提交」和「可重复读」的区别就在于它们生成 Read View 的策略不同。大家可以把 Read View 理解成一个数据快照就像相机拍照那样定格某一时刻的风景。“读已提交是在每个语句执行前都会重新生成一个 Read View而“可重复读“是启动事务时生成一个 Read View然后整个事务期间都在用这个 Read View。要理解 Read View 需要知道 Read View 中四个重要字段的作用。creator_trx_id 创建读视图的事务 id。m_ids 创建读视图时当前数据库中「活跃事务」的事务 id 列表。注意是一个列表活跃事务指启动了但还没提交的事务。min_trx_id 创建读视图时当前数据库中「活跃事务」中事务 id 最小的事务也就是 m_ids 的最小值。max_trx_id 这个并不是 m_ids 的最大值而是创建读视图时当前数据库应该分配给下一个事务的 id 值也就是全局事务中最大事务 id 值 1。在创建读视图后我们可以将记录中的 trx_id 划分为三种情况一个事务去访问记录的时候除了自己的更新记录总是可见之外还有这几种情况1如果记录的 trx_id 小于 min_trx_id表示这个版本的记录是在创建 Read View 由已经提交的事务生成所以该版本的记录对当前事务可见。2如果记录的 trx_id 大于等于 max_trx_id表示这个版本的记录是在创建 Read View 后才启动的事务生成的所以该版本的记录对当前事务不可见。3如果记录的 trx_id 在 Read View 的 min_trx_id 和 max_trx_id 之间需要判断 trx_id 是否在 m_ids 列表中如果记录的 trx_id 在 m_ids 列表中表示生成该版本记录的事务依然活跃着还没提交事务。如果记录的 trx_id 不在 m_ids 列表中表示生成该版本记录的事务已经被提交。这种通过「版本链」来「读视图」控制事务并发访问同一个记录的行为就叫 MVCC多版本并发控制。6.3 举例说明最后我们来举个例子让我们更好理解上面的内容。比如我们有如下表现在有一个 id 是 60 的事务执行如下语句并提交updateusersetname强哥1whereid1;此时 undo log 存在版本链如下提交事务 id 是 60 的记录后接着有一个事务 id 为 100 的事务修改name强哥2但是事务还没提交。则此时的版本链是此时另一个事务发起 select 语句查询 id1 的记录因为 trx_ids 当前只有事务 id 100所以该条记录不可见继续查询下一条发现 trx_id60 的事务号小于 min_trx_id则可见直接返回结果“强哥1”。那这时候我们把 id 为 100 的事务提交了并且新建了一个事务 id 为 110 也修改 id 为 1 的记录name强哥3并且不提交事务。这时候版本链就是这时候之前那个 select 事务又执行了一次查询要查询 id 为 1 的记录。如果是「读已提交」此时会重新生成一个 Read View那你的活动事务列表中的值就变了变成了[110]。按照上的说法你去版本链通过 trx_id 对比查找到合适的结果就是“强哥2”。如果你是「可重复读」此时使用的 Read View 还是第一次 SELECT 时生成的所以查询结果是“强哥1”。因为第二次查询结果和第一次一样所以叫可重复读。也就是说「读已提交」隔离级别是在每次读取数据时都会生成一个新的 Read View。「可重复读」隔离级别是启动事务时生成一个 Read View然后整个事务期间都在用这个 Read View。6.4 小结MVCC 通过版本链实现多版本管理通过 Read View 生成策略的不同实现实现「读已提交」和「可重复读」这两种隔离级别。这样子可以使不同事务的读写操作并发执行从而提升系统性能。在 MySQL 中「读已提交」和「可重复读」隔离级别的区别就是它们生成 Read View 的方式不同。「读已提交」每次查询都会重新生成一个 Read View做到每次提交后的数据可被当前事务读到。「可重复读」一直使用启动事务时生成的 Read View直到当前事务提交以此保证可重复读。7.更新丢失事务并发时不仅存在读的问题还有可能存在更新丢失的情况。更新丢失Update Lost指更新结果被其他事务覆盖。两个事务同时读取相同数据并分别修改后一个事务的修改覆盖了另一个事务的修改。这是因为系统没有执行任何锁操作因此并发事务没有被隔离开来。第一类更新丢失回滚丢失。比如 A 事务对某一列 1B 事务对某一列 2。A 事务事务提交后B 事务回滚了导致 A 事务更新丢失。第二类更新丢失提交丢失。比如 A 事务对某一列 1B 事务对某一列 2A B 事务执行完成后正常预期结果是某一列值被 3但是 B 事务的结果覆盖了 A 事务导致结果只被 2A 事务的更新丢失了。SQL 标准并未提及更新丢失的问题所以不同隔离级别下是否会存在更新丢失的问题不同数据库厂商实现有所不同。比如 SQL Server 和 PostgreSQL 在 Repeatable Read 隔离级别下不会出现更新丢失。但对于 MySQL 在 Repeatable Read 隔离级别下会出现更新丢失需要额外加锁来避免此问题。MySQL 可以通过以下办法避免更新丢失。提升隔离级别至串行化Serializable使用乐观锁比如版本号的 CASCompare And Swap使用悲观锁先表上加上意向排他锁然后对读取的记录加排他锁 SELECT xxx FOR UPDATE8.数据库事务的使用对于单条 SQL 语句数据库系统自动将其作为一个事务执行这种事务被称为隐式事务。要手动把多条SQL语句作为一个事务执行使用 BEGIN 开启一个事务使用 COMMIT 提交一个事务这种事务被称为显式事务例如把上述的转账操作作为一个显式事务。BEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;UPDATEaccountsSETbalancebalance100WHEREid2;COMMIT;很显然多条 SQL 语句要想作为一个事务执行就必须使用显式事务。COMMIT 是指提交事务即试图把事务内的所有 SQL 所做的修改永久保存。如果 COMMIT 语句执行失败了整个事务也会失败。有些时候我们希望主动让事务失败。这时可以用 ROLLBACK 回滚事务整个事务会失败。BEGIN;UPDATEaccountsSETbalancebalance-100WHEREid1;UPDATEaccountsSETbalancebalance100WHEREid2;ROLLBACK;数据库事务是由数据库系统保证的我们只需要根据业务逻辑使用它就可以。参考文献ISO/IEC 9075:1992, the Database Language SQL15.7.2.1 Transaction Isolation Levels - MySQLMySQL 8.0 Reference Manual :: 17.7.4 Phantom RowsRe: does repeatable read prevent lost update with pessimisticPrevent lost updates with high transaction isolation levels - stackoverflow.comMySQL Repeatable Read isolation level and Lost Update phenomena彻底理解数据库事务数据库事务隔离级别 - 博客园快速理解脏读、不可重复读、幻读和MVCC - 腾讯云浅谈MySQL并发控制隔离级别、锁与MVCC - 稀土掘金图解MySQL介绍 - 小林coding