MySQL的事务机制与并发读异常示例详解
目录
- 一、事务
- 1.1 概念
- 1.2 事务控制语句
- 1.3 ACID特性
- ① 原子性(A)
- ② 一致性(C)
- ③ 隔离性(I)
- ④ 持久性(D)
- 1.4 隔离级别
- 1.5 命令
- 二、并发读异常
- 2.1 脏读
- 2.2 不可重复读
- 2.3 幻读
- 2.4 丢失更新
- 2.5 隔离级别下并发读异常
- 2.6 区别
- 三、Redo Log(重做日志)
- 四、Undo Log(回滚日志)
- 总结
一、事务
事务是用户定义的一系列操作,这些操作需要让mysql视为一个整体执行,这些操作要么都做,要么都不做,是一个不可分割的工作单位。
1.1 概念
前提:有并发连接访问,多条连接,并发执行 sql 语句
事务是用户定义的不可分割的操作序列,是并发控制的最小单元,这些操作要么全部执行(提交),要么全部不执行(回滚)。
目的:事务将数据库从编程客栈一种一致性状态转换为另一种一致性状态;保证系统的数据完整性与可靠性。
举个例子:在银行转账中,“A 扣款 + B 收款”就是一个典型事务,要么两个操作都成功,要么都失败,不能只执行一半。
组成:
- 简单事务:可由单条 SQL 语句组成(如
UPDATE students SET age=26 WHERE id=15); - 复杂事务:可由一组关联的 SQL 语句组成(如转账场景中的 “扣款 + 收款” 两条更新语句)。
特征:
- 在数据库提交事务时,可以确保要么所有修改都已经保存,要么所有修改都不保存;
- 事务是访问并更新数据库各种数据项的一个程序执行单元。
- 在 MySQL innodb 下,单条语句都具备事务;可以通过
set autocommit = 0;设置当前会话手动提交;
1.2 事务控制语句
begin/start transactioncommitrollback
-- 显示开启事务 START TRANSACTION | BEGIN -- 提交事务,并使得已对数据库做的所有修改持久化 COMMIT -- 回滚事务,结束用户的事务,并撤销正在进行的所有未提交的修改 ROLLBACK -- 创建一个保存点,一个事务可以有多个保存点 SAVEPOINT identifier -- 删除一个保存点 RELEASE SAVEPOINT identifier -- 事务回滚到保存点 ROLLBACK TO [SAVEPOINT] identifier
1.3 ACID特性
① 原子性(A)
事务是最小执行单元,事务操作要么都做(提交),要么都不做(回滚);MySQL 通过 undo log 实现回滚,它记录了每个操作的反向操作,从而在异常时恢复数据。
② 一致性(C)
事务执行前后,数据库必须处于一致状态。
- 数据库完整性约束(唯一、非空等)
- 逻辑上的一致性:用户定义的一系列操作,需要数据库视为一个整体,这是用户定义的
事务执行前后,数据库的 “完整性约束”(如唯一键、非空约束)与 “逻辑一致性”(如转账总金额不变)不能被破坏。一致性由原子性、隔离性、持久性共同保障。
③ 隔离性(I)
描述:控制 “多个并发事务的相互影响程度”;各自运行的环境隔离。
多个事务并发执行时,应互不干扰。
一个事务内部的操作和数据,在其提交前对其他事务是不可见的。
InnoDB 用 MVCC(多版本并发控制) 处理 “读 - 写并发”(读操作通过版本链读历史数据,无需加锁);用锁(表锁、页锁、行锁等) 处理 “写 - 写并发”(写操作加锁避免冲突)。
MVCC 是多版本并发控制,主要解决一致性非锁定读,通过记录和获取行版本,而不是使用锁来限制读操作,从而实现高效并发读性能。锁用来处理并发 DML 操作;数据库中提供粒度锁的策略,针对表(聚集索引 B+ 树)、页(聚集索引 B+ 树叶子节点)、行(叶子节点当中某一段记录行)三种粒度加锁;
不同隔离级别会导致不同并发现象(脏读、不可重复读、幻读)
④ 持久性(D)
事务一旦提交,数据的修改就会永久保存。
无论系统宕机、断电还是重启,数据都能通过 redo log(重做日志) 恢复。
redo log 记录的是物理层面的“页修改”信息,比如修改了哪个数据页、偏移量、内容等,保证事务的最终落盘。
1.4 隔离级别
ISO 和 ANIS SQL 标准制定了四种事务隔离级别的标准,各数据库厂商在正确性和性能之间做了妥协,并没有严格遵循这些标准;MySQL innodb默认支持的隔离级别是 REPEATABLE READ;
数据库标准(ISO/ANSI SQL)定义了四种隔离级别,从低到高依次是:
| 隔离级别 | 说明 | 读操作 | 写操作 | 并发现象 | 性能特点 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | 读未提交,读不加锁,性能最高但风险最大 | 不加锁,直接读最新数据 | 自动加 X 锁 | 可能脏读、不可重复读、幻读 | 性能最高,安全性最差 |
| READ COMMITTED | 读已提交;读取最新已提交版本(支持 MVCC) | 通过 MVCC 读已提交数据 | 自动加 X 锁 | 避免脏读,仍可能不可重复读、幻读 | 性能较好 |
| REPEATABLE READ(默认) | 可重复读,事务期间读到的是事务开始时的数据快照(支持 MVCC) | 通过 MVCC 读事务开始前版本 | 自动加 X 锁 | 避免脏读、不可重复读,但仍可能幻读;MySQL 已基本避免幻读 | 性能适中 |
| SERIALIZABLE | 可串行化;所有事务顺序执行 | 加 S 锁(Next-Key Lock) | 加 X 锁 | 完全避免并发异常 | 性能最差,最安全 |
1.5 命令
-- 设置隔离级别 SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL REPEATABLE READ; -- 或者采用下面的方式设置隔离级别 SET @@tx_isolation = js'REPEATABLE READ'; SET @@global.tx_isolation = 'REPEATABLE READ'; -- 查看全局隔离级别 SELECT @@global.tx_isolation; -- 查看当前会话隔离级别 SELECT @@session.tx_isolation; SELECT @@tx_isolation; -- 手动给读加 S 锁 SELECT ... LOCK IN SHARE MODE; -- 手动给读加 X 锁 SELECT ... FOR UPDATE; -- 查看当前锁信息 SELECT * FROM information_schema.innodb_locks;
二、并发读异常
2.1 脏读
事务(A)可以读到另外一个事务(B)中未提交的数据;也就是事务A读到脏数据;在读写分离的场景下,可以将 slave 节点设置为 READ UNCOMMITTED;此时脏读不影响,在 slave 上查询并不需要特别精准的返回值。
示例
| seq | session A | session B |
|---|---|---|
| 1 | SET @@tx_isolation='READ UNCOMMITTED'; | SET @@tx_isolation='READ UNCOMMITTED'; |
| 2 | BEGIN; | |
| 3 | UPDATE account_t SET money = money - 100 WHERE name = 'A'; | |
| 4 | BEGIN; | |
| 5 | SELECT money FROM account_t WHERE name = 'A'; | |
| 6 | SELECT money FROM account_t WHERE name = 'B'; | |
| 7 | UPDATE account_t SET money = money - 100 WHERE name = 'B'; | |
| 8 | COMMIT | COMMIT |

2.2 不可重复读
事务(A) 可以读到另外一个事务(B)中提交的数据;通常发生在一个事务中两次读到的数据是不一样的情况;不可重复读在隔离级别 READ COMMITTED 存在。一般而言,不可重复读的问题是可以接受的,因为读到已经提交的数据,一般不会带来很大的问题,所以很多厂商(如oracle、SQL Server)默认隔离级别就是 READ COMMITTED;
示例
| seq | session A | session B |
|---|---|---|
| 1 | SET @@tx_isolation='READ COMMITTED'; | SET @@tx_isolation='READ COMMITTED'; |
| 2 | BEGIN; | BEGIN; |
| 3 | SELECT money FROM account_t WHERE name = 'A'; | |
| 4 | UPDATE account_t SET money = money - 100 WHERE name = 'A'; | |
| 5 | COMMIT; | SELECT money FROM account_t WHERE name = 'A'; |
| 6 | COMMIT; |

2.3 幻读
两次读取同一个范围内的记录得到的结果集不一样;快照读和当前读不一致;
例如:以 name 为唯一键的表,一个事务中查询 select * from t where name = 'gulu'; 不存在,接下来 insert into t(name) values ('gulu'); 出现错误,因为此时另外一个事务也执行了 insert 操作;
幻读在隔离级别 REPEATABLE READ 及以下存在;但是可以在 REPEATABLE READ 级别下通过读加锁(使用 next-key locking)解决;
示例
| seq | session A | session B |
|---|---|---|
| 1 | SET @@tx_isolation='REPEATABLE READ'; | SET @@tx_isolation='REPEATABLE READ'; |
| 2 | BEGIN; | BEGIN; |
| 3 | SELECT * FROM account_t WHERE id >= 2; | |
| 4 | INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); | |
| 5 | COMMIT; | |
| 6 | INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); | |
| 7 | COMMIT; |

解决:给读操作加锁
| seq | session A | session B |
|---|---|---|
| 1 | SET @@tx_isolation='REPEATABLE READ'; | SET @@tx_isolation='REPEATABLE READ'; |
| 2 | BEGIN; | BEGIN; |
| 3 | SELECT * FROM account_t WHERE id >= 2 lock in share mode; | |
| 4 | INSERT INTO account_t(id,name,money) VALUES (4,'D',1000); | |
| 5 | COMMIT; | |
| 6 | SELECT * FROM account_t WHERE id >= 2; | |
| 7 | COMMIT; |


2.4 丢失更新
脏读、不可重复读、幻读都是一个事务写,一个事务读,由于一个事务的写导致另一个事务读到了不该读的数据;丢失更新是两个事务都是写;丢失更新分为提交覆盖和回滚覆盖;回滚覆盖数据库拒绝不可能产生,重点关注提交覆盖;
示例
| seq | session A | session B |
|---|---|---|
| 1 | SET @@tx_isolation='REPEATABLE READ'; | SET @@tx_isolation='REPEATABLE READ'; |
| 2 | BEGIN; | BEGIN; |
| 3 | SELECT money FROM account_t WHERE name = 'A'; | |
| 4 | SELECT money FROM account_t WHERE name = 'A'; | |
| 5 | UPDATE account_t SET money = 1000+100 WHERE name = 'A'; | |
| 6 | COMMIT; | |
| 7 | UPDATE apythonccount_t SET money = 1000-100 WHERE name = 'A'; | |
| 8 | COMMIT; |

2.5 隔离级别下并发读异常
| 隔离级别 | 回滚覆盖 | 脏读 | 不可重复读 | 幻读 | 提交覆盖 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | no | yes | yes | yes | yes |
| READ COMMITTED | no | no | yes | yes | yes |
| REPEATABLE READ | no | no | no | yes(手动加锁) | yes(手动加锁) |
| SERIALIZABLE | no | no | no | no | no |
2.6 区别
- 脏读和不可重复读的区别在于,脏读是读取了另一个事务未提交的android数据,而不可重复读是读取了另一个事务提交之后的修改;本质上都是其他事务的修改影响了本事务的读取;
- 不可重复读和幻读比较类似;不可重复读是两次读取同一条记录,得到不一样的结果;而幻读是两次读取同一个范围内的记录得到的结果集不一样(可能不同个数,也可能相同个数内容不一样,比如x一行后又添加新行);不可重复读是因为其他事务进行了 update 操作,幻读是因为其他事务进行了 insert 或者 delete 操作。
三、Redo Log(重做日志)
在 MySQL InnoDB 存储引擎中,Redo Log 是实现事务持久性的核心机制。
简单来说,它用来保证事务提交后,即使系统宕机,也能通过日志恢复到正确状态。在事务执行过程中,数据的修改首先发生在内存中的 Buffer Pool,但内存数据存在易失性,一旦断电或崩溃就会丢失。为了防止这种情况,InnoDB 设计了 Redo Log 来记录数据的物理修改。
Redo Log 由两部分组成:
- redo log buffer(内存中):暂存事务执行时产生的日志;
- redo log file(磁盘文件):持久化存储日志记录。
在事务提交(COMMIT)的过程中,必须先将 redo log buffer 写入磁盘文件(刷盘),确认日志已经持久化后,事务js才算真正提交成功。这就是事务的 WAL(Write-Ahead Logging,预写日志)机制 —— 先写日志,后写数据。
Redo Log 的特性与作用
- 记录内容: 页号、页偏移量、修改内容(物理变化);
- 写入方式: 顺序写入,性能高;
- 使用场景: 正常运行时只写不读,只有在 宕机恢复 时才会被读取,用于将数据恢复到最近一次提交的状态。
- 作用总结: 确保了数据的持久性(D)
四、Undo Log(回滚日志)
与 Redo Log 相对,Undo Log 用来实现事务的原子性(Atomicity)和支持 MVCC(多版本并发控制)。
如果 Redo Log 是“向前重做”,Undo Log 就是“向后回滚”。当事务执行时,InnoDB 会为每一次数据修改生成相应的 Undo 记录,用来保存修改前的数据版本。这些 Undo 日志存储在 共享表空间(Undo Tablespace) 中。
在事务回滚(ROLLBACK)时,系统会根据 Undo Log 执行反向操作,把数据逻辑地恢复到修改前的状态:
- 若事务执行了
INSERT,回滚时会执行DELETE; - 若事务执行了
DELETE,回滚时会执行INSERT; - 若事务执行了
UPDATE,回滚时会执行相反方向的UPDATE。
Undo Log 的特性与作用
保证事务原子性:
如果事务执行中途失败,可以根据 Undo Log 将已执行的部分操作撤销,确保“要么全部成功,要么全部失败”。支撑 MVCC(多版本并发控制):
Undo Log 记录了每行数据的历史版本,通过它 MySQL 可以在不同事务中读取到不同时刻的数据快照,从而实现非锁定读。记录形式: Undo Log 是逻辑日志(记录操作逻辑),不同于 Redo Log 的物理页修改。
总结
到此这篇关于MySQL事务机制与并发读异常的文章就介绍到这了,更多相关MySQL事务与并发读异常内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!
加载中,请稍侯......
精彩评论