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

场景引入

你在银行转账时,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';

注意事项

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

小结

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

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

相关推荐
面试鸭1 小时前
Anthropic 让 Claude 接了 CI 值班的第一班,人剩下的是写规则和合 PR
后端·面试
小强19881 小时前
useRef 到底用来干什么?不止获取 DOM 元素
后端
大勇前进1 小时前
React Hooks 常见 5 大坑:写完代码疯狂 Bug,避坑总结
后端
颜进强1 小时前
04 - 一张图看懂 OpenSpec 变更的一生:从创建到归档
前端·后端·ai编程
Cobyte1 小时前
Claude Code 的 Task System 的实现原理
后端·aigc·ai编程
小强19881 小时前
useEffect 完整使用指南:依赖数组、闭包陷阱、清理函数实战
后端
智驭未来掌门人1 小时前
在windows下快速搭建go2rtc流媒体平台
后端
大白802 小时前
为什么不要滥用 useMemo 和 useCallback?过度优化反而更卡
后端
神奇小汤圆2 小时前
Codex 子 Agent 配置指南:让 Sol 当军师,Luna 当搬砖工
后端