MySQL的事务和隔离机制

一、基础:数据库事务机制

事务是数据库中一组不可分割的操作单元,这组操作要么全部执行成功(提交),要么全部执行失败(回滚),核心目标是保证数据在多操作、多并发场景下的一致性。

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读取的数据就是无效的。例如,

  1. 事务A(转账):将用户1的余额从1000改为1100;
  2. 事务B(查询):读取用户1的账户余额为1100;
  3. 事务A因错误执行ROLLBACK,用户1 的账号余额恢复为1000;
  4. 事务B持有的用户1余额1100就是个错误数据。

2. 不可重复读(Non-Repeatable Read)

同一个事务中,多次读取同一数据,返回结果不一致。数据结果收到其他提交事务的影响。
和脏读的主要区别是,脏读读取到的是其他事务未提交数据,不可重复读读取到的是其他事务已经提交的数据,同一事物中查询结果不一致。例如,

  1. 事务A(查询):第一次读取用户的账号余额为1000;
  2. 事务B(转账):修复用户余额为900,执行commit;
  3. 事务A(查询):第二次读取用户的余额为900,两次结构不一致。

3. 幻读(Phantom Read)

同一事务内,多次执行相同条件的范围查询,结果集的 “行数” 不一致(因中间被其他事务提交的插入 / 删除干扰)。
和不可重复读的主要区别是,不可重复读是 “单条数据的值变了”,幻读是 “结果集的行数变了”(像出现了 “幻影”)。例如,

  1. 事务 A(统计):查询 “余额 < 1000 的用户数”,结果为 5 人;
  2. 事务 B(新增用户):插入 1 个余额 900 的用户,执行 COMMIT;
  3. 事务 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)按锁模式划分(控制访问权限)
按事务对数据的访问目的,锁分为共享锁排他锁,核心遵循兼容则并发,冲突则排队的规则:

  1. 共享锁(Shared Lock,简称 S 锁)
    也称读锁,事务获取 S 锁后,仅能读取数据,不能修改。
    兼容性:多个事务可同时获取同一数据的 S 锁(读 - 读不冲突);但 S 锁与排他锁(X 锁)冲突(读 - 写冲突)。
    释放时机:事务提交(COMMIT)或回滚(ROLLBACK)后自动释放。
    手动获取S锁
SELECT * FROM product WHERE id=1 LOCK IN SHARE MODE; -- 加 S 锁
  1. 排他锁(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 锁
  1. 意向锁(Intention Lock,辅助锁)
    InnoDB 为解决表锁与行锁的冲突检测效率问题,引入意向锁(表级锁),分为:
    意向共享锁(IS 锁):事务准备给某行加 S 锁前,先给表加 IS 锁;
    意向排他锁(IX 锁):事务准备给某行加 X 锁前,先给表加 IX 锁。
    核心作用:避免表锁检查所有行的锁状态(如加表 X 锁时,只需检查表是否有 IS/IX 锁,无需遍历每行),提升锁冲突检测效率。
    意向锁兼容性规则:
    IS 锁与 S/IX 锁兼容,仅与表级 X 锁冲突;
    IX 锁与所有表级锁( 表 S / 表 X)都冲突。

(3)按锁算法划分(InnoDB 行锁的实现细节)
InnoDB 的行锁并非单纯锁定 “行”,而是基于索引实现,不同索引场景对应不同锁算法,直接影响锁的范围:

  1. 记录锁(Record Lock):锁定单条记录
    定义:仅锁定索引对应的单条数据行,是最精准的行锁。
    触发条件:基于唯一索引(主键、唯一键)的等值查询(如 WHERE id=1,id 是主键)。
    示例:
-- 事务 A 加 X 锁(唯一索引等值查询)
BEGIN;
SELECT * FROM product WHERE id=1 FOR UPDATE; -- 仅锁定 id=1 的行
-- 事务 B 可修改 id=2 的行,不阻塞;修改 id=1 则阻塞
  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 的记录
  1. 临键锁(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 内置的 “锁类型”,而是并发控制的策略:

  1. 悲观锁(Pessimistic Lock)
    核心思想:默认认为并发冲突一定会发生,事务操作前先获取锁,阻塞其他事务直到自己释放锁。
    实现方式:
    表锁:MyISAM 自动加表锁,InnoDB 手动加表锁(LOCK TABLES …);
    行锁:InnoDB 的 SELECT … FOR UPDATE(X 锁)、LOCK IN SHARE MODE(S 锁)。
    优点:一致性强,无需处理冲突重试;
    缺点:并发度低,锁等待会导致吞吐量下降;
    适用场景:写操作频繁(如库存扣减、订单状态修改)、数据一致性要求高。

  2. 乐观锁(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 和锁机制,针对性解决并发问题,平衡一致性与性能。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值