Appearance
事务处理
事务是数据库操作的最小逻辑单元——一组操作要么全部成功,要么全部失败。它是数据一致性的基石,也是理解分布式事务的前置知识。
一、什么是事务?
事务的概念:事务是一组共享数据上的操作序列,作为整体保持系统一致性;
例子:
银行转账
张三给李四转 100 元:
① 张三账户余额 -100
② 李四账户余额 +100
这两步必须同时成功或同时失败,不能出现:
- 张三扣了钱,李四没收到(钱丢了)
- 张三没扣钱,李四收到了(银行亏了)1.1 MySQL 事务基础语法
sql
-- 显式事务(推荐方式)
START TRANSACTION;
-- 或
BEGIN;
-- 执行 SQL 操作
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 提交或回滚
COMMIT; -- 确认,数据永久生效
ROLLBACK; -- 撤销,回到事务开始前的状态autocommit 模式:
MySQL 默认 autocommit = ON,每条 SQL 语句自动作为一个事务提交
-- 查看 autocommit 状态
SELECT @@autocommit; -- → 1(ON)
-- 关闭自动提交(当前会话)
SET autocommit = 0;
-- 关闭后,每条 SQL 不会自动提交,必须手动 COMMIT 或 ROLLBACK
UPDATE account SET balance = 1000 WHERE id = 1;
-- 此时其他连接看不到修改,因为事务还没提交
COMMIT;
-- 显示开启事务(START TRANSACTION)会自动禁用 autocommit
-- 事务结束后恢复 autocommit 设置隐式提交:以下情况会自动提交当前事务(即使你没有写 COMMIT):
① 执行 DDL 语句(CREATE / ALTER / DROP / TRUNCATE TABLE)
② 隐式使用或修改 mysql 库中的表(如创建用户、授权)
③ 执行 LOCK TABLES / UNLOCK TABLES
④ 加载数据(LOAD DATA INFILE)
⑤ 开启一个新的事务(START TRANSACTION / BEGIN)会提交上一个
⑥ 从机执行 STOP SLAVE 等复制控制语句
注意:在事务中穿插 DDL 语句要特别小心,它会隐式提交当前事务sql
-- 事务的几种控制语句
START TRANSACTION; -- 开始事务
START TRANSACTION READ ONLY; -- 只读事务(优化,不能执行写操作)
START TRANSACTION READ WRITE; -- 读写事务(默认)
COMMIT; -- 提交
COMMIT AND CHAIN; -- 提交后立即开始一个新事务(省去 BEGIN)
ROLLBACK; -- 全部回滚
ROLLBACK AND CHAIN; -- 回滚后立即开始一个新事务
-- 事务特征设置(在 START TRANSACTION 时指定)
START TRANSACTION
WITH CONSISTENT SNAPSHOT, -- 开启时立即创建快照(等同 RR 行为)
READ WRITE;二、标准事务必须具备的四大核心特性(ACID 四大特性)
A(Atomicity,原子性):
事务是一个不可分割的单位,要么全做,要么全不做
实现:undo log,回滚时执行反向操作
C(Consistency,一致性):
事务执行前后,数据库从一个一致状态到另一个一致状态
例如:转账前后,总金额不变
实现:由 AID 共同保证
I(Isolation,隔离性):
多个事务并发执行时,互不干扰
实现:锁 + MVCC
D(Durability,持久性):
事务一旦提交,数据永久保存,即使系统崩溃也不丢失
实现:redo log,崩溃恢复时重放ACID 之间的依赖关系:
A(原子性)──┐
I(隔离性)──┼──→ C(一致性)←── D(持久性)
│
一致性是目标,原子性、隔离性、持久性是手段三、并发事务带来的问题
假设初始余额 = 1000 元
① 脏读(Dirty Read):
事务 A 改了余额为 2000,还没提交
事务 B 读到余额 = 2000
事务 A 回滚,余额恢复为 1000
→ 事务 B 读到了不存在的 2000,脏数据
② 不可重复读(Non-Repeatable Read):
事务 A 第一次读余额 = 1000
事务 B 把余额改为 2000 并提交
事务 A 第二次读余额 = 2000
→ 同一个事务内两次读结果不同
③ 幻读(Phantom Read):
事务 A 查询"余额 > 500 的用户" → 10 条
事务 B 插入了一条余额 = 800 的新用户并提交
事务 A 再次查询"余额 > 500 的用户" → 11 条
→ 多出来一条"幽灵"数据| 问题 | 现象 | 本质区别 |
|---|---|---|
| 脏读 | 读到别人未提交的修改 | 读到了"可能不存在"的数据 |
| 不可重复读 | 同一行数据两次读结果不同 | 别人 UPDATE 了同一行 |
| 幻读 | 同样的条件两次查出的行数不同 | 别人 INSERT/DELETE 了 |
四、四种隔离级别
隔离级别从低到高,性能从高到低:
READ UNCOMMITTED(读未提交) ← 最低,啥问题都不解决
READ COMMITTED(读已提交) ← 解决脏读
REPEATABLE READ(可重复读) ← 解决脏读 + 不可重复读(MySQL 默认)
SERIALIZABLE(串行化) ← 最高,全解决,但性能最差| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
|---|---|---|---|---|
| READ UNCOMMITTED | 是 | 是 | 是 | 无锁,直接读 |
| READ COMMITTED | 否 | 是 | 是 | 每次读生成新 ReadView |
| REPEATABLE READ | 否 | 否 | 部分解决 | 事务开始时生成 ReadView |
| SERIALIZABLE | 否 | 否 | 否 | 表锁 + 串行执行 |
4.1 Savepoint(保存点)
Savepoint 允许在事务内部设置回滚标记点,部分回滚而不是全部撤销。
sql
-- 基本语法
SAVEPOINT point_name; -- 创建保存点
ROLLBACK TO SAVEPOINT point_name; -- 回滚到保存点(保存点之后的语句撤销,之前的保留)
RELEASE SAVEPOINT point_name; -- 释放保存点(删除,不可再回滚到该点)sql
-- 示例:转账业务中的部分回滚
START TRANSACTION;
-- ① 张三扣款
UPDATE account SET balance = balance - 100 WHERE id = 1;
SAVEPOINT after_deduct; -- 扣款后打个标记
-- ② 李四加款
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 这里李四账户有问题,需要撤销加款,但张三的扣款保留
ROLLBACK TO SAVEPOINT after_deduct; -- 回到扣款后,加款被撤销
-- ③ 尝试给王五加款
UPDATE account SET balance = balance + 100 WHERE id = 3;
COMMIT; -- 最终:张三扣了 100,王五加了 100,李四不受影响Savepoint 常见场景:
场景 1:批量处理中逐条容错
遍历 1000 条数据,每条处理前设 savepoint
某条失败 → 回滚到 savepoint,继续处理下一条
场景 2:分支逻辑回滚
先执行 A 步骤,设置 savepoint
尝试 B 步骤,失败 → 回滚到 savepoint,尝试 C 步骤
场景 3:Spring NESTED 传播的底层实现
@Transactional(propagation = Propagation.NESTED)
内部调用的方法失败时,只回滚到 savepoint,不回滚整个事务Spring 中使用 Savepoint:
@Transactional
public void processBatch() {
// 主事务逻辑
orderMapper.insert(order); // ① 一定会保留
try {
// NESTED 传播:内部创建 savepoint
nestedService.doSubTask(); // ② 失败不影响主事务
} catch (Exception e) {
log.warn("子任务失败,继续主流程", e);
}
paymentMapper.record(order); // ③ 继续执行
}
@Service
public class NestedService {
@Transactional(propagation = Propagation.NESTED)
public void doSubTask() {
// 如果这里抛异常,只回滚到 savepoint
// 主事务的 ① 和 ③ 不受影响
}
}4.2 隔离级别的三种设置方式
MySQL 提供了三种粒度的隔离级别设置,生效范围从大到小,优先级从低到高。
三种设置级别:
GLOBAL(全局级别)
├─ 生效范围:所有新建立的连接
├─ 已有连接:不受影响
├─ 重启后:失效(除非写入配置文件)
└─ 优先级:最低(可被 SESSION 和 TRANSACTION 覆盖)
SESSION(会话级别)
├─ 生效范围:当前连接中的所有后续事务
├─ 已有事务:不受影响
├─ 断开连接后:失效
└─ 优先级:中等(可被 TRANSACTION 覆盖)
TRANSACTION(事务级别)
├─ 生效范围:仅当前事务中的下一个语句
├─ 事务结束后:恢复为 SESSION 级别
└─ 优先级:最高(覆盖 GLOBAL 和 SESSION)① GLOBAL 级别
sql
-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
-- 设置全局隔离级别(需要 SUPER 权限)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 验证
SELECT @@global.transaction_isolation;
-- → READ-COMMITTED
-- 已有的连接不会受影响,只有新连接才生效
-- 重启 MySQL 后失效,永久生效需要写入配置文件:
-- [mysqld]
-- transaction-isolation = READ-COMMITTEDGLOBAL 使用场景:
场景:生产环境统一隔离级别
例:公司规范要求所有服务使用 READ COMMITTED
→ SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
→ 所有新连接自动生效,已有连接下次重连后生效② SESSION 级别
sql
-- 查看当前会话的隔离级别
SELECT @@transaction_isolation;
-- 或
SELECT @@session.transaction_isolation;
-- 设置当前会话的隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 这个连接中的所有后续事务都使用 REPEATABLE READ
-- 其他连接不受影响
-- 断开连接后恢复为全局默认级别SESSION 使用场景:
场景:临时调试或测试
例:排查一个脏读问题,需要把当前会话降到 READ UNCOMMITTED
→ SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
→ 只在当前连接生效,不影响其他连接,关闭连接后自动恢复③ TRANSACTION 级别
sql
-- 为下一个事务设置隔离级别(只影响下一个事务)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 开始事务
START TRANSACTION;
-- 或
BEGIN;
-- 这个事务使用 SERIALIZABLE 级别
-- 事务提交/回滚后,隔离级别恢复为 SESSION 级别sql
-- 完整示例:混合使用不同级别
-- 当前会话默认是 REPEATABLE READ
SELECT @@transaction_isolation; -- → REPEATABLE-READ
-- 事务 1:用 SERIALIZABLE
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
SELECT * FROM account WHERE id = 1; -- 串行化读
COMMIT;
-- 事务 2:恢复为 REPEATABLE READ
START TRANSACTION;
SELECT * FROM account WHERE id = 1; -- 可重复读
COMMIT;TRANSACTION 使用场景:
场景:特定业务需要更高隔离级别
例:金融对账事务需要 SERIALIZABLE,但不想影响其他业务
→ SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
→ START TRANSACTION;
→ 只影响这一个事务,结束后自动恢复三种级别对比
| 维度 | GLOBAL | SESSION | TRANSACTION |
|---|---|---|---|
| 语法 | SET GLOBAL TRANSACTION ... | SET SESSION TRANSACTION ... | SET TRANSACTION ... |
| 生效范围 | 所有新连接 | 当前连接 | 下一个事务 |
| 已有连接 | 不影响 | 不影响 | 不影响 |
| 已有事务 | 不影响 | 不影响 | 不影响 |
| 重启后 | 失效 | 失效 | 失效 |
| 优先级 | 最低 | 中等 | 最高 |
| 权限要求 | SUPER 或 SYSTEM_VARIABLES_ADMIN | 无 | 无 |
| 典型场景 | 全局规范 | 调试排查 | 特定业务隔离 |
实验验证
sql
-- 查看所有级别的隔离级别
SELECT @@global.transaction_isolation; -- 全局
SELECT @@session.transaction_isolation; -- 会话
SELECT @@transaction_isolation; -- 当前生效(默认显示 session)脏读实验(READ UNCOMMITTED):
事务 A: 事务 B:
SET SESSION TRANSACTION
ISOLATION LEVEL READ UNCOMMITTED;
BEGIN; BEGIN;
UPDATE account SET balance = 2000
WHERE id = 1;
(未提交)
SELECT balance FROM account
WHERE id = 1;
→ 读到 2000 ← 脏读!
ROLLBACK; ← 余额恢复为 1000
COMMIT;
→ 事务 B 基于错误数据做了决策
不可重复读实验(READ COMMITTED):
事务 A: 事务 B:
BEGIN; BEGIN;
SELECT balance FROM account
WHERE id = 1; → 1000
UPDATE account SET balance = 2000
WHERE id = 1;
COMMIT;
SELECT balance FROM account
WHERE id = 1; → 2000 ← 不可重复读!
COMMIT;五、MVCC 原理
MVCC(Multi-Version Concurrency Control,多版本并发控制)是 MySQL InnoDB 实现隔离性的核心机制。 它的核心思想:读不阻塞写,写不阻塞读,通过保存数据的多个版本来实现。
5.1 三个隐藏字段
InnoDB 每行数据都有三个隐藏字段:
┌──────────┬──────────┬──────────┬──────────────┬──────────────┐
│ DB_TRX_ID │ DB_ROLL_PTR │ DB_ROW_ID │ col1 │ col2 │ ... │
└──────────┴──────────┴──────────┴──────────────┴──────────────┘
↑ ↑ ↑
│ │ └── 行 ID(没有主键时自动生成)
│ └── 回滚指针(指向 undo log 中的上一个版本)
└── 事务 ID(最后一次修改这行的事务 ID)5.2 ReadView(读视图)
ReadView 决定了当前事务能看到哪个版本的数据:
活跃事务列表(m_ids):当前未提交的事务 ID 集合
最小事务 ID(min_trx_id):m_ids 中的最小值
最大事务 ID(max_trx_id):当前系统已分配的最大事务 ID + 1
创建者 ID(creator_trx_id):当前事务自己的 ID
版本可见性判断(对每个数据版本做判断):
① trx_id == creator_trx_id → 可见(自己改的)
② trx_id < min_trx_id → 可见(已提交的老数据)
③ trx_id >= max_trx_id → 不可见(未来的事务)
④ trx_id 在 m_ids 中 → 不可见(未提交的事务)
⑤ trx_id 不在 m_ids 中 → 可见(已提交)5.3 RC 与 RR 的区别
READ COMMITTED(每次读生成新 ReadView):
事务 A:
BEGIN;
SELECT ① → 生成 ReadView 1 → 看到已提交的数据
SELECT ② → 生成 ReadView 2 → 看到新提交的数据(可能不同)
COMMIT;
每次 SELECT 都生成新的 ReadView,所以能看到别人提交的修改
→ 这就是为什么 RC 下会有"不可重复读"
REPEATABLE READ(事务开始时生成 ReadView,整个事务不变):
事务 A:
BEGIN;
SELECT ① → 生成 ReadView → 快照建立
SELECT ② → 复用同一个 ReadView → 看到的数据和 ① 完全一样
COMMIT;
整个事务只用一个 ReadView,所以读到的数据始终一致
→ 这就是为什么 RR 下不会有"不可重复读"5.4 RR 如何解决幻读
MVCC 的快照读(Snapshot Read)可以避免幻读:
SELECT * FROM users WHERE balance > 500;
→ 使用 ReadView,看不到其他事务插入的新行
但当前读(Current Read)仍然可能幻读:
SELECT * FROM users WHERE balance > 500 FOR UPDATE;
→ 加锁读取最新数据,能读到其他事务插入的行
MySQL RR 下的"间隙锁"解决了当前读的幻读:
SELECT * FROM users WHERE balance > 500 FOR UPDATE;
→ 不仅锁住存在的行,还锁住行之间的间隙
→ 其他事务无法在间隙中插入新行
→ 幻读被彻底解决六、Spring 事务管理
6.1 @Transactional 核心机制
@Transactional 工作原理(AOP 代理):
调用 service.method()
│
▼
┌─────────────────────────────────────┐
│ 代理对象(CGLIB 动态代理) │
│ │
│ TransactionInterceptor │
│ ↓ │
│ ① 开启事务(从连接池获取连接,关闭自动提交)│
│ ② 执行目标方法 │
│ ③ 提交事务 / 回滚事务 │
│ ④ 归还连接 │
└─────────────────────────────────────┘6.2 核心属性
java
@Service
public class OrderService {
@Transactional(
propagation = Propagation.REQUIRED, // 传播机制
isolation = Isolation.REPEATABLE_READ, // 隔离级别
timeout = 30, // 超时时间(秒)
readOnly = false, // 是否只读
rollbackFor = Exception.class, // 哪些异常回滚
noRollbackFor = IllegalArgumentException.class // 哪些异常不回滚
)
public void createOrder(Order order) {
orderMapper.insert(order);
inventoryMapper.deduct(order.getProductId(), order.getQuantity());
}
}6.3 七种传播机制
传播机制 = 当前方法被另一个事务方法调用时,事务如何处理
REQUIRED(默认):
有事务 → 加入;没事务 → 新建
████████████████████ ← 最常用,适合大多数场景
REQUIRES_NEW:
不管有没有事务,都新建一个独立事务,挂起当前事务
████████ ░░░░░░░░░░ ← 子事务独立提交/回滚,不影响父事务
SUPPORTS:
有事务 → 加入;没事务 → 非事务运行
████████████████████ 或 ─ ─ ─ ─ ─ ─ ─ ─
NOT_SUPPORTED:
不管有没有事务,都非事务运行,挂起当前事务
─ ─ ─ ─ ─ ─ ─ ─ ─ ─
MANDATORY:
必须有事务,没有就抛异常
████████████████████
NEVER:
必须没有事务,有就抛异常
─ ─ ─ ─ ─ ─ ─ ─ ─ ─
NESTED:
有事务 → 创建嵌套事务(savepoint);没事务 → 和 REQUIRED 一样
████████████████████
└─ savepointjava
// REQUIRES_NEW 典型场景:记录日志,日志失败不影响主业务
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order); // 主业务
try {
logService.recordLog(order); // 日志记录
} catch (Exception e) {
// 日志失败不影响订单创建
}
}
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void recordLog(Order order) {
logMapper.insert(order); // 独立事务,提交/回滚不影响外部
}
}6.4 事务失效的 8 个场景
java
// ① 非 public 方法
@Transactional
void createOrder() { } // ❌ AOP 代理只能拦截 public 方法
// ② 同类内部调用(this 调用)
public void outerMethod() {
this.innerMethod(); // ❌ 绕过了代理,事务不生效
}
@Transactional
public void innerMethod() { }
// 解决:注入自己
@Autowired
private OrderService self;
public void outerMethod() {
self.innerMethod(); // ✅ 通过代理调用
}
// ③ 异常被吞掉
@Transactional
public void createOrder() {
try {
orderMapper.insert(order);
int i = 1 / 0; // 抛出异常
} catch (Exception e) {
log.error("error", e); // ❌ 异常被吞了,事务不回滚!
}
}
// ④ 抛出的不是 RuntimeException
@Transactional
public void createOrder() throws Exception {
throw new Exception("..."); // ❌ 默认只回滚 RuntimeException
}
// 解决:@Transactional(rollbackFor = Exception.class)
// ⑤ 数据库引擎不支持事务(MyISAM)
// ⑥ 多线程调用(子线程不在同一事务中)
// ⑦ 方法用 final 修饰(CGLIB 不能代理 final 方法)
// ⑧ @Transactional 放在了抽象类上(Spring 不代理抽象类)6.5 事务最佳实践
java
// ✅ 1. 放在 Service 层,不要放在 Controller
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 业务逻辑集中在这里
}
}
// ✅ 2. 明确指定 rollbackFor
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) { }
// ✅ 3. 事务方法尽量短,只包裹必要的数据库操作
@Transactional
public void createOrder(Order order) {
orderMapper.insert(order); // 数据库操作
// 不要把下面的放进来:
// sendEmail(order); // 外部 API 调用
// generatePDF(order); // 文件操作
// Thread.sleep(5000); // 阻塞操作
}
// ✅ 4. 使用编程式事务(事务模板),更灵活
@Autowired
private TransactionTemplate transactionTemplate;
public void createOrder(Order order) {
transactionTemplate.execute(status -> {
orderMapper.insert(order);
inventoryMapper.deduct(order.getProductId(), order.getQuantity());
return null;
});
}七、分布式事务入口
单机事务 → 分布式事务的演进:
单机事务(本章):
一个数据库 → @Transactional 搞定
保证:ACID(原子性、一致性、隔离性、持久性)
分布式事务(下一章):
多个数据库、多个服务 → 需要协调多个参与者
代价:放弃强一致性,追求最终一致性(BASE 理论)
详见 [分布式事务与实现](../../microservices-architecture/09-微服务解决方案/分布式事务与实现)八、面试要点
Q1:MySQL 默认隔离级别是什么?为什么?
REPEATABLE READ(可重复读)。原因:1) 避免不可重复读;2) 通过间隙锁解决幻读;3) 比 SERIALIZABLE 性能好很多。Oracle 默认是 READ COMMITTED。
Q2:MVCC 的实现原理?
核心是隐藏字段(DB_TRX_ID + DB_ROLL_PTR)+ undo log + ReadView。通过 ReadView 的可见性判断,让不同事务看到不同版本的数据,实现读不阻塞写、写不阻塞读。
Q3:@Transactional 失效的原因有哪些?
非 public 方法、同类 this 调用绕过了代理、异常被 catch 吞掉、抛出 checked 异常未指定 rollbackFor、数据库引擎不支持事务、final 方法不能被 CGLIB 代理。
Q4:REQUIRED 和 REQUIRES_NEW 的区别?
REQUIRED 加入已有事务,一起提交回滚;REQUIRES_NEW 挂起当前事务,创建独立事务,提交回滚互不影响。典型场景:日志记录失败不影响主业务。
Q5:RR 隔离级别下,幻读是怎么解决的?
快照读(普通 SELECT)通过 MVCC 的 ReadView 避免幻读。当前读(SELECT FOR UPDATE)通过间隙锁(Gap Lock)锁住行之间的间隙,阻止其他事务插入。
