写在前面:事务是数据库最核心的特性之一。没有事务,我们的银行转账可能钱打出去但账户没扣款;没有事务,订单创建可能商品库存减了但订单没生成。这些数据不一致的问题,在事务面前都将不复存在。
开篇引入:转账失败的惊魂一刻
想象这个场景:你在线转账200元给朋友。系统显示"转账成功",你很开心。但第二天朋友说没收到。一查流水,钱确实扣了,但朋友账户没增加。
这在有事务保护的数据库中是不可能发生的。转账操作会被包裹在一个事务中:扣款和存款,要么同时成功,要么同时失败。
今天,让我们深入理解事务——这个数据库世界最重要的概念之一。
什么是事务?
事务(Transaction)是一组SQL语句,作为一个工作单元以原子方式进行处理。原子意味着不可分割——要么全部执行成功,要么全部执行失败。
START TRANSACTION;
-- 步骤1:检查余额
SELECT balance FROM accounts WHERE user_id = 1;
-- 如果余额 >= 200,继续
-- 步骤2:扣款
UPDATE accounts SET balance = balance - 200 WHERE user_id = 1;
-- 步骤3:存款
UPDATE accounts SET balance = balance + 200 WHERE user_id = 2;
-- 如果以上都成功
COMMIT;
-- 如果任何一步失败
ROLLBACK; -- 全部回滚到事务开始前的状态
事务的四个阶段
START TRANSACTION
↓
执行SQL(修改数据)
↓
┌─────────────────┐
│ 一切顺利? │
└────────┬────────┘
│
┌────┴────┐
↓ ↓
COMMIT ROLLBACK
↓ ↓
提交 回滚
生效 撤销
ACID:事务的四条铁律
ACID是数据库事务处理遵循的四个原则,代表了事务应有的特性。
A - Atomicity(原子性)
原子性意味着事务是不可分割的工作单元。事务中的所有操作要么全部提交,要么全部回滚。
实际案例:
START TRANSACTION;
UPDATE accounts SET balance = balance - 200 WHERE user_id = 1; -- 成功
UPDATE accounts SET balance = balance + 200 WHERE user_id = 2; -- 成功
COMMIT; -- 提交,整个转账完成
-- 或者中途失败
START TRANSACTION;
UPDATE accounts SET balance = balance - 200 WHERE user_id = 1; -- 成功
UPDATE accounts SET balance = balance + 200 WHERE user_id = 2; -- 网络断开!
COMMIT; -- 失败
ROLLBACK; -- 自动回滚,扣款也撤销
如果原子性不保证,钱可能扣了但没到账——这是灾难性的。
C - Consistency(一致性)
一致性确保数据库从一个一致状态转换到另一个一致状态。事务执行前后,数据库的完整性约束必须保持。
什么是完整性约束?
-- 主键约束:id 必须唯一
-- 外键约束:orders.user_id 必须引用 users.id
-- 唯一约束:email 不能重复
-- 非空约束:name 不能为空
-- 检查约束:age > 0
一致性示例:
-- 假设有外键约束:orders.user_id 引用 users.id
START TRANSACTION;
INSERT INTO users (id, name) VALUES (1, 'Alice');
INSERT INTO orders (user_id, amount) VALUES (1, 100);
COMMIT; -- 成功,两个表都有一致的数据
START TRANSACTION;
DELETE FROM users WHERE id = 1; -- 失败!有订单引用这个用户
COMMIT;
-- 触发外键约束,事务回滚
I - Isolation(隔离性)
隔离性意味着一个事务的修改在提交前对其他事务不可见。这防止了并发导致的数据混乱。
并发问题的例子:
-- 问题场景:脏读
Session A Session B
──────── ────────
SELECT balance UPDATE balance = 0
FROM accounts WHERE user_id = 1;
WHERE user_id = 1;
COMMIT;
-- A 读取到了 B 未提交的修改
-- 如果 B 回滚了,A 读取的数据就是"脏"的
隔离性通过隔离级别来控制,后面会详细讲解。
D - Durability(持久性)
持久性意味着一旦事务提交,其修改将永久保存,即使系统崩溃也不会丢失。
START TRANSACTION;
INSERT INTO orders (id, amount) VALUES (1, 100);
COMMIT; -- 返回"提交成功"
-- 即使数据库服务器立即断电重启
-- 这条订单记录依然存在
ACID vs CAP:不同的哲学
分布式系统中有CAP定理:一个系统不能同时满足一致性(Consistency)、可用性(Availability)和分区容错(Partition tolerance)。
MySQL作为单节点数据库,可以通过ACID保证强一致性。分布式数据库可能为了可用性而牺牲强一致性。
MySQL的事务控制
基本语法
-- 开始事务
START TRANSACTION;
-- 或者
BEGIN;
-- 提交事务
COMMIT;
-- 回滚事务
ROLLBACK;
-- 设置保存点
SAVEPOINT point1;
-- 回滚到保存点
ROLLBACK TO SAVEPOINT point1;
AUTOCOMMIT模式
MySQL默认启用自动提交模式——每条SQL语句立即生效。
-- 查看当前 AUTOCOMMIT 设置
SHOW VARIABLES LIKE 'autocommit';
-- 禁用自动提交
SET autocommit = 0;
-- 执行多条SQL(需要手动 COMMIT)
INSERT INTO users VALUES (1, 'Alice');
INSERT INTO orders VALUES (1, 1, 100);
COMMIT;
-- 重新启用
SET autocommit = 1;
建议:除非是只读查询,否则显式使用事务。
多语句事务的陷阱
-- 第一个 INSERT 立即生效(AUTOCOMMIT=1)
INSERT INTO users VALUES (1, 'Alice');
-- 第二个 INSERT 在事务中
START TRANSACTION;
INSERT INTO orders VALUES (1, 1, 100);
ROLLBACK; -- 只回滚第二个 INSERT
事务隔离级别
这是事务最复杂的部分。SQL标准定义了4种隔离级别,从低到高依次为:
READ UNCOMMITTED(未提交读)
最低的隔离级别。事务可以读取其他事务未提交的修改。
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
-- Session A
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 1;
-- 未提交
-- Session B(同一时间)
SELECT balance FROM accounts WHERE user_id = 1;
-- 读取到 100(脏数据)!即使 A 还没提交
问题:脏读、不可重复读、幻读都可能发生
场景:几乎不用,除非你能接受脏数据
READ COMMITTED(提交读)
只能读取已提交事务的修改。大多数数据库的默认级别。
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Session A
START TRANSACTION;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 1;
COMMIT; -- 提交后
-- Session B
START TRANSACTION;
SELECT balance FROM accounts WHERE user_id = 1;
-- 只能读到提交后的数据
问题:不可重复读、幻读可能发生
场景:适合对数据一致性要求不高的系统
REPEATABLE READ(可重复读)
MySQL InnoDB的默认隔离级别。同一个事务中多次读取相同数据,结果一致。
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- Session A
START TRANSACTION;
SELECT SUM(amount) FROM orders; -- 第一次读取:1000
-- Session B(同一时间)
INSERT INTO orders VALUES (2, 1, 500);
COMMIT;
-- Session A 再次读取
SELECT SUM(amount) FROM orders; -- 结果还是 1000!
-- B 的插入对 A 不可见
问题:理论上幻读可能发生,但InnoDB通过MVCC解决
场景:MySQL默认,适合大多数应用
SERIALIZABLE(可串行化)
最高的隔离级别。强制事务串行执行,完全消除并发问题。
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- Session A
START TRANSACTION;
SELECT * FROM orders;
-- 实际上给所有读取的行加了锁
-- Session B
START TRANSACTION;
INSERT INTO orders VALUES (3, 1, 200);
-- 等待 A 提交/回滚后才执行
问题:高并发下性能很差
场景:对数据一致性要求极高、并发要求极低的系统
隔离级别对比
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 最高 |
| READ COMMITTED | 不可能 | 可能 | 可能 | 高 |
| REPEATABLE READ | 不可能 | 不可能 | 可能* | 中 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 低 |
*InnoDB通过MVCC解决了幻读
事务日志:持久性的秘密
事务日志(Transaction Log)是实现持久性的关键。
Write-Ahead Logging(WAL)
普通写入:
内存 → 磁盘(随机I/O) → 完成
WAL写入:
内存 → 事务日志(顺序I/O) → 后台线程 → 磁盘
↓ 快速完成 ↓ 异步写入
日志已持久化
Redo Log vs Undo Log
Redo Log(重做日志):
- 记录"将要执行"的操作
- 用于崩溃恢复,重做未完全持久化的修改
- InnoDB特有
Undo Log(回滚日志):
- 记录"反向操作"
- 用于回滚未提交的事务
- 支持MVCC
-- 查看 Redo Log 配置
SHOW VARIABLES LIKE 'innodb_log%';
-- innodb_log_file_size:单个日志文件大小
-- innodb_log_files_in_group:日志文件数量
-- 总大小 = innodb_log_file_size × innodb_log_files_in_group
崩溃恢复流程
数据库崩溃
↓
重启后检查 Redo Log
↓
重做已提交但未持久化的修改
↓
回滚未提交事务的修改(使用 Undo Log)
↓
恢复完成
实战:事务使用最佳实践
1. 短事务原则
-- ❌ 反面:长事务
BEGIN;
-- 用户登录检查
SELECT * FROM users WHERE email = 'xxx';
-- 用户下单
INSERT INTO orders VALUES (...);
-- 发送通知邮件(可能很慢)
-- ...
COMMIT; -- 整个过程都持有锁
-- ✅ 正面:短事务
BEGIN;
INSERT INTO orders VALUES (...);
COMMIT;
-- 发送通知邮件(异步)
2. 批量操作的事务控制
-- 插入10000条记录
-- ❌ 方式1:一个大事务
BEGIN;
INSERT INTO orders VALUES (...); -- 重复10000次
COMMIT; -- 风险:长时间锁表
-- ✅ 方式2:分批小事务
DELIMITER //
CREATE PROCEDURE batch_insert()
BEGIN
DECLARE i INT DEFAULT 1;
WHILE i <= 10000 DO
INSERT INTO orders VALUES (i, 1, 100);
IF i % 1000 = 0 THEN
COMMIT;
END IF;
SET i = i + 1;
END WHILE;
COMMIT;
END //
DELIMITER ;
3. 异常处理与事务
DELIMITER //
CREATE PROCEDURE transfer_money(
IN from_user INT,
IN to_user INT,
IN amount DECIMAL(10,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT 'Transfer failed' AS message;
END;
START TRANSACTION;
UPDATE accounts SET balance = balance - amount
WHERE user_id = from_user;
IF (SELECT balance FROM accounts WHERE user_id = from_user) < 0 THEN
ROLLBACK;
SELECT 'Insufficient balance' AS message;
ELSE
UPDATE accounts SET balance = balance + amount
WHERE user_id = to_user;
COMMIT;
SELECT 'Transfer successful' AS message;
END IF;
END //
DELIMITER ;
4. 避免事务中的远程调用
-- ❌ 反面:事务中调用外部API
BEGIN;
UPDATE orders SET status = 'paid' WHERE id = 1;
-- 远程调用支付接口(网络延迟大)
COMMIT; -- 锁等待时间过长
-- ✅ 正面:先更新状态,异步调用
BEGIN;
UPDATE orders SET status = 'paying' WHERE id = 1;
COMMIT;
-- 异步调用支付API,成功后
UPDATE orders SET status = 'paid' WHERE id = 1;
事务的陷阱与注意事项
1. 混合使用存储引擎
-- MyISAM 表不遵守事务语义
START TRANSACTION;
UPDATE accounts SET balance = balance - 100; -- InnoDB,事务性
INSERT INTO logs VALUES (NOW(), 'withdraw'); -- MyISAM,非事务性
COMMIT;
-- 第一个成功,第二个失败,无法一起回滚!
建议:所有表使用相同的存储引擎(InnoDB)
2. DDL语句的隐式提交
某些语句会自动提交当前事务:
START TRANSACTION;
INSERT INTO users VALUES (1, 'Alice');
ALTER TABLE users ADD COLUMN phone VARCHAR(20); -- 自动提交!
-- Alice 的插入已经提交,ALTER TABLE 前的事务已结束
3. 锁等待与死锁
长事务会长时间持有锁,增加死锁风险。
-- Session A
BEGIN;
UPDATE users SET name = 'A' WHERE id = 1;
-- 长时间持有锁
-- Session B
BEGIN;
UPDATE orders SET status = 'new' WHERE user_id = 1;
UPDATE users SET name = 'B' WHERE id = 1; -- 等待 A 释放锁
-- 潜在死锁风险
隔离级别配置
配置服务器默认级别
[mysqld]
transaction-isolation = REPEATABLE-READ
配置会话级别
-- 仅当前会话
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 全局(对新连接生效)
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
小结
事务是数据库一致性的守护者。
核心要点:
- ACID四原则:原子性、一致性、隔离性、持久性
- 四种隔离级别:从 READ UNCOMMITTED 到 SERIALIZABLE
- MySQL默认是REPEATABLE READ:InnoDB通过MVCC解决幻读
- 事务日志是持久性的保证:WAL机制
- 短事务原则:减少锁持有时间,降低死锁风险
- 不要混合存储引擎:保持事务语义一致
实践建议:
- 显式使用事务(START TRANSACTION)
- 保持事务简短
- 避免在事务中执行外部调用
- 合理选择隔离级别
- 监控长事务和死锁
延伸阅读:
- 《高性能MySQL(第4版)》第1章 - 事务部分
- MySQL 8.0 Reference Manual - Transaction Management
- InnoDB Transaction Model