一、基础:数据库事务机制
事务是数据库中一组不可分割的操作单元,这组操作要么全部执行成功(提交),要么全部执行失败(回滚),核心目标是保证数据在多操作、多并发场景下的一致性。
1. 事务的核心特性:ACID
ACID 是事务的四大基石,缺一不可,具体定义如下:
| 特性 | 核心含义 | 示例(转账场景:A 转 100 给 B) |
|---|---|---|
| 原子性(Atomicity) | 事务是 “最小执行单元”,不可拆分;要么全成,要么全败 | 若 A 扣钱后 B 未到账,事务回滚,A 的钱恢复,B 的钱不变 |
| 一致性(Consistency) | 事务执行前后,数据的 “业务规则” 保持一致(如总金额不变) | 转账前 A+B=1000,转账后 A+B 仍 = 1000 |
| 隔离性(Isolation) | 多事务并发执行时,一个事务的操作不会干扰另一个事务 | 事务 1 查询 A 的余额时,事务 2 对 A 的修改不会被事务 1 “中途看到” |
| 持久性(Durability) | 事务提交后,修改会永久保存到磁盘,即使数据库崩溃也不丢失 | 事务提交后,A 的余额 - 100、B 的余额 + 100 会写入磁盘,重启数据库后数据仍有效 |
2. 事务的生命周期
事务从开始到结束分为3个关键阶段,需要通过SQL显式或者隐式控制:
1. 事务开始(start)
- 显式开始:通过BEGIN或者START TRANSACTION触发;
- 隐式开始:MySQL默认设置自动提交事务(autocommit=ON),每条SQL语句单独构成一个事务(语句执行后自动提交)。
2. 事务执行(execute)
执行SQL操作(如INSERT、UPDATE、DELETE等),此时修改仅在内存缓冲区生效,未写入磁盘。
3. 事务结束(end)
- 成功:执行提交(commit),将缓存区数据持久化到磁盘,事务结束;
- 失败:执行回滚(rollback),撤销缓存区数据修改,事务回滚到开始前状态。
3. 事务的类型
- 显式事务:手动控制事务的开始,提交/回滚,需设置autocommit=off,或者用BEGIN开始;
- 隐式事务:自动提交模式下,每条SQL语句自动构成一个事务(如UPDATE user SET balance = 100 WHERE id = 1 执行后自动立即提交)。
二、问题:事务并发带来的风险
当多个事务同时操作同一批数据时(如多用户同时更新同一条订单),若缺少隔离机制,就会引发 3 类典型问题,脏读,不可重复读,幻读
1. 脏读(Dirty Read)
一个事务B读取了另一个事务A未提交的数据(脏数据),若后续事务A回滚数据,事务B读取的数据就是无效的。例如,
- 事务A(转账):将用户1的余额从1000改为1100;
- 事务B(查询):读取用户1的账户余额为1100;
- 事务A因错误执行ROLLBACK,用户1 的账号余额恢复为1000;
- 事务B持有的用户1余额1100就是个错误数据。
2. 不可重复读(Non-Repeatable Read)
同一个事务中,多次读取同一数据,返回结果不一致。数据结果收到其他提交事务的影响。
和脏读的主要区别是,脏读读取到的是其他事务未提交数据,不可重复读读取到的是其他事务已经提交的数据,同一事物中查询结果不一致。例如,
- 事务A(查询):第一次读取用户的账号余额为1000;
- 事务B(转账):修复用户余额为900,执行commit;
- 事务A(查询):第二次读取用户的余额为900,两次结构不一致。
3. 幻读(Phantom Read)
同一事务内,多次执行相同条件的范围查询,结果集的 “行数” 不一致(因中间被其他事务提交的插入 / 删除干扰)。
和不可重复读的主要区别是,不可重复读是 “单条数据的值变了”,幻读是 “结果集的行数变了”(像出现了 “幻影”)。例如,
- 事务 A(统计):查询 “余额 < 1000 的用户数”,结果为 5 人;
- 事务 B(新增用户):插入 1 个余额 900 的用户,执行 COMMIT;
- 事务 A 再次执行相同查询,结果为 6 人,统计结果不一致。
三、解决方案:MySQL 事务隔离级别
为解决事务并发问题,SQL标准定义了4种事务隔离级别,MySQL(主要是InnoDB引擎)在此基础上做了优化。隔离级别越高,一致性越强,但并发性能越弱。
隔离级别
| 隔离级别 | 解决的并发问题 | 未解决的问题 | 性能 |
|---|---|---|---|
| 读未提交(Read Uncommitted, RU) | 无 | 脏读、不可重复读、幻读 | 最高(无锁开销) |
| 读已提交(Read Committed, RC) | 脏读 | 不可重复读、幻读 | 较高 |
| 可重复读(Repeatable Read, RR) | 脏读、不可重复读 | InnoDB 中已解决幻读 | 中等(MySQL 默认级别) |
| 串行化(Serializable) | 所有并发问题 | 无 | 最低 |
- SQL 标准中,可重复读(RR)未解决幻读,但 InnoDB 通过MVCC和 “Next-Key Lock” 机制在 RR 级别下解决了幻读;
- 生产环境中,极少使用 RU(一致性太差)和 Serializable(性能太差),主流选择是RC(需频繁读最新数据,如电商订单查询)或RR(需事务内数据一致,如财务对账)。
MySQL 中隔离级别的操作
(1)查看当前隔离级别
-- MySQL 5.7及以下
show variables like '%tx_isolation%'
-- 或者
SELECT @@tx_isolation;
-- MySQL 8.0及以上(变量名变更)
show variables like '%transaction_isolation%';
-- 或者
SELECT @@transaction_isolation;
(2)设置隔离级别
-- 会话级(仅当前连接生效)
set session transaction isolation level read committed;
-- 全局级(需重启连接生效)
set global transaction isolation level repeatable read;
四、深入事务与隔离的底层支撑
MySQL要支持ACID事务,需要设置存储引擎为InnoDB,其核心技术决定了事务的实现和隔离级别的效果,关键技术包括:事务日志,锁机制,MVCC。
1. 事务日志:保证 ACID 中的原子性和持久性
InnoDB 依赖两类日志实现事务的 “提交 / 回滚” 和 “崩溃恢复”:redo log(重做日志) 和 undo log(回滚日志)。
(1)redo log(重做日志)
- 作用:保证事务的持久性,记录 “数据页的修改内容”而非 SQL 语句(如:数据页 0x123 的偏移量 456 处,将值从 10 改为 9),防止数据库崩溃后未持久化的修改丢失。
- 原理:
事务执行时,先将修改写入 redo log buffer(内存),同时修改内存中的数据页(Buffer Pool);
满足 “刷盘条件”(如事务提交、缓冲区满)时,将 redo log buffer 刷到磁盘的 redo log file(物理文件,循环写入);
数据库崩溃后重启,通过 redo log 恢复所有已提交但未刷到数据文件的修改。 - 关键特性:“WAL(Write-Ahead Logging)” 机制 —— 先写日志,再写数据,确保日志优先持久化。并将 “随机 IO 刷盘” 转化为 “顺序 IO 写日志”,提升高并发写入效率。
(2)undo log(回滚日志)
- 作用:保证事务的原子性,记录的是 “SQL 操作的反向逻辑”(如:插入一条记录→undo log 记录 “删除该记录”;更新一条记录→undo log 记录 “将字段改回旧值”),而非物理数据页修改。用于事务回滚或 MVCC 的版本读取。
- 原理:
每个事务会生成对应的 undo log 片段;
事务提交后,undo log 不会立即删除 —— 因为可能被其他事务的 MVCC 读取(需要历史版本);
后台有purge 线程,定期清理 “不再被任何事务引用” 的 undo log(如所有事务都已读取过该历史版本)。
2. 锁机制:保证隔离性,防止并发修改冲突
InnoDB 支持多种锁,核心是通过锁的 “互斥性” 阻止并发事务的冲突操作。
(1)按锁粒度划分(影响并发性能的关键)
锁粒度指锁定数据的范围,粒度越小,并发度越高(锁冲突越少),但锁开销越大(维护锁的成本高);反之则并发度低、开销小。MySQL 支持 3 种核心锁粒度:
| 锁粒度 | 锁定范围 | 支持引擎 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| 表锁(Table Lock) | 整个表 | MyISAM、InnoDB(默认支持) | 开销极小(仅需锁定表结构)、无死锁风险 | 并发度极低(写操作阻塞全表读 / 写) | MyISAM 引擎(非事务)、InnoDB |
| 行锁(Row Lock) | 单条数据行 | 仅 InnoDB | 并发度极高(仅锁定目标行,不影响其他行) | 开销较大(需维护每行锁状态)、可能产生死锁 | 高并发读写场景(如电商订单、库存)、事务型业务 |
| 页锁(Page Lock) | 数据页(InnoDB 中默认 16KB / 页) | 仅 InnoDB(极少用) | 平衡表锁和行锁(粒度介于二者之间) | 锁冲突概率高于行锁、开销高于表锁 | 极少场景(如大批量顺序读写,且不适合行锁 / 表锁) |
InnoDB 是唯一支持行锁的主流引擎,也是事务型业务的首选
(2)按锁模式划分(控制访问权限)
按事务对数据的访问目的,锁分为共享锁和排他锁,核心遵循兼容则并发,冲突则排队的规则:
- 共享锁(Shared Lock,简称 S 锁)
也称读锁,事务获取 S 锁后,仅能读取数据,不能修改。
兼容性:多个事务可同时获取同一数据的 S 锁(读 - 读不冲突);但 S 锁与排他锁(X 锁)冲突(读 - 写冲突)。
释放时机:事务提交(COMMIT)或回滚(ROLLBACK)后自动释放。
手动获取S锁
SELECT * FROM product WHERE id=1 LOCK IN SHARE MODE; -- 加 S 锁
- 排他锁(Exclusive Lock,简称 X 锁)
也称写锁,事务获取 X 锁后,既能读取数据,也能修改数据。
兼容性:与任何锁(S 锁 / X 锁)都冲突(写 - 读、写 - 写均冲突)—— 同一时间,只有一个事务能获取同一数据的 X 锁。
释放时机:事务提交(COMMIT)或回滚(ROLLBACK)后自动释放。
自动获取X锁:执行 UPDATE/DELETE/INSERT 时,InnoDB 会自动为目标行加 X 锁;
手动获取X锁
SELECT * FROM product WHERE id=1 FOR UPDATE; -- 加 X 锁
- 意向锁(Intention Lock,辅助锁)
InnoDB 为解决表锁与行锁的冲突检测效率问题,引入意向锁(表级锁),分为:
意向共享锁(IS 锁):事务准备给某行加 S 锁前,先给表加 IS 锁;
意向排他锁(IX 锁):事务准备给某行加 X 锁前,先给表加 IX 锁。
核心作用:避免表锁检查所有行的锁状态(如加表 X 锁时,只需检查表是否有 IS/IX 锁,无需遍历每行),提升锁冲突检测效率。
意向锁兼容性规则:
IS 锁与 S/IX 锁兼容,仅与表级 X 锁冲突;
IX 锁与所有表级锁( 表 S / 表 X)都冲突。
(3)按锁算法划分(InnoDB 行锁的实现细节)
InnoDB 的行锁并非单纯锁定 “行”,而是基于索引实现,不同索引场景对应不同锁算法,直接影响锁的范围:
- 记录锁(Record Lock):锁定单条记录
定义:仅锁定索引对应的单条数据行,是最精准的行锁。
触发条件:基于唯一索引(主键、唯一键)的等值查询(如 WHERE id=1,id 是主键)。
示例:
-- 事务 A 加 X 锁(唯一索引等值查询)
BEGIN;
SELECT * FROM product WHERE id=1 FOR UPDATE; -- 仅锁定 id=1 的行
-- 事务 B 可修改 id=2 的行,不阻塞;修改 id=1 则阻塞
- 间隙锁(Gap Lock):锁定索引区间(防止幻读)
定义:锁定索引值之间的间隙(不包含索引本身),仅在 InnoDB 的 Repeatable Read(RR)隔离级别下生效(默认隔离级别)。
核心目的:防止幻读(同一事务两次查询,结果集行数不一致)。
触发条件:基于非唯一索引的范围查询(如 WHERE price BETWEEN 100 AND 200)或唯一索引的范围查询(不包含等值)。
示例(表 product 有非唯一索引 price,数据:price=99, 150, 200):
-- 事务 A 加间隙锁(范围查询)
BEGIN;
SELECT * FROM product WHERE price BETWEEN 100 AND 200 FOR UPDATE;
-- 锁定的间隙:(99,150)、(150,200)、(200, +∞)
-- 事务 B 无法插入 price=120、180、250 的记录(会阻塞),但可修改 price=99、150 的记录
- 临键锁(Next-Key Lock):记录锁 + 间隙锁(默认行锁算法)
定义:InnoDB RR 隔离级别下的 默认行锁算法,组合记录锁(锁定当前行)+ 间隙锁(锁定当前行与下一行的间隙),形成 “左闭右开” 的区间锁。
唯一索引的等值查询会 “退化” 为记录锁(无间隙锁),仅范围查询保留临键锁;
非唯一索引的任何查询(等值 / 范围)都默认用临键锁;
关闭间隙锁:将隔离级别改为 READ COMMITTED(RC)
示例(同上表,price 非唯一索引):
-- 事务 A 加临键锁(等值查询非唯一索引)
BEGIN;
SELECT * FROM product WHERE price=150 FOR UPDATE;
-- 锁定区间:(99, 150](记录锁锁定 150,间隙锁锁定 (99,150))
-- 事务 B 无法插入 price=120(间隙内)、150(记录锁)的记录,可插入 price=90(间隙外)
(4)按锁的乐观 / 悲观策略划分
按事务对冲突的 “预期”,锁分为乐观锁和悲观锁,二者并非 MySQL 内置的 “锁类型”,而是并发控制的策略:
-
悲观锁(Pessimistic Lock)
核心思想:默认认为并发冲突一定会发生,事务操作前先获取锁,阻塞其他事务直到自己释放锁。
实现方式:
表锁:MyISAM 自动加表锁,InnoDB 手动加表锁(LOCK TABLES …);
行锁:InnoDB 的 SELECT … FOR UPDATE(X 锁)、LOCK IN SHARE MODE(S 锁)。
优点:一致性强,无需处理冲突重试;
缺点:并发度低,锁等待会导致吞吐量下降;
适用场景:写操作频繁(如库存扣减、订单状态修改)、数据一致性要求高。 -
乐观锁(Optimistic Lock)
核心思想:默认认为并发冲突很少发生,事务操作时不加锁,仅在提交时检查数据是否被修改(通过版本号 / 时间戳),若修改则回滚重试。
实现方式(MySQL 无内置乐观锁,需业务实现):
版本号机制:表加 version 字段,更新时判断版本号是否一致(如 UPDATE … WHERE id=1 AND version=3);
时间戳机制:表加 update_time 字段,更新时判断时间戳是否匹配。
优点:并发度高,无锁等待开销;
缺点:需业务处理重试逻辑,冲突频繁时会导致多次重试(性能下降);
适用场景:读操作频繁、冲突少(如商品详情查询、用户信息查看)。
示例:
-- 1. 读取数据时获取版本号
SELECT stock, version FROM product WHERE id=1; -- stock=10, version=3
-- 2. 提交时检查版本号,仅当版本号未变时更新
UPDATE product SET stock=9, version=version+1 WHERE id=1 AND version=3;
-- 3. 若影响行数为 0,说明数据已被修改,事务重试
五、落地:InnoDB 如何解决事务并发问题
(1)MVCC:多版本并发控制(高并发读的核心)
MVCC 是 InnoDB 实现 “读写不阻塞、读不加锁” 的核心机制,通过 “保存数据的多个历史版本”,让读事务读取历史版本,写事务修改最新版本,从而提升并发性能。
- 核心组成:
隐藏列:每行数据除了用户定义的列,还有 3 个隐藏列:
DB_TRX_ID:最后修改该数据的事务 ID;
DB_ROLL_PTR:指向该数据的 undo log 记录(用于构建版本链);
DB_ROW_ID:默认主键(无主键时自动生成)。 - undo log 版本链:每次修改数据时,InnoDB 会生成一条 undo log,通过 DB_ROLL_PTR 将所有版本串联成 “版本链”(最新版本在链头,历史版本在链尾)。
- Read View(读视图):事务读取数据时生成的 “版本过滤规则”,决定当前事务能看到哪些版本的数据:
m_low_limit_id:当前所有活跃事务的最小 ID(小于该 ID 的事务已提交,其版本可见);
m_up_limit_id:当前事务的 ID(大于等于该 ID 的事务版本不可见);
m_ids:当前所有活跃事务的 ID 列表(在列表中的事务版本不可见)。
creator_trx_id:生成该 Read View 的事务自身 ID。 - 判断某行数据的版本(trx_id 为 row_trx_id)是否可见的规则:
如果 row_trx_id == creator_trx_id:当前事务修改的行,可见;
如果 row_trx_id < min_trx_id:修改该行的事务已提交,可见;
如果 row_trx_id >= max_trx_id:修改该行的事务是后续启动的,不可见;
如果 min_trx_id <= row_trx_id < max_trx_id:
若 row_trx_id 在 m_ids 中(事务还活跃):不可见;
若不在 m_ids 中(事务已提交):可见。
如果版本不可见,就通过 DB_ROLL_PTR 去 undo 日志中找上一个版本,重复判断,直到找到可见版本或版本链结束(返回空)。
(2)解决脏读
技术依赖:MVCC 的 Read View + undo log 版本链。
逻辑:
读事务生成 Read View 时,会过滤掉 “未提交事务的版本”(即 DB_TRX_ID 在活跃事务列表 m_ids 中的版本),仅读取 “已提交事务的版本”(DB_TRX_ID < m_low_limit_id),从而避免读取未提交的脏数据。
(3)解决不可重复读
技术依赖:MVCC 的 “同一事务共享 Read View”。
逻辑:
在 RR 级别下,同一事务的所有读操作共用一个 Read View,即使其他事务提交了新的版本,当前事务仍读取第一次生成 Read View 时的可见版本,保证多次读取结果一致;
在 RC 级别下,每次读操作都会重新生成 Read View,因此会看到其他事务提交的新版本,无法解决不可重复读(这也是 RC 和 RR 的核心区别)。
(4)解决幻读
技术依赖:RR 级别下的 Next-Key Lock + MVCC。
逻辑:
写事务:执行范围修改(如 UPDATE … WHERE balance < 1000)时,InnoDB 会加 Next-Key Lock,锁定 “满足条件的行” 和 “行之间的间隙”,阻止其他事务插入数据,从根源上防止幻读;
读事务:通过 MVCC 读取历史版本,即使其他事务插入了新数据,当前事务仍读取 Read View 对应的历史版本,看不到新插入的 “幻影数据”。
((5)未解决的特殊幻读场景
极端场景下,同一事务内进行快照读 → 当前读 → 快照读导致的幻读,RR 的 MVCC 仅保证事务启动后,快照读的一致性,但当前读会打破这个一致性,导致后续快照读 “看到” 新行。解决思路,对需要避免幻读的范围查询,全程使用当前读。不过需要谨慎使用,防止死锁。
总结:知识体系逻辑链
基础认知:事务是不可分割的操作单元,核心是 ACID 特性,需通过开始 / 提交 / 回滚控制生命周期;
问题提出:多事务并发会引发脏读、不可重复读、幻读 ,需隔离机制解决;
方案选型:4 种隔离级别对应不同的一致性 / 性能权衡,MySQL 默认 RR 级别(InnoDB 优化后解决幻读);
底层支撑:InnoDB 通过 redo log(持久化)、undo log(原子性)、锁机制(隔离性)、MVCC(高并发读)实现事务与隔离;
落地解决:结合 MVCC 和锁机制,针对性解决并发问题,平衡一致性与性能。
1310

被折叠的 条评论
为什么被折叠?



