MySQL事务ACID与隔离级别

MySQL事务ACID与隔离级别

Spring 事务用起来简单,但失效场景非常多。很多人遇到过 @Transactional 不生效的情况,却不知道原因。本文把日常开发中常见的坑和对应的排查思路整理出来,帮你避免踩坑。

一、事务基础

#

1.1 什么是事务

事务是一组逻辑操作的集合,要么全部成功,要么全部失败。

START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT;

#

1.2 事务控制语句

-- 开启事务
START TRANSACTION;
-- 或
BEGIN;

-- 提交事务
COMMIT;

-- 回滚事务
ROLLBACK;

-- 设置保存点
SAVEPOINT sp1;

-- 回滚到保存点
ROLLBACK TO SAVEPOINT sp1;

#

1.3 自动提交

-- 查看自动提交状态
SELECT @@autocommit;

-- 关闭自动提交
SET autocommit = 0;

-- 开启自动提交(默认)
SET autocommit = 1;

二、ACID特性

#

2.1 原子性(Atomicity)

定义:事务中的所有操作要么全部完成,要么全部不完成。

实现原理:Undo Log(回滚日志)

事务执行过程:
1. 记录undo log
2. 修改内存数据页
3. 写redo log(Prepare阶段)
4. 写binlog
5. redo log commit

如果需要回滚:
- 根据undo log将数据恢复到事务前状态

#

2.2 一致性(Consistency)

定义:事务执行前后,数据库从一个一致状态转换到另一个一致状态。

含义

  • 数据库完整性约束不被破坏
  • 外键约束、触发器等正常执行
  • 业务规则保证(如转账总额不变)

实现:原子性、隔离性、持久性共同保证一致性。

#

2.3 隔离性(Isolation)

定义:多个事务并发执行时,一个事务的执行不应影响其他事务。

实现原理

  • 锁机制(Locking)
  • 多版本并发控制(MVCC)

#

2.4 持久性(Durability)

定义:事务一旦提交,对数据库的改变就是永久的。

实现原理

  • Redo Log(重做日志):保证已提交事务不丢失
  • Force Log at Commit:事务提交时强制刷盘
事务提交时的写盘流程:
1. 修改Buffer Pool中的数据页(内存)
2. 写Redo Log Buffer(内存)
3. 刷Redo Log到磁盘(顺序写,速度快)
4. 事务标记为已提交
5. 后台线程异步将脏页刷到数据文件

三、并发问题

#

3.1 脏读(Dirty Read)

现象:读取到其他事务未提交的数据。

时间线:
T1: UPDATE account SET balance = 900 WHERE id = 1; -- 未提交
T2: SELECT balance FROM account WHERE id = 1; -- 读到900(脏读)
T1: ROLLBACK; -- 回滚,balance实际还是1000

解决:禁止读取未提交数据(Read Committed及以上)。

#

3.2 不可重复读(Non-repeatable Read)

现象:同一事务内两次读取同一数据,结果不同。

时间线:
T1: SELECT balance FROM account WHERE id = 1; -- 读到1000
T2: UPDATE account SET balance = 900 WHERE id = 1; COMMIT;
T1: SELECT balance FROM account WHERE id = 1; -- 读到900(不可重复读)

解决:Repeatable Read及以上隔离级别。

#

3.3 幻读(Phantom Read)

现象:同一事务内两次查询,第二次查到了第一次没有的行。

时间线:
T1: SELECT * FROM account WHERE balance > 500; -- 查到3条
T2: INSERT INTO account VALUES (4, 600); COMMIT;
T1: SELECT * FROM account WHERE balance > 500; -- 查到4条(幻读)

解决:Serializable隔离级别,或InnoDB的间隙锁(Gap Lock)。

#

3.4 丢失更新(Lost Update)

现象:两个事务同时修改同一数据,后提交的事务覆盖了先提交的修改。

时间线:
T1: SELECT balance FROM account WHERE id = 1; -- 读到1000
T2: SELECT balance FROM account WHERE id = 1; -- 读到1000
T1: UPDATE account SET balance = 1100 WHERE id = 1; COMMIT;
T2: UPDATE account SET balance = 900 WHERE id = 1; COMMIT;
-- 最终结果900,T1的更新丢失了

解决:使用乐观锁(版本号)或悲观锁(SELECT FOR UPDATE)。

四、隔离级别

#

4.1 四种隔离级别

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 不可能 可能 可能
REPEATABLE READ 不可能 不可能 可能(InnoDB解决)
SERIALIZABLE 不可能 不可能 不可能

#

4.2 READ UNCOMMITTED

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
  • 可以读取未提交的数据
  • 性能最好,但数据一致性最差
  • 生产环境不建议使用

#

4.3 READ COMMITTED

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
  • 只能读取已提交的数据(解决脏读)
  • 每次SELECT生成新的ReadView
  • Oracle、SQL Server默认级别
  • 可能出现不可重复读和幻读

#

4.4 REPEATABLE READ(MySQL默认)

SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
  • 同一事务内多次读取结果一致(解决不可重复读)
  • 事务开始时生成ReadView,整个事务复用
  • InnoDB通过MVCC + 间隙锁,一定程度上解决幻读

#

4.5 SERIALIZABLE

SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
  • 所有操作串行执行
  • 通过加锁实现,性能最差
  • 完全解决所有并发问题

#

4.6 查看和设置隔离级别

-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 或
SHOW VARIABLES LIKE 'transaction_isolation';

-- 设置当前会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;

五、InnoDB的MVCC实现

#

5.1 隐藏字段

InnoDB每行记录都有两个隐藏字段:

  • DB_TRX_ID:最后修改该记录的事务ID
  • DB_ROLL_PTR:回滚指针,指向undo log
+----------+--------+---------+------------+-------------+
| id | name | balance | DB_TRX_ID | DB_ROLL_PTR |
+----------+--------+---------+------------+-------------+
| 1 | Alice| 1000 | 100 | 0x7f... |
+----------+--------+---------+------------+-------------+

#

5.2 Undo Log链

每次修改记录,都会生成undo log,形成版本链:

当前记录(最新版本)
│ DB_ROLL_PTR

Undo Log(上一个版本)
│ DB_ROLL_PTR

Undo Log(更早版本)

NULL

#

5.3 ReadView机制

ReadView是事务进行快照读时生成的读视图,决定能看到哪个版本的数据。

ReadView包含

  • creator_trx_id:创建该ReadView的事务ID
  • m_ids:生成ReadView时,活跃的事务ID列表
  • min_trx_id:m_ids中的最小值
  • max_trx_id:下一个要分配的事务ID

可见性判断规则

对于某条记录的DB_TRX_ID:

  1. 如果DB_TRX_ID == creator_trx_id:自己修改的,可见
  2. 如果DB_TRX_ID < min_trx_id:在ReadView创建前已提交,可见
  3. 如果DB_TRX_ID >= max_trx_id:在ReadView创建后启动,不可见
  4. 如果min_trx_id <= DB_TRX_ID < max_trx_id:
    • 在m_ids列表中:事务活跃,不可见
    • 不在m_ids列表中:已提交,可见

不可见时:通过DB_ROLL_PTR找到上一个版本,继续判断。

#

5.4 不同隔离级别的ReadView差异

READ COMMITTED

  • 每次SELECT生成新的ReadView
  • 能看到其他事务已提交的最新数据

REPEATABLE READ

  • 事务第一次SELECT时生成ReadView
  • 整个事务期间复用该ReadView
  • 保证同一事务内读取一致性

六、锁机制与隔离级别

#

6.1 InnoDB锁类型

按粒度

  • 行锁(Record Lock)
  • 间隙锁(Gap Lock)
  • 临键锁(Next-Key Lock = Record Lock + Gap Lock)
  • 表锁

按功能

  • 共享锁(S Lock):读锁
  • 排他锁(X Lock):写锁

#

6.2 各隔离级别的锁行为

READ UNCOMMITTED

  • SELECT不加锁,直接读最新数据(包括未提交的)

READ COMMITTED

  • SELECT不加锁,使用MVCC读取已提交数据
  • UPDATE/DELETE只锁匹配的行(记录锁)

REPEATABLE READ

  • SELECT不加锁,使用MVCC读取快照
  • UPDATE/DELETE使用临键锁(Next-Key Lock),防止幻读
  • 范围查询时加间隙锁

SERIALIZABLE

  • 所有SELECT加共享锁
  • 完全串行化

#

6.3 幻读的解决

InnoDB在REPEATABLE READ下通过间隙锁解决幻读:

-- T1
BEGIN;
SELECT * FROM account WHERE id > 5 FOR UPDATE;
-- 锁住id>5的记录,以及id>5的间隙

-- T2
INSERT INTO account VALUES (10, ...); -- 被阻塞!间隙锁阻止插入

注意:普通SELECT(快照读)不受间隙锁保护,可能看到幻读。只有FOR UPDATE(当前读)加间隙锁才能完全避免。

七、事务优化建议

#

7.1 事务设计原则

  1. 保持事务简短:长事务占用锁时间长,影响并发
  2. 避免在事务中做RPC调用:网络延迟导致事务长时间不提交
  3. 按固定顺序访问资源:减少死锁概率
  4. 合适的隔离级别:不需要RR时,使用RC提升并发

#

7.2 大事务处理

-- 大DELETE分批处理
REPEAT
DELETE FROM logs WHERE create_time < '2023-01-01' LIMIT 1000;
SLEEP 1;
UNTIL ROW_COUNT() = 0 END REPEAT;

#

7.3 监控长事务

-- 查看正在执行的事务
SELECT
trx_id,
trx_state,
trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_seconds,
trx_mysql_thread_id,
trx_tables_locked,
trx_rows_locked
FROM information_schema.innodb_trx
ORDER BY trx_started;

-- 杀死长事务
KILL <trx_mysql_thread_id>;

八、总结

特性 实现机制 关键概念
原子性 Undo Log 回滚
一致性 ACID综合 约束、触发器
隔离性 MVCC + 锁 ReadView、间隙锁
持久性 Redo Log WAL、刷盘
隔离级别 脏读 不可重复读 幻读 实现
RU
RC MVCC
RR 部分 MVCC + 间隙锁
Serializable 串行化

理解ACID和隔离级别的实现原理,有助于:

  1. 合理选择隔离级别
  2. 设计出高效的事务逻辑
  3. 快速定位和解决并发问题

核心要点

  1. 事务失效的常见原因:非 public 方法、自调用、异常被吞掉、错误的传播级别

  2. 事务传播级别决定了方法之间的事务关系,REQUIRED 是默认值

  3. 使用 @Transactional(rollbackFor = Exception.class) 确保异常回滚

  4. 编程式事务在某些场景下比声明式事务更灵活

总结

事务管理是保证数据一致性的关键。理解事务的工作机制和常见陷阱,能帮你写出更健壮的代码。在实际项目中,合理配置事务边界非常重要。


   转载规则


《MySQL事务ACID与隔离级别》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录