📊 SQL 入门 Day 19:事务与隔离级别

12 阅读6分钟

场景引入

你在银行转账时,A 账户扣钱、B 账户加钱——如果扣完钱服务器宕机,钱就凭空消失了。事务(Transaction)就是解决这类问题的方案:要么全部成功,要么全部回滚。

另一个常见场景:同时有 10 个人在抢票,有人读到"还剩 1 张"时另一人已付款,结果两人都买到同一张票——这叫"脏读"或"不可重复读",事务的隔离级别就是用来控制这种并发的。


一、什么是事务?

事务是一组 SQL 操作,作为一个不可分割的工作单元执行。

ACID 四大特性

特性含义通俗理解
Atomicity 原子性事务内的操作要么全做,要么全不做"全有或全无"
Consistency 一致性事务前后数据状态必须合法转账前后总额不变
Isolation 隔离性并发事务互不干扰"你改你的,我读我的"
Durability 持久性提交后修改永久保存即使断电也不丢

二、事务基本操作

语法

-- 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;

完整示例

-- 建表
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;

执行结果:

+----+-------+---------+
| id | name  | balance |
+----+-------+---------+
|  1 | Alice | 1000.00 |  ← 没有变化,回滚生效
|  2 | Bob   | 500.00  |
+----+-------+---------+

SAVEPOINT(保存点)

可以在事务中设置回滚点,只撤销部分操作:

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 条

四种隔离级别(由低到高)

-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- MySQL 8.0
SHOW VARIABLES LIKE 'transaction_isolation';
级别脏读不可重复读幻读性能
READ UNCOMMITTED✅ 可能✅ 可能✅ 可能⭐⭐⭐⭐⭐
READ COMMITTED❌ 避免✅ 可能✅ 可能⭐⭐⭐⭐
REPEATABLE READ(MySQL 默认)❌ 避免❌ 避免✅ 可能(InnoDB 通过 MVCC 解决)⭐⭐⭐
SERIALIZABLE❌ 避免❌ 避免❌ 避免⭐

设置隔离级别

-- 全局(影响新连接)
SET GLOBAL transaction_isolation = 'REPEATABLE-READ';

-- 会话(仅当前连接)
SET SESSION transaction_isolation = 'READ-COMMITTED';

-- 下一个事务
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

隔离级别实战演示

脏读演示(READ UNCOMMITTED)

-- 会话 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)

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 能同时保证"不脏读"和"不幻读"(大部分场景)
-- 查看 InnoDB 事务状态
SELECT * FROM information_schema.INNODB_TRX\G

-- 查看当前正在运行的事务
SELECT trx_id, trx_state, trx_started, trx_isolation_level
FROM information_schema.INNODB_TRX;

五、事务最佳实践

✅ DO

-- 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

-- 1. 事务中混入无关查询
START TRANSACTION;
  SELECT * FROM logs;       -- ❌ 无意义的读操作增加锁持有时间
  UPDATE accounts SET ...;
COMMIT;

-- 2. 事务内做大量循环
START TRANSACTION;
  -- 循环 10000 次插入  ❌ 锁持有时间过长
COMMIT;

-- 3. 事务中包含用户交互(先查再让用户确认)
START TRANSACTION;
  SELECT balance FROM accounts;  -- 用户去泡咖啡...
  -- 10 分钟后才提交  ❌ 长事务!
COMMIT;

常见错误与排查

-- 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';

注意事项

  1. MySQL 默认是自动提交:每条 DML 语句单独一个事务,SHOW VARIABLES LIKE 'autocommit' 查看,SET autocommit = 0 关闭
  2. DDL 语句无法回滚:CREATE TABLE、ALTER TABLE、DROP TABLE 会隐式提交当前事务
  3. 长事务是性能杀手:长时间持有锁 → 阻塞其他请求 → 积累大量 undo log → 磁盘爆炸
  4. SERIALIZABLE 级别性能最差:实际生产几乎不用,除非对一致性要求极端严苛
  5. REPEATABLE READ 下 InnoDB 用间隙锁(Gap Lock)防幻读:锁住索引范围而非单行,但可能造成更多锁冲突

小结

概念一句话
事务一组 SQL,要么全成功要么全失败
ACID原子性、一致性、隔离性、持久性
COMMIT确认修改
ROLLBACK撤销修改
脏读读到未提交的数据
不可重复读同一行两次读不一样
幻读同一查询两次结果行数不同
MVCCInnoDB 实现隔离的秘密武器

理解事务与隔离级别是进阶数据库开发的核心——它能帮你写出正确的并发代码,也能帮你排查棘手的死锁问题。🔥