技术面:MySQL篇(InnoDB事务执行过程、事务隔离级别、事务并发异常)

MySQL的InnoDB引擎下更新操作时事务的执行过程

MySQL数据库在InnoDB中一次update的操作过程基本如下:

  1. 首先将数据加载到Buffer Pool里:当InnoDB需要更新一条记录时,首先会在Buffer Pool中查找该记录是否在内存中。若没在内存中,则从磁盘读取该页到Buffer Pool中。
  2. 记录UndoLog :在修改操作前,InnoDB会在Undo Log中记录修改前的数据。Undo Log是用来保证事务原子性和一致性的一种机制,用于在发生事务回滚等情况时,将修改操作回滚到修改前的状态,以达到事务的原子性和一致性。Undo Log 首先写入内存中的 Undo 缓冲区,随后由后台线程定期刷盘,无需等待事务提交。
  3. 在Buffer Pool中更新数据 :当执行update语句时,InnoDB会先更新已经读取到Buffer Pool中的数据,而不是直接写入磁盘。同时,InnoDB会将修改后的数据页状态设置为"脏页"(DirtyPage)状态,表示该页已经被修改但尚未写入磁盘。
  4. 记录RedoLog Buffer:InnoDB在Buffer Pool中记录修改操作的同时,InnoDB会先将修改操作写入到Redo Log Buffer 中。
  5. 提交事务:在执行完所有修改操作后,事务被提交。在提交事务时,InnoDB会将Redo Log写入磁盘,以保证事务持久性(commit 时会保证 write+fsync,但 log buffer 中的内容可能在 prepare 阶段就已被后台线程提前刷盘)。
  6. 写入磁盘:在提交后,InnoDB会将Buffer Pool中的脏页写入磁盘,以保证数据的持久性。但是这个写入过程并不是立即执行的,是有一个后台线程异步执行的,所以可能会延迟写入,总之MySQL会选择合适的时机把数据写入磁盘做持久化。
  7. 记录Binlog:在提交过程中,MySQL Server层将事务提交的信息记录到Binlog中。Binlog是MySQL用来实现主从复制的一种机制,用于将主库上的事务同步到从库上。在Binlog中记录的信息包括:事务开始的时间、数据库名、表名、事务ID、SQL语句等。

什么是脏读、幻读、不可重复读

在整个事务的执行过程中,当出现并发、以及兼顾性能等情况,会出现一系列的问题,但是针对不同的情况,MySQL 也给出了针对性方案。下面就针对脏读幻读不可重复读三种异常情况来说明一下。

脏读

脏读是指读到了其他事务还没提交的数据。

脏读产生的过程是当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交(commit)到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。

不可重复读

不可重复读是指在对某数据进行读取过程中,有其他事务对数据进行了修改(UPDATEDELETE),导致第二次读取的结果不同。

不可重复读的发生过程,在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。这是由于查询时系统中其他事务修改的提交而引起的。比如事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。

幻读

幻读是指事务在做范围查询过程中,有另外一个事务对范围内新增了记录(INSERT),导致范围查询的结果条数不一致。

幻读也常被看作不可重复读在'范围查询'场景下的特殊表现

例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中符合条件的的"全部数据行"。

同时,第二个事务也修改这个表中的数据,这种修改是向表中插入"一行新数据"。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。

MySQL中的事务隔离级别

脏读幻读不可重复读 这三种异常情况,产生的主要原因还是由于数据库支持多个事务同时执行时,互相操作相同的数据。那么事务之间能看到哪些数据呢?

这就是由数据库的事务隔离级别来决定的。

事务隔离级别,主要用于定义事务处理过程中,不同事务之间可见性和相互影响程度的一套标准。

业内标准SQL-92(也叫 SQL2)是 ANSI/ISO 在 1992 年发布的第二版 SQL 标准,它对 1986 年的 SQL-86 做了大幅扩充,奠定了今天主流关系数据库(Oracle、DB2、SQL Server、PostgreSQL、MySQL 等)所支持的核心功能。

SQL-92定义了4 种隔离级别来解决脏读幻读不可重复读等这些异常情况,从高到底依次为:可串行化(Serializable)可重复读(Repeatable reads)读已提交(Read committed)读未提交(Read uncommitted)

读未提交(Read uncommitted) ,最低级的隔离级别,就是说这种隔离级别下,一个事务可以读取到另一个事务未提交的数据。这种事务隔离级别下,会产生,脏读、幻读、不可重复读

读已提交(Read committed) ,这种的事务隔离级别下,一个事务如果在修改数据,还未提交事务,其他事务是不能够读取该数据的。因此这种事务隔离级别可以防止脏读

可重复读(Repeatable reads) ,这种事务隔离级别下,比读已提交更严谨,不但可以防止脏读 ,还能够防止不可重复读但是没办法彻底解决幻读问题

可串行化(Serializable) ,这种事务隔离级别下,所有的事务都是按照顺序串行执行,但是事务并发执行速度也是最慢的。此隔离级别可以防止脏读、幻读、不可重复读

以上是SQL-92标准中对事务隔离级别的定义,但是不同的数据库在实际实现的过程中,是有一些细微差异的。

MySQL的InnoDB引擎是如何解决脏读、幻读、不可重复读

解决脏读

脏读是在一个事务中读取到了另一个事务中未提交的数据,在【读已提交 】隔离级别下,事务只能读取到其他事务已经提交的数据版本。这就是靠MVCC(多版本并发控制)来实现的。

InnoDB下,系统会检查每个数据行的版本,只有当该版本是由已提交事务修改的,才对当前事务可见。

当事务在【读已提交 】隔离级别下执行读取操作时,InnoDB获取当前最新的全局事务ID,这个ID表示在当前时刻所有已提交事务的最新状态。InnoDB会检查每个数据行的版本,如果该版本是由一个小于或等于当前事务ID的事务修改的,并且该事务已提交,则这个版本是可见的。这保证了事务只能看到在它开始之前已经提交的数据版本。

实现原理

通过行的隐藏字段如:DB_TRX_ID (事务ID)和ReadView 来判断数据版本的可见性。
DB_TRX_ID 6字节,是全局事务ID,记录最后修改该行的事务ID。
DB_ROLL_PTR:7字节,回滚指针,指向undo log中的历史版本
DB_ROW_ID:6字节,行ID(当表没有主键时使用)

ReadView

ReadView 是 InnoDB 用来实现 MVCC(多版本并发控制) 的核心数据结构,它相当于事务在执行 快照读(普通 SELECT) 时生成的一张"瞬时照片",记录了当前系统中所有活跃事务(已开启但未提交)的 ID 列表,从而决定当前事务能看到哪些版本的数据。

ReadView的结构

bash 复制代码
struct read_view_t{
	trx_id_t	low_limit_id; /*!< 大于等于此ID的事务不可见 */
	trx_id_t	up_limit_id; /*!< 小于此ID的事务可见 */
	trx_id_t*	trx_ids; /*!< 创建时的活跃事务ID列表 */
	trx_id_t	creator_trx_id; /*!< 创建该ReadView的事务ID */
};

例如:

bash 复制代码
trx_ids = [20,28,34,40);
low_limit_id =40;
up_limit_id = 20;
creator_trx_id = 38

上面这种情况下:
trx_id < 20:说明改trx_id的事务在这个ReadView生成前已提交,那么该事务的结果是可见
trx_id > 20 & trx_id < 40:说明该trx_id的事务在这个ReadView生成时,是活跃状态,那么这个记录对于当前事务(创建ReadView的事务)来说不可见。
trx_id ≥ 41:未来事务,对于创建ReadView的事务来说,不可见。

不同隔离级别下ReadView 的行为

隔离级别 ReadView 创建时机 是否复用
READ UNCOMMITTED(读未提交) 不创建 ReadView,直接读最新版本 -
READ COMMITTED(读已提交) 每次执行 SELECT 时都创建一个新的 ReadView 不复用
REPEATABLE READ(可重复读) 事务中第一次 SELECT 时创建 ReadView,之后整个事务复用 复用
SERIALIZABLE(串行化) 退化为加锁机制,不依赖 MVCC -

解决不可重复读

不可重复读在同一事务中,多次读取同一数据返回的结果不同。

解决方案:InnoDB主要是通过MVCCReadView机制解决不可重复读问题。

核心实现:
在【可重复读】REPEATABLE READ,这种事务隔离级别下,当使用快照读,读取数据时,只会在第一次读取的时候生成一个ReadView,后续事务中所有快照读都用同一个快照,这样就不会发生不可重复读的问题了。

即使其他事务修改了数据并提交,当前事务仍然看到原来的版本。

解决幻读

幻读在事务的范围查询中,多次查询出来的数量不一致。

解决方案:
InnoDB的RR隔离级别(可重复读)通过MVCC + 间隙锁(Next-Key Lock)的组合机制解决幻读问题。

这种方式只是在一定程度上解决了幻读,但是并没有完全避免,当一个事务中,对同一段范围查询,既有快照读又有当前读时,可能出现"先快照、后当前"导致的幻读,需要业务层注意。

分场景来说明是如何解决幻读的

快照读场景:

MVCC机制 :普通SELECT语句使用快照读,基于ReadView机制可以避免幻读
实现原理 :事务只能看到ReadView创建之前存在的数据,新插入的数据对当前事务不可见。

当前读场景:

Next-Key Lock机制 :对于SELECT ... FOR UPDATE、UPDATE、DELETE等当前读操作

技术细节

  • 不仅锁定读取到的记录,还会锁定记录之间的间隙(Gap Lock
  • 防止其他事务在查询范围内插入新数据。
  • 通过锁定索引记录和记录之间的间隙来避免幻读。

总结

MySQL InnoDB 的 UPDATE 语句看似一条简单命令,背后却要走完"加载→写 Undo→改内存→写 Redo→两阶段提交→Binlog→异步刷脏"整条链路;
其中 Redo 保证崩溃恢复,Binlog 保证主从复制,Undo 提供回滚与 MVCC 多版本。

SQL-92 的 4 种隔离级别,在 InnoDB 里被细化为 MVCC + 锁的混合方案:

  • READ UNCOMMITTED 直接读最新版,可能脏读;
  • READ COMMITTED 每次新建 ReadView,解决脏读;
  • REPEATABLE READ 复用首次 ReadView,解决不可重复读,并用 Next-Key Lock 在当前读场景下堵住幻读;
  • SERIALIZABLE 退化为纯加锁,彻底串行。

理解这套"日志先行、MVCC 多版本、锁补边界 "的设计,才能在业务开发中正确选择隔离级别、合理使用当前读与索引,避免"锁等待 "或"幻读"带来的意外结果。

相关推荐
科技小花2 小时前
数据治理平台架构演进观察:AI原生设计如何重构企业数据管理范式
数据库·重构·架构·数据治理·ai-native·ai原生
一江寒逸2 小时前
零基础从入门到精通MySQL(中篇):进阶篇——吃透多表查询、事务核心与高级特性,搞定复杂业务SQL
数据库·sql·mysql
D4c-lovetrain3 小时前
linux个人心得22 (mysql)
数据库·mysql
阿里小阿希3 小时前
CentOS7 PostgreSQL 9.2 升级到 15 完整教程
数据库·postgresql
荒川之神3 小时前
Oracle 数据仓库雪花模型设计(完整实战方案)
数据库·数据仓库·oracle
做个文艺程序员3 小时前
MySQL安全加固十大硬核操作
数据库·mysql·安全
不吃香菜学java4 小时前
Redis简单应用
数据库·spring boot·tomcat·maven
一个天蝎座 白勺 程序猿4 小时前
Apache IoTDB(15):IoTDB查询写回(INTO子句)深度解析——从语法到实战的ETL全链路指南
数据库·apache·etl·iotdb
不知名的老吴4 小时前
Redis的延迟瓶颈:TCP栈开销无法避免
数据库·redis·缓存
YOU OU4 小时前
三大范式和E-R图
数据库