引言
事务是数据库系统的核心特性,是保证数据正确性和一致性的关键机制。理解事务的 ACID 特性,对于编写正确的数据库应用至关重要。
本章将深入讲解:
- 事务的四大特性(ACID)详解
- 并发事务的问题与隔离级别
- MySQL 事务的实现原理
- 事务控制的实战技巧
核心概念
什么是事务
事务(Transaction) 是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。这些操作要么全部执行成功,要么全部不执行,是一个不可分割的工作单位。
典型场景:
-- 银行转账:A 向 B 转账 100 元
START TRANSACTION;
-- 步骤 1:A 账户扣 100 元
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
-- 步骤 2:B 账户加 100 元
UPDATE accounts SET balance = balance + 100 WHERE account_id = 'B';
-- 步骤 3:提交事务
COMMIT;
事务的特点:
- 原子性:要么都做,要么都不做
- 一致性:转账前后总金额不变
- 隔离性:多个转账操作互不干扰
- 持久性:提交后永久保存
事务的 ACID 特性
graph TD
A[事务 ACID] --> B[原子性 Atomicity]
A --> C[一致性 Consistency]
A --> D[隔离性 Isolation]
A --> E[持久性 Durability]
B --> B1[Undo Log 实现]
C --> C1[原子性 + 隔离性保证]
D --> D1[锁+MVCC 实现]
E --> E1[Redo Log 实现]
style A fill:#ff9999
style B fill:#99ccff
style C fill:#99ccff
style D fill:#99ccff
style E fill:#99ccff
原子性(Atomicity)
概念
原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部发生,要么全部不发生。
示例:
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
-- 假设这里发生错误或断电
UPDATE accounts SET balance = balance + 100 WHERE account_id = 'B';
COMMIT;
如果第 2 步失败:
- 没有原子性:A 扣了钱,B 没收到的钱 → 数据不一致 ❌
- 有原子性:A 的扣款操作回滚 → 数据一致 ✅
MySQL 实现原理
Undo Log(回滚日志) 实现原子性:
-- 执行更新时,先记录 Undo Log
-- 事务 101:UPDATE accounts SET balance = 900 WHERE id = 1
-- Undo Log 记录:
-- {transaction_id: 101, prev_balance: 1000, operation: "UPDATE", row_id: 1}
-- 如果事务回滚或崩溃恢复:
-- 根据 Undo Log 将 balance 恢复为 1000
Undo Log 的作用:
- 回滚:事务执行失败时,撤销已做的修改
- MVCC:提供数据的历史版本,实现多版本并发控制
崩溃恢复流程:
graph LR
A[数据库崩溃] --> B[重启恢复]
B --> C[读取 Undo Log]
C --> D{事务是否提交?}
D -->|未提交 | E[回滚操作]
D -->|已提交 | F[保持修改]
E --> G[数据一致性恢复]
F --> G
实战示例
手动回滚:
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
-- 检查条件
SELECT balance FROM accounts WHERE account_id = 'A';
-- 发现余额不足
-- 回滚事务
ROLLBACK;
-- A 账户余额恢复原状
异常自动回滚:
-- 设置自动回滚
SET autocommit = 0;
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
-- 假设发生错误(如违反约束)
-- MySQL 会自动回滚整个事务
一致性(Consistency)
概念
一致性是指事务执行前后,数据库从一个一致性状态变换到另一个一致性状态。
一致性状态包括:
- 数据的完整性约束(主键、外键、唯一约束)
- 业务逻辑的一致性(如转账总金额不变)
- 触发器、存储过程等定义的业务规则
示例:
-- 约束:balance >= 0
ALTER TABLE accounts ADD CONSTRAINT chk_balance CHECK (balance >= 0);
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 'A';
-- 如果 A 账户余额只有 50,违反 CHECK 约束
-- MySQL 会拒绝执行,事务回滚
-- 保证了数据的一致性
一致性的保证
一致性是 ACID 的最终目的,由其他三个特性共同保证:
-- 原子性保证:操作要么全做,要么全不做
-- 隔离性保证:并发事务不互相干扰
-- 持久性保证:提交后数据永久保存
-- 三者共同作用 → 数据一致性
业务一致性示例:
-- 场景:订单支付
START TRANSACTION;
-- 1. 更新订单状态
UPDATE orders SET status = 'PAID' WHERE order_id = 'ORD001';
-- 2. 扣减库存
UPDATE products SET stock = stock - 1 WHERE product_id = 'PROD001';
-- 3. 记录支付流水
INSERT INTO payment_logs (order_id, amount, create_time)
VALUES ('ORD001', 100.00, NOW());
COMMIT;
-- 三个操作要么都成功,要么都失败
-- 保证了业务数据的一致性
隔离性(Isolation)
概念
隔离性是指多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
并发事务可能出现的问题:
| 问题 | 描述 | 示例 |
|---|---|---|
| 脏读 | 读到未提交的数据 | T1 修改未提交,T2 读取 |
| 不可重复读 | 同一事务内多次读取结果不同 | T1 读取后,T2 修改并提交,T1 再次读取 |
| 幻读 | 同一事务内多次查询,结果集数量不同 | T1 查询后,T2 插入新数据,T1 再次查询 |
脏读(Dirty Read)
场景:
时间线:
T1: START TRANSACTION;
T1: UPDATE accounts SET balance = 900 WHERE id = 1; -- 余额从 1000 改为 900
T2: START TRANSACTION;
T2: SELECT balance FROM accounts WHERE id = 1; -- 读到 900(未提交)❌
T1: ROLLBACK; -- T1 回滚,余额恢复为 1000
T2: SELECT balance FROM accounts WHERE id = 1; -- 读到 1000
-- T2 发现前后读取结果不同,之前读到的是脏数据!
避免方法:
-- 设置隔离级别为 READ COMMITTED 或更高
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
不可重复读(Non-repeatable Read)
场景:
时间线:
T1: START TRANSACTION;
T1: SELECT balance FROM accounts WHERE id = 1; -- 读到 1000
T2: START TRANSACTION;
T2: UPDATE accounts SET balance = 900 WHERE id = 1;
T2: COMMIT; -- T2 提交
T1: SELECT balance FROM accounts WHERE id = 1; -- 读到 900 ❌
-- T1 在同一事务内两次读取结果不同!
避免方法:
-- 设置隔离级别为 REPEATABLE READ 或更高
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
幻读(Phantom Read)
场景:
时间线:
T1: START TRANSACTION;
T1: SELECT * FROM accounts WHERE balance > 500; -- 返回 10 条记录
T2: START TRANSACTION;
T2: INSERT INTO accounts (id, balance) VALUES (101, 600);
T2: COMMIT; -- T2 提交
T1: SELECT * FROM accounts WHERE balance > 500; -- 返回 11 条记录 ❌
-- T1 发现"凭空"多了一条记录,像幻影一样!
避免方法:
-- 设置隔离级别为 SERIALIZABLE
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 或使用 InnoDB 的 MVCC + 间隙锁
-- MySQL 默认隔离级别 REPEATABLE READ 已能避免大部分幻读
四种隔离级别
SQL 标准定义了四种隔离级别,从低到高:
graph TD
A[READ UNCOMMITTED] -->|解决 | B[脏读 ❌]
B --> C[READ COMMITTED]
C -->|解决 | D[不可重复读 ❌]
D --> E[REPEATABLE READ]
E -->|解决 | F[幻读 ❌]
F --> G[SERIALIZABLE]
style A fill:#ffcccc
style C fill:#ffeb99
style E fill:#ccffcc
style G fill:#99ccff
详细对比:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ UNCOMMITTED | ⚠️ 可能 | ⚠️ 可能 | ⚠️ 可能 | ⭐⭐⭐⭐⭐ |
| READ COMMITTED | ✅ 避免 | ⚠️ 可能 | ⚠️ 可能 | ⭐⭐⭐⭐ |
| REPEATABLE READ | ✅ 避免 | ✅ 避免 | ⚠️ 部分避免 | ⭐⭐⭐ |
| SERIALIZABLE | ✅ 避免 | ✅ 避免 | ✅ 避免 | ⭐⭐ |
MySQL 的隔离级别
查看当前隔离级别:
-- 查看会话级别
SELECT @@transaction_isolation;
-- 或
SELECT @@tx_isolation;
-- 查看全局级别
SELECT @@global.transaction_isolation;
设置隔离级别:
-- 设置会话级别(只对当前连接生效)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局级别(对新连接生效)
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置单个事务
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
-- 事务结束后恢复原级别
MySQL 8.0 默认: REPEATABLE READ
持久性(Durability)
概念
持久性是指事务一旦提交,其对数据库的修改就是永久性的,即使发生故障也不会丢失。
示例:
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT; -- 提交后,即使立即断电,修改也不会丢失
MySQL 实现原理
Redo Log(重做日志) 实现持久性:
-- 事务执行流程:
-- 1. 修改数据页(在内存 Buffer Pool 中)
-- 2. 写入 Redo Log(顺序写磁盘,速度快)
-- 3. 提交事务
-- 崩溃恢复流程:
-- 1. 数据库重启
-- 2. 读取 Redo Log
-- 3. 重做已提交的事务
-- 4. 数据恢复到一致状态
Redo Log 的特点:
- 顺序写:比随机写数据页快得多
- 循环写:写满后从头开始覆盖
- 崩溃安全:即使数据页没刷盘,Redo Log 已落盘
WAL(Write-Ahead Logging)技术:
graph LR
A[事务修改] --> B[写 Redo Log]
B --> C{提交事务?}
C -->|是 | D[刷盘 Redo Log]
C -->|否 | E[继续执行]
D --> F[异步刷数据页]
F --> G[持久化完成]
双 1 设置(最安全)
# my.cnf 配置
[mysqld]
# 每次事务提交都刷盘 Redo Log
innodb_flush_log_at_trx_commit = 1
# 每次提交都同步刷盘
sync_binlog = 1
性能影响:
- 安全性最高 ✅
- 性能最低(每次提交都要刷盘)❌
- 适合对数据一致性要求高的场景(如金融)
性能优化配置
# 性能优先配置
[mysqld]
# 每秒刷盘一次 Redo Log
innodb_flush_log_at_trx_commit = 2
# 操作系统控制 binlog 刷盘
sync_binlog = 0
风险:
- 性能提升约 30-50% ✅
- 崩溃可能丢失 1 秒数据 ⚠️
- 适合对性能要求高的场景(如日志、统计)
事务控制实战
事务控制语句
-- 开启事务
START TRANSACTION;
-- 或
BEGIN;
-- 提交事务
COMMIT;
-- 回滚事务
ROLLBACK;
-- 设置保存点
SAVEPOINT sp1;
ROLLBACK TO sp1; -- 回滚到保存点
RELEASE SAVEPOINT sp1; -- 释放保存点
自动提交
-- 查看自动提交状态
SELECT @@autocommit;
-- 关闭自动提交(手动控制事务)
SET autocommit = 0;
-- 开启自动提交
SET autocommit = 1;
注意:
-- autocommit = 1 时,每条 SQL 都是一个事务
UPDATE accounts SET balance = 100 WHERE id = 1; -- 自动提交
-- autocommit = 0 时,需要手动提交
SET autocommit = 0;
UPDATE accounts SET balance = 100 WHERE id = 1;
UPDATE accounts SET balance = 200 WHERE id = 2;
COMMIT; -- 两条语句作为一个事务提交
隐式提交
以下操作会导致隐式提交:
-- DDL 语句
CREATE TABLE ...
ALTER TABLE ...
DROP TABLE ...
-- 管理语句
SET autocommit = 1;
START TRANSACTION; -- 会先提交当前事务
注意:
START TRANSACTION;
UPDATE accounts SET balance = 100 WHERE id = 1;
CREATE TABLE logs (...); -- 隐式提交!前面的 UPDATE 被提交
ROLLBACK; -- 无法回滚 UPDATE 操作
事务超时与死锁
锁等待超时:
-- 查看超时时间(秒)
SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
-- 默认:50 秒
-- 设置超时时间
SET innodb_lock_wait_timeout = 10;
-- 超时后,语句失败但事务可以继续
死锁检测:
-- 查看死锁日志
SHOW ENGINE INNODB STATUS;
-- 死锁处理:
-- 1. 自动回滚权重小的事务
-- 2. 应用层重试机制
避免死锁:
-- 1. 固定顺序访问表
-- 事务 1 和事务 2 都按相同顺序操作
-- 2. 降低锁粒度
-- 使用索引减少锁范围
-- 3. 减少事务持有时间
-- 大事务拆分为小事务
注意事项
1. 长事务问题
问题:
START TRANSACTION;
-- 大量操作...
-- 长时间未提交
-- 问题:
-- 1. 占用 Undo Log,影响性能
-- 2. 持有锁,阻塞其他事务
-- 3. 主从延迟
监控长事务:
-- 查询运行超过 10 秒的事务
SELECT * FROM information_schema.INNODB_TRX
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 10;
-- 杀掉长事务
KILL <thread_id>;
2. 事务大小
避免大事务:
-- ❌ 错误:一次性删除 100 万行
START TRANSACTION;
DELETE FROM logs WHERE create_time < '2023-01-01';
COMMIT;
-- ✅ 正确:分批删除
SET @batch_size = 10000;
REPEAT
START TRANSACTION;
DELETE FROM logs WHERE create_time < '2023-01-01' LIMIT @batch_size;
COMMIT;
DO SLEEP(0.1);
UNTIL ROW_COUNT() < @batch_size END REPEAT;
3. 隔离级别选择
推荐配置:
# 大多数场景
transaction_isolation = REPEATABLE READ
# 需要与 Oracle 兼容
transaction_isolation = READ COMMITTED
# 特殊场景(如统计报表)
# 可以考虑 READ UNCOMMITTED(允许脏读)
4. 应用层事务管理
Java Spring 示例:
@Transactional(rollbackFor = Exception.class)
public void transfer(String from, String to, BigDecimal amount) {
// 扣款
accountDao.debit(from, amount);
// 收款
accountDao.credit(to, amount);
// 异常时自动回滚
}
总结
ACID 特性总结
| 特性 | 保证 | 实现机制 |
|---|---|---|
| 原子性 | 操作不可分割 | Undo Log |
| 一致性 | 数据状态一致 | 原子性 + 隔离性 + 持久性 |
| 隔离性 | 并发不干扰 | 锁 + MVCC |
| 持久性 | 提交不丢失 | Redo Log |
隔离级别选择
| 场景 | 推荐隔离级别 |
|---|---|
| 金融系统 | REPEATABLE READ / SERIALIZABLE |
| 电商系统 | READ COMMITTED / REPEATABLE READ |
| 日志统计 | READ UNCOMMITTED / READ COMMITTED |
| 默认配置 | REPEATABLE READ |
最佳实践
- 保持事务短小:避免长事务和大事务
- 合理选择隔离级别:平衡一致性和性能
- 注意隐式提交:DDL 语句会提交事务
- 监控事务状态:及时发现长事务和死锁
- 应用层重试:处理死锁和超时异常
下一步
理解 ACID 特性后,下一章我们将深入学习:
- 事务隔离级别与 MVCC 原理
- 锁机制详解(行锁、表锁、间隙锁)
- 死锁分析与解决方案
参考资料
- MySQL 官方文档 - 事务
- 《高性能 MySQL》第 6 章:MySQL 架构与历史
- 《MySQL 技术内幕:InnoDB 存储引擎》第 7 章:事务
- Innodb 事务实现原理