Mysql事务基本原理

事务特性

事务特性是数据库系统中确保数据完整性和一致性的核心机制,通常用ACID四个特性来描述。

  • 原子性(Atomicity):事务是不可分割的工作单位,事务中的操作要么全部成功,要么全部失败回滚。
  • 一致性(Consistency):事务执行前后,数据库从一个一致状态转变为另一个一致状态。
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务。。
  • 持久性(Durability):事务一旦提交,其结果就是永久性的。

  这些特性协同解决了数据库系统中的核心问题:其中,一致性是最终目标,而原子性、隔离性和持久性共同服务于这一目标。具体而言,原子性通过 undo log 解决“部分失败”问题;隔离性借助 MVCC 或锁机制应对“并发冲突”问题;持久性依赖 redo log 防止“数据丢失”问题,最终,一致性通过这三者的协同作用得以实现。




并发问题

脏读

事务A读取了事务B未提交的修改数据,事务B随后回滚。这会造成事务A基于错误数据做出业务决策,示例如下:

1
2
3
4
5
6
7
-- 事务A
SELECT balance FROM accounts WHERE id = 1; -- 读取到事务B未提交的修改

-- 事务B
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 随后回滚
ROLLBACK;

不可重复读

事务A内多次读取同一数据,期间事务B修改并提交了该数据,导致两次读取结果不同。这会造成同一事务内数据不一致,影响业务逻辑判断,示例如下:

1
2
3
4
-- 事务A
SELECT balance FROM accounts WHERE id = 1; -- 第一次读取
-- 事务B此时提交了UPDATE...
SELECT balance FROM accounts WHERE id = 1; -- 第二次读取结果不同

幻读

事务A查询某条件范围内的数据,事务B插入/删除了符合该条件的新数据并提交,导致事务A两次查询结果集不同。这会影响范围查询的准确性,特别是基于结果集计数的操作,示例如下:

1
2
3
4
-- 事务A
SELECT * FROM orders WHERE status = 'pending'; -- 返回5条记录
-- 事务B此时INSERT了一条status='pending'的新订单并提交
SELECT * FROM orders WHERE status = 'pending'; -- 返回6条记录

丢失更新

两个事务同时读取同一数据并修改,后提交的事务覆盖了前一个事务的修改。这会导致数据更新丢失,业务操作不完整,示例如下:

1
2
3
4
5
-- 事务A和事务B同时执行:
SELECT balance FROM accounts WHERE id = 1; -- 都读到1000
-- 事务A: balance = 1000 - 100
-- 事务B: balance = 1000 - 200
-- 后提交的事务会覆盖前一个的修改

其他事务相关问题

锁等待超时:大事务执行时间长或未正确提交或回滚事务,导致当前事务持有锁时间过长,等待该锁的事务超时失败。

长事务问题:务执行时间过长(秒级甚至分钟级),长时间占用连接资源、持有锁时间影响到并发,并可能产生主从复制延迟、undo日志膨胀等问题。

死锁:两个或多个事务互相等待对方释放锁。

[!NOTE]

此前经历过运维人员在执行线上SQL工单时操作失败,但未及时回滚事务,导致该事务长时间持有锁。由于事务中涉及唯一索引操作,进而引发表级锁,最终造成该表的大量更新操作阻塞失败,数据一致性遭到破坏。




隔离级别

MySQL 的隔离级别主要通过 多版本并发控制(MVCC)锁机制 实现。不同隔离级别的行为差异主要源于 ReadView 生成时机锁策略 的不同。

隔离级别 ReadView生成时机 读操作锁策略 写操作锁策略 防止的问题
READ UNCOMMITTED 直接读取数据页的最新版本(无 MVCC 快照) 不加锁 排他锁(X锁),事务结束释放
READ COMMITTED 每次查询生成新的 ReadView,只读取已提交的数据 共享锁(S锁),读取后立即释放 排他锁(X锁),事务结束释放 脏读
REPEATABLE READ 事务首次查询时生成 ReadView,后续查询复用该视图 共享锁(S锁),事务期间保持 排他锁(X锁),事务结束释放 脏读、不可重复读
SERIALIZABLE 所有查询默认加共享锁(SELECT ... FOR SHARE),读写互斥 共享锁(S锁),事务结束释放 排他锁(X锁),事务结束释放 脏读、不可重复读、幻读



[!TIP]

刚开始,建议从版本链和ReadView的角度入手,这样可以清晰地理解 MVCC 的工作流程。


版本链

InnoDB 的每一行记录都包含两个隐藏字段:

  • DB_TRX_ID:记录最后一次修改该行的事务 ID(事务版本号)。
  • DB_ROLL_PTR:指向 undo log 的指针,用于构建版本链。

版本链的形成

  • 当事务修改某行数据时,InnoDB 不会直接覆盖原数据,而是:
    1. 将当前行的 DB_TRX_IDDB_ROLL_PTR 记录到 undo log(回滚日志)。
    2. 修改该行数据,并更新 DB_TRX_ID 为当前事务 ID,DB_ROLL_PTR 指向 undo log 中的旧版本。
  • 这样,每次修改都会在 undo log 中形成一个历史版本,并通过 DB_ROLL_PTR 串联成版本链


ReadView(一致性视图)

当事务执行 SELECT 时,InnoDB 会生成一个ReadView,用于判断哪些版本对当前事务可见。ReadView是C语言结构体,包含下列字段:

  • m_ids:当前活跃(未提交)的事务 ID 列表。
  • min_trx_id:当前活跃事务中的最小事务 ID,即m_ids 中的最小事务 ID。
  • max_trx_id:下一个要分配的事务 ID,即m_ids 中的最大事务 ID + 1。
  • creator_trx_id:当前事务的 ID。


[!TIP]

最后,基于 ReadView 和版本链,我们分析MVCC 工作流程。


当一个事务执行 SELECT 时,InnoDB 会:

  1. 找到数据行的最新版本(即当前存储的最新数据)。
  2. 检查该版本的 DB_TRX_ID
    • 如果 DB_TRX_ID < min_trx_id
      • 说明该版本是由已提交的事务修改的,对当前事务可见
    • 如果 DB_TRX_IDmax_trx_id
      • 说明该版本是由未来事务修改的(当前事务开始后创建的事务),不可见
    • 如果 DB_TRX_IDm_ids
      • 说明该版本是由未提交事务修改的,不可见
    • 否则(min_trx_idDB_TRX_ID < max_trx_id 且不在 m_ids 中):
      • 说明该版本是由已提交事务修改的,可见
  3. 如果当前版本不可见,则沿着 DB_ROLL_PTR 查找更早的版本,直到找到可见的版本或版本链结束。



工作备忘

  在维护一个基于Hibernate框架的遗留项目时,我遇到了一个典型的事务隔离级别问题。该项目配置的数据库事务隔离级别为REPEATABLE_READ(可重复读),在此环境下,同一事务内出现了无法读取自身更新的异常现象。
  经过深入排查,发现问题根源在于BaseDao类的实现存在设计缺陷。以updateEntity方法为例:

1
2
3
4
5
6
public void updateEntity(T entity) {
Session session = sessionFactory.getCurrentSession();
session.clear();
session.update(entity);
session.flush();
}

  在REPEATABLE_READ隔离级别下,事务只能看到其启动时的数据快照。当结合clear()和flush()操作时,会引发一系列连锁反应。
  首先,clear()强制清空一级缓存,导致后续操作必须重新从数据库加载数据。紧接着,flush()会将当前session中的变更强制同步到数据库,使更新操作真正生效。此时触发关键问题:虽然flush()已经更新持久化到数据库,但由于ReadView是事务开始时创建的,新加载的数据仍然会基于这个初始视图进行判断。
具体来说,虽然当前事务已经执行了更新操作,但这些更新在数据库中被标记为属于当前事务,而根据ReadView的可见性规则,这些修改在重新查询时会被过滤掉,导致查询到的仍然是事务开始时的旧数据。这样就形成了一个矛盾现象:事务明明已经执行了更新,却无法看到自己的修改,最终表现为”自我更新不可见”的异常现象。
  这个问题的本质在于,clear()操作破坏了Hibernate维护的事务内一致性,使得系统不得不依赖数据库层的ReadView机制,而该机制在可重复读隔离级别下会严格保持初始视图的一致性。




其他

相关SQL命令

1
2
3
4
5
6
7
8
9
10
11
-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 设置全局隔离级别REPEATABLE READ
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 设置当前会话隔离级别READ COMMITTED
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 设置下一个事务的隔离级别SERIALIZABLE
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

推荐书籍

《MySQL是怎样运行的》从根儿上理解MySQL