场景引入
你在银行转账时,A 账户扣钱、B 账户加钱------如果扣完钱服务器宕机,钱就凭空消失了。事务(Transaction)就是解决这类问题的方案:要么全部成功,要么全部回滚。
另一个常见场景:同时有 10 个人在抢票,有人读到"还剩 1 张"时另一人已付款,结果两人都买到同一张票------这叫"脏读"或"不可重复读",事务的隔离级别就是用来控制这种并发的。
一、什么是事务?
事务是一组 SQL 操作,作为一个不可分割的工作单元执行。
ACID 四大特性
| 特性 | 含义 | 通俗理解 |
|---|---|---|
| Atomicity 原子性 | 事务内的操作要么全做,要么全不做 | "全有或全无" |
| Consistency 一致性 | 事务前后数据状态必须合法 | 转账前后总额不变 |
| Isolation 隔离性 | 并发事务互不干扰 | "你改你的,我读我的" |
| Durability 持久性 | 提交后修改永久保存 | 即使断电也不丢 |
二、事务基本操作
语法
sql
-- 1. 开启事务
START TRANSACTION;
-- 或 BEGIN;
-- 2. 执行操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 3. 提交(确认生效)
COMMIT;
-- 4. 或回滚(撤销操作)
ROLLBACK;
完整示例
sql
-- 建表
CREATE TABLE accounts (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
balance DECIMAL(10,2)
);
INSERT INTO accounts VALUES
(1, 'Alice', 1000.00),
(2, 'Bob', 500.00);
-- 转账事务
START TRANSACTION;
UPDATE accounts SET balance = balance - 200 WHERE id = 1;
UPDATE accounts SET balance = balance + 200 WHERE id = 2;
-- 假设此时检查到余额不足或网络异常
ROLLBACK; -- 回滚,两边都不变
SELECT * FROM accounts;
执行结果:
diff
+----+-------+---------+
| id | name | balance |
+----+-------+---------+
| 1 | Alice | 1000.00 | ← 没有变化,回滚生效
| 2 | Bob | 500.00 |
+----+-------+---------+
SAVEPOINT(保存点)
可以在事务中设置回滚点,只撤销部分操作:
sql
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
SAVEPOINT sp1; -- 设置保存点
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- 发现不对,只回滚到 sp1
ROLLBACK TO SAVEPOINT sp1;
COMMIT; -- 只提交了第一个 UPDATE(扣了 100),第二个被撤销
三、事务隔离级别
为什么需要隔离级别?
并发事务可能产生 3 种问题:
| 问题 | 描述 | 举个🌰 |
|---|---|---|
| 脏读 | 读到另一个事务未提交的数据 | A 改数据未提交,B 读到"新值",A 又回滚了 |
| 不可重复读 | 同一事务内两次读同一行,结果不同 | B 第一次读 100,A 修改并提交,B 再读变成 200 |
| 幻读 | 同一条件查询,两次返回行数不同 | B 查询 10 条,A 插入 1 条提交,B 再查变 11 条 |
四种隔离级别(由低到高)
sql
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- MySQL 8.0
SHOW VARIABLES LIKE 'transaction_isolation';
| 级别 | 脏读 | 不可重复读 | 幻读 | 性能 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✅ 可能 | ✅ 可能 | ✅ 可能 | ⭐⭐⭐⭐⭐ |
| READ COMMITTED | ❌ 避免 | ✅ 可能 | ✅ 可能 | ⭐⭐⭐⭐ |
| REPEATABLE READ(MySQL 默认) | ❌ 避免 | ❌ 避免 | ✅ 可能(InnoDB 通过 MVCC 解决) | ⭐⭐⭐ |
| SERIALIZABLE | ❌ 避免 | ❌ 避免 | ❌ 避免 | ⭐ |
设置隔离级别
sql
-- 全局(影响新连接)
SET GLOBAL transaction_isolation = 'REPEATABLE-READ';
-- 会话(仅当前连接)
SET SESSION transaction_isolation = 'READ-COMMITTED';
-- 下一个事务
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
隔离级别实战演示
脏读演示(READ UNCOMMITTED)
sql
-- 会话 A
SET SESSION transaction_isolation = 'READ UNCOMMITTED';
START TRANSACTION;
UPDATE accounts SET balance = 1000000 WHERE id = 1;
-- 不提交
-- 会话 B(读到 A 未提交的数据)
SET SESSION transaction_isolation = 'READ UNCOMMITTED';
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;
-- 结果:1000000 ← 脏读!A 可能回滚
-- 会话 A
ROLLBACK;
-- 会话 B 再查:又变回 1000.00 ------ 读到的是不存在的数据
避免脏读(READ COMMITTED)
sql
SET SESSION transaction_isolation = 'READ COMMITTED';
-- 会话 A 修改但不提交
START TRANSACTION;
UPDATE accounts SET balance = 999999 WHERE id = 1;
-- 未提交
-- 会话 B 读取
SELECT balance FROM accounts WHERE id = 1;
-- 结果:1000.00 ← 读不到未提交的数据,脏读被避免
四、InnoDB 如何实现隔离?
MySQL InnoDB 使用 MVCC(多版本并发控制) + 锁 来实现隔离:
- MVCC:每行数据维护多个版本,不同事务看到不同快照
- REPEATABLE READ 级别下,事务启动时创建一致性读视图(Read View),整个事务期间使用同一快照,实现可重复读
- 这是为什么 MySQL 默认的 REPEATABLE READ 能同时保证"不脏读"和"不幻读"(大部分场景)
sql
-- 查看 InnoDB 事务状态
SELECT * FROM information_schema.INNODB_TRX\G
-- 查看当前正在运行的事务
SELECT trx_id, trx_state, trx_started, trx_isolation_level
FROM information_schema.INNODB_TRX;
五、事务最佳实践
✅ DO
sql
-- 1. 显式控制事务边界
START TRANSACTION;
-- 业务逻辑
COMMIT;
-- 2. 设置合理超时(防止长事务阻塞)
SET SESSION innodb_lock_wait_timeout = 5; -- 默认 50 秒
-- 3. 事务中错误时主动回滚
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 如果第二步出错
ROLLBACK;
COMMIT;
❌ DON'T
sql
-- 1. 事务中混入无关查询
START TRANSACTION;
SELECT * FROM logs; -- ❌ 无意义的读操作增加锁持有时间
UPDATE accounts SET ...;
COMMIT;
-- 2. 事务内做大量循环
START TRANSACTION;
-- 循环 10000 次插入 ❌ 锁持有时间过长
COMMIT;
-- 3. 事务中包含用户交互(先查再让用户确认)
START TRANSACTION;
SELECT balance FROM accounts; -- 用户去泡咖啡...
-- 10 分钟后才提交 ❌ 长事务!
COMMIT;
常见错误与排查
sql
-- 1. 忘记提交导致锁等待
-- 查看被阻塞的事务
SELECT * FROM sys.innodb_lock_waits;
-- 2. 杀掉阻塞的会话
KILL <thread_id>;
-- 3. 查看事务持续时间(过长的事务要优化)
SELECT trx_id, trx_started,
TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS trx_seconds
FROM information_schema.INNODB_TRX
WHERE trx_state = 'RUNNING';
注意事项
- MySQL 默认是自动提交 :每条 DML 语句单独一个事务,
SHOW VARIABLES LIKE 'autocommit'查看,SET autocommit = 0关闭 - DDL 语句无法回滚 :
CREATE TABLE、ALTER TABLE、DROP TABLE会隐式提交当前事务 - 长事务是性能杀手:长时间持有锁 → 阻塞其他请求 → 积累大量 undo log → 磁盘爆炸
- SERIALIZABLE 级别性能最差:实际生产几乎不用,除非对一致性要求极端严苛
- REPEATABLE READ 下 InnoDB 用间隙锁(Gap Lock)防幻读:锁住索引范围而非单行,但可能造成更多锁冲突
小结
| 概念 | 一句话 |
|---|---|
| 事务 | 一组 SQL,要么全成功要么全失败 |
| ACID | 原子性、一致性、隔离性、持久性 |
| COMMIT | 确认修改 |
| ROLLBACK | 撤销修改 |
| 脏读 | 读到未提交的数据 |
| 不可重复读 | 同一行两次读不一样 |
| 幻读 | 同一查询两次结果行数不同 |
| MVCC | InnoDB 实现隔离的秘密武器 |
理解事务与隔离级别是进阶数据库开发的核心------它能帮你写出正确的并发代码,也能帮你排查棘手的死锁问题。🔥