【MySql】事务的原理
- 数据库的隔离级别原理
- 读未提交
- 读已提交
- 可重复读(Repeatable Read)
- 串行化(最高的隔离级别,强制事务串行执行,避免了所有并发问题)
- MVCC(Multi-Version Concurrency Control多版本并发控制)
- UNDO日志版本链(数据库原子性原理)
- 介绍
- UNDO的实现
- Read View(一致性视图)
- ***Read View的执行流程***
- Read View的可见性规则
- 数据库的一致性
- 持久性
- 事务的优化
事务:要么全部成功要么全部失败,目的是为了保证数据最终的一致性。
数据库的隔离级别原理
在事务并发执行时,他们内部的操作不能互相干扰,隔离 性由MySQL的各种锁以及MVCC机制来实现
先了解下面三个概念
- 脏读(Dirty Read)
指一个事务读取到了另一个未提交事务修改的数据。 - 不可重复读(Non-Repeatable Read)
指同一个事务内,两次读取同一数据时,结果不一致(因为中间被其他已提交事务修改 / 删除了)。 - 幻读(Phantom Read)
指同一个事务内,两次执行相同的查询语句时,返回的结果集行数不一致(因为中间被其他已提交事务新增 / 删除了符合条件的记录)。 - 可重复读
可重复读的核心目标是:保证同一事务内,多次读取同一数据时,结果始终一致,不受其他已提交事务的修改影响,从而避免 “不可重复读” 问题。
例子:在这个事务执行的时候,InnoDB 会为该事务创建一个 一致性视图(Read View),他事务即使修改了数据并提交,只要当前事务没结束,就看不到这些修改(因为视图没变)。 - 脏写(Dirty Write)
是指一个事务修改了另一个未提交事务已经修改过的数据。
比如:事务 A 修改数据 X 为 100(未提交);
事务 B 此时也修改数据 X 为 200(未提交);
最终无论 A/B 是否提交,A 的修改都会被 B 覆盖(或反之),导致未提交的中间状态被直接覆盖。
所以不建议在 Java 代码中先读取数据、在内存中计算(如做加法),再更新回数据库。这种方式在并发场景下很容易出现 “更新丢失” 问题,而应该尽量通过数据库层面的原子操作来实现。
优先用数据库原子操作(推荐)
示例:给用户余额加 100 元
// 条件构造器:更新id=1的用户,balance = balance + 100
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", 1).setSql("balance = balance + 100"); // 直接在数据库层面计算// 执行更新(底层生成 SQL:UPDATE user SET balance = balance + 100 WHERE id = 1)
userMapper.update(null, updateWrapper);
读未提交
读未提交(Read Uncommitted)
最低的隔离级别,允许一个事务读取另一个未提交事务的数据
可能出现的问题:脏读、不可重复读、幻读
适用场景:对数据一致性要求极低,追求最高并发性能的场景
SET transaction_isolation = 'READ-UNCOMMITTED';
BEGIN;
select * from app_userw where id = 1;
使用读未提交事务进行修改
SET transaction_isolation = 'READ-UNCOMMITTED';-- 开启事务
BEGIN;-- 执行更新操作
UPDATE app_userw
SET age = 30
WHERE id = 1;
我并没有提交更新但是读到的age已经被修改成了30这就是读未提交(READ UNCOMMITTED)
在这种情况下如果使用代码去读就会”脏读“。
读已提交
读已提交(Read Committed)
跟上面不一样的点则是如果未提交则不能被读
保证一个事务只能读取另一个已提交事务的数据
解决了脏读问题,但仍可能出现不可重复读和幻读
适用场景:大多数关系型数据库的默认隔离级别(如 SQL Server、Oracle),适用于对数据一致性有一定要求的场景
可重复读(Repeatable Read)
确保同一事务中多次读取同一数据的结果一致
解决了脏读和不可重复读问题,但仍可能出现幻读
适用场景:MySQL 的默认隔离级别,适用于需要保证数据可重复读取的场景
串行化(Serializable)
实现原理:
第一次查的时候都是查视图,除非使用了update等操作才会进行数据的更新,但是也仅限于更新的那条数据而已,其他的数据还是视图的数据。
串行化(最高的隔离级别,强制事务串行执行,避免了所有并发问题)
解决了脏读、不可重复读和幻读所有问题
适用场景:对数据一致性要求极高,但可以接受极低并发性能的场景
对事务操作的数据加锁(如表锁、行锁、范围锁),阻止其他事务同时修改或读取。例如,事务 A 操作某行数据时,其他事务必须等待 A 完成后才能操作。
MVCC(Multi-Version Concurrency Control多版本并发控制)
MVCC 通过 “版本链存储历史数据 + Read View 判断可见性” 的机制,巧妙地让读写操作并行执行,同时通过事务 ID 和可见性规则保证隔离性。
数据库事务是通过MVCC机制来实现的,其核心思想是通过UNDO保存数据的多个历史版本,让读写操作在不阻塞彼此的情况下并行执行,同时保证事务隔离性。
InnoDB 会为每个数据行自动添加三个隐藏列,用于版本管理:
DB_TRX_ID:记录最后一次修改该行数据的事务 ID(6 字节)。
DB_ROLL_PTR:回滚指针(7 字节),指向该行的上一个历史版本(存储在 undo 日志中)。
DB_ROW_ID:若表没有主键,InnoDB 会自动生成该列作为隐含主键(6 字节)。
UNDO日志版本链(数据库原子性原理)
介绍
UNDO 日志作为版本链的载体,不仅支撑了 MVCC,还为事务回滚提供了基础,是数据库高并发能力的核心技术之一。 记录事务对数据的修改操作(如插入、更新、删除)的反向操作,用于在事务回滚或需要读取历史版本时恢复数据。
若事务执行失败(如代码异常、数据库崩溃),数据库会通过 undo 日志反向执行所有操作,将数据恢复到事务开始前的状态,保证 “全不做”。
1.事务提交机制
事务只有在显式执行COMMIT时,所有修改才会被确认(持久化到磁盘);若执行ROLLBACK或异常终止,数据库直接根据 undo 日志回滚,确保 “全不做”。
2.崩溃恢复
数据库崩溃后重启时,会通过事务日志(如 InnoDB 的 redo log 和 undo log) 检查未完成的事务:对已提交的事务,确保修改生效;对未提交的事务,通过 undo 日志回滚,避免残留中间状态。
UNDO的实现
trx_id是事务的唯一id,roll_poiner是回滚指针,指向前一条数据,如果出现了问题就会通过这个回滚指针回滚到上一个版本,每个事务都是单独的,第一条事务的id就是他自己。
注意(begin/start transaction 命令并不是一个事务的起点,在执行到它们之后第一条执行的修改操作的InnoDB语句事务才真正的启动,才会像数据库申请事务id(trx_id),mysql内部是严格按照事务的启动顺序来分配id。)
每一次的修改就是把上一个的事务的事务id写到roll_poiner中并且在新增一个新的trx_id,不管事务有没有执行commit他都会存储在这个版本链里面
这种机制既保证了事务的原子性(要么全做,要么全不做),又为多版本并发控制提供了基础。
Read View(一致性视图)
Read View 是 MVCC 中判断数据版本可见性的依据。
他有4 个参数:
m_ids:当前未提交的活跃事务 ID 数组
min_trx_id:活跃事务中最小的 ID
max_trx_id:下一个要分配的事务 ID(未开始的事务)这是一个全局递增的计数器,用于为新事务分配唯一 ID。
creator_trx_id:当前事务自己的 ID
组成规则:
这个视图包含查询时所有未提交事务的 ID 数组,以及该数组中的最小事务 ID、已创建的最大事务 ID,同时还包括生成此视图的当前事务自身 ID,共同作为判断数据版本可见性的依据。
Read View的执行流程
图里事务 300 提交后执行查询,此时生成 Read View 时,会记录当前数据库里的活跃事务(也就是未提交的事务,比如图里的 100、200 ,因为 300 自己提交了,不算活跃了),用来决定查询能看到哪些版本的数据。
注意:
只要事务第一次需要读数据 ,InnoDB 就会给这个事务生成一个 Read View 。一旦生成,在可重复读(Repeatable Read)隔离级别 下,这个 Read View 会跟着事务走到底。
Read View的可见性规则
在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链(undo回滚日志)里的最新数据开始逐条跟read-view做比对从而得到最终的视图结果。
在读已提交的隔离级别中, Read View的读取规则是有变化的,在可重复读中只有第一次查询的 Read View视图会从表里面拿最新的数据而在读已提交的隔离级别中每一次查询都会去数据库中拿一份新的来生成一个 Read View最新的值是什么 Read View里面就是什么,底层和可重复读也是通过版本链来比对。
数据库的一致性
一致性是 ACID 的最终目标,其他三个特性(原子性、隔离性、持久性)以及业务代码正确逻辑来实现的。
原子性确保事务不残留中间状态;
隔离性避免并发事务相互干扰(如脏读导致的数据逻辑错误);
持久性确保已提交的合法状态不丢失。
持久性
一旦提交了事务,它对数据库的改变是永久性的,持久性由redo log日志来实现。
REDO LOG 称为重做日志,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性
这篇文章的介绍比较详细。
【MySql】数据库Redo日志介绍
事务的优化
事务优化原则
1.将查询等数据准备操作放到事务外
2.事务中避免远程调用,若必须进行远程调用则要设置超时,防止事务等待时间过久
3.事务中避免一次性处理太多数据,可以拆分成多个事务分次处理
4.更新等涉及加锁的操作尽可能放在事务靠后的位置
5.能异步处理的尽量异步处理
6.应用侧(业务代码)保证数据一致性,采用非事务方式执行(当业务逻辑相对简单时,可以不依赖数据库事务,而是通过 Java 等业务代码手动实现数据一致性保障)
最后避免大事务