Mysql事务基本原理

Mysql事务基本原理
Jint事务特性
事务特性是数据库系统中确保数据完整性和一致性的核心机制,通常用ACID四个特性来描述。
- 原子性(Atomicity):事务是不可分割的工作单位,事务中的操作要么全部成功,要么全部失败回滚。
- 一致性(Consistency):事务执行前后,数据库从一个一致状态转变为另一个一致状态。
- 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务。。
- 持久性(Durability):事务一旦提交,其结果就是永久性的。
这些特性协同解决了数据库系统中的核心问题:其中,一致性是最终目标,而原子性、隔离性和持久性共同服务于这一目标。具体而言,原子性通过 undo log 解决“部分失败”问题;隔离性借助 MVCC 或锁机制应对“并发冲突”问题;持久性依赖 redo log 防止“数据丢失”问题,最终,一致性通过这三者的协同作用得以实现。
并发问题
脏读
事务A读取了事务B未提交的修改数据,事务B随后回滚。这会造成事务A基于错误数据做出业务决策,示例如下:
1 | -- 事务A |
不可重复读
事务A内多次读取同一数据,期间事务B修改并提交了该数据,导致两次读取结果不同。这会造成同一事务内数据不一致,影响业务逻辑判断,示例如下:
1 | -- 事务A |
幻读
事务A查询某条件范围内的数据,事务B插入/删除了符合该条件的新数据并提交,导致事务A两次查询结果集不同。这会影响范围查询的准确性,特别是基于结果集计数的操作,示例如下:
1 | -- 事务A |
丢失更新
两个事务同时读取同一数据并修改,后提交的事务覆盖了前一个事务的修改。这会导致数据更新丢失,业务操作不完整,示例如下:
1 | -- 事务A和事务B同时执行: |
其他事务相关问题
锁等待超时:大事务执行时间长或未正确提交或回滚事务,导致当前事务持有锁时间过长,等待该锁的事务超时失败。
长事务问题:务执行时间过长(秒级甚至分钟级),长时间占用连接资源、持有锁时间影响到并发,并可能产生主从复制延迟、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 不会直接覆盖原数据,而是:
- 将当前行的
DB_TRX_ID和DB_ROLL_PTR记录到 undo log(回滚日志)。 - 修改该行数据,并更新
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 会:
- 找到数据行的最新版本(即当前存储的最新数据)。
- 检查该版本的
DB_TRX_ID:- 如果
DB_TRX_ID<min_trx_id:- 说明该版本是由已提交的事务修改的,对当前事务可见。
- 如果
DB_TRX_ID≥max_trx_id:- 说明该版本是由未来事务修改的(当前事务开始后创建的事务),不可见。
- 如果
DB_TRX_ID∈m_ids:- 说明该版本是由未提交事务修改的,不可见。
- 否则(
min_trx_id≤DB_TRX_ID<max_trx_id且不在m_ids中):- 说明该版本是由已提交事务修改的,可见。
- 如果
- 如果当前版本不可见,则沿着
DB_ROLL_PTR查找更早的版本,直到找到可见的版本或版本链结束。
工作备忘
在维护一个基于Hibernate框架的遗留项目时,我遇到了一个典型的事务隔离级别问题。该项目配置的数据库事务隔离级别为REPEATABLE_READ(可重复读),在此环境下,同一事务内出现了无法读取自身更新的异常现象。
经过深入排查,发现问题根源在于BaseDao类的实现存在设计缺陷。以updateEntity方法为例:
1 | public void updateEntity(T entity) { |
在REPEATABLE_READ隔离级别下,事务只能看到其启动时的数据快照。当结合clear()和flush()操作时,会引发一系列连锁反应。
首先,clear()强制清空一级缓存,导致后续操作必须重新从数据库加载数据。紧接着,flush()会将当前session中的变更强制同步到数据库,使更新操作真正生效。此时触发关键问题:虽然flush()已经更新持久化到数据库,但由于ReadView是事务开始时创建的,新加载的数据仍然会基于这个初始视图进行判断。
具体来说,虽然当前事务已经执行了更新操作,但这些更新在数据库中被标记为属于当前事务,而根据ReadView的可见性规则,这些修改在重新查询时会被过滤掉,导致查询到的仍然是事务开始时的旧数据。这样就形成了一个矛盾现象:事务明明已经执行了更新,却无法看到自己的修改,最终表现为”自我更新不可见”的异常现象。
这个问题的本质在于,clear()操作破坏了Hibernate维护的事务内一致性,使得系统不得不依赖数据库层的ReadView机制,而该机制在可重复读隔离级别下会严格保持初始视图的一致性。
其他
相关SQL命令
1 | -- 查看当前隔离级别 |
推荐书籍
《MySQL是怎样运行的》从根儿上理解MySQL



