📖 本篇简介:本篇收拾的是"事务能用,但一上并发就出事"那类坑——长事务、死锁、隔离级别选错、乐观锁误用、事务里套外部调用。这些问题在单机低并发时永远不会暴露,一旦用户量上来,就会以锁等待、死锁回滚、主从延迟、数据不一致的形式集中爆发。前置知识是你已经学过 ACID、四种隔离级别、redo/undo log 和 InnoDB 锁的基本概念。学完之后,你应该能设计一个不会死锁的事务、判断隔离级别该选 RC 还是 RR,并看懂死锁日志。本系列所有示例基于 MySQL 8.0 语法书写,本示例基于 MySQL 8.0 环境。
目录
- 坑 1:长事务占着连接和 undo log 不放
- 坑 2:两个事务交叉加锁顺序导致死锁
- 坑 3:隔离级别选错
- 坑 4:乐观锁 version 字段没正确用
- 坑 5:事务里包 rpc 和外部调用
- 坑 6:误以为 autocommit 默认行为
- 坑 7:间隙锁导致插入莫名阻塞
- 坑 8:异常后忘记 rollback
- 坑 9:快照读和当前读混淆
- 坑 10:并发插入唯一键重复报错
- 坑 11:大事务导致 undo 膨胀
- 坑 12:rr 下幻读没真的消失
- 速查表:事务并发坑点速查
- 注意事项:设计事务时的几条铁律
- 练习题:动手自测与参考
坑 1:长事务占着连接和 undo log 不放
场景 / 错误示范:
-- 错误示范:一个事务里做了一堆事,还等人确认
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
-- 中间去调了一个第三方接口(耗时 5 秒)
-- 又去查了一大堆数据做计算
-- 用户在页面上点"确认"之前,事务一直挂着
COMMIT;
-- 这个事务持有的行锁、undo log 版本链一直不释放
-- purge 线程没法清理旧版本,undo 表空间膨胀
-- 其他事务来改这些行,全部排队等锁
为什么是坑:InnoDB 的 MVCC 靠 undo log 保存历史版本。一个事务不提交,它看到的版本链就不能被 purge 清理。事务越长,undo 链越长,其他事务的一致性读也要顺着更长的版本链找,性能雪崩。同时持有的行锁一直不释放,并发能力直接腰斩。
正确做法:
-- 正确做法:事务尽量短,只包数据库操作本身
-- 反例:把 RPC、用户交互、批量计算都踢出事务
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 毫秒级完成
-- 查一下当前有没有长事务
SELECT trx_id, trx_started, trx_query
FROM information_schema.innodb_trx
WHERE TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 5;
-- 超过 5 秒还没提交的事务,就该警惕
加分点:设置 innodb_lock_wait_timeout=50,并监控 innodb_trx 里事务持续时间。发现长事务先 Kill,别等它自己结束。事务里绝对不要放用户等待、RPC、消息发送。
坑 2:两个事务交叉加锁顺序导致死锁
场景 / 错误示范:
-- 事务 A:先扣用户 1 的钱,再扣用户 2 的钱
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance - 100 WHERE id = 2;
COMMIT;
-- 事务 B:同时转账,顺序相反
START TRANSACTION;
UPDATE account SET balance = balance + 100 WHERE id = 2;
UPDATE account SET balance = balance + 100 WHERE id = 1;
COMMIT;
-- A 等 B 释放 id=2 的锁,B 等 A 释放 id=1 的锁,死锁!
-- InnoDB 检测到后随机回滚一个事务
为什么是坑:两个事务以不同顺序锁定同一组行,就会循环等待。死锁不是 MySQL 的 bug,是业务逻辑的加锁顺序不统一。你在代码里怎么写加锁顺序,数据库就怎么等;只要顺序不一致,并发一上来必然死锁。
正确做法:
-- 正确做法:所有事务按统一顺序加锁(比如按 id 升序)
-- 转账:永远先锁 id 小的账户,再锁 id 大的
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = LEAST(1, 2);
UPDATE account SET balance = balance + 100 WHERE id = GREATEST(1, 2);
COMMIT;
-- 两个事务都按 id 升序,要么都等 id=1,不会循环等待
-- 代码层:把"要操作的账户 id 列表"先排序,再依次 UPDATE
-- 死锁发生后,MySQL 会回滚其中一个事务,应用要捕获错误并重试
-- 1213 错误码就是死锁
-- 查看最近死锁日志:
SHOW ENGINE INNODB STATUS;
-- 找到 LATEST DETECTED DEADLOCK 段,看两个事务各持什么锁、等什么锁
加分点:除了统一加锁顺序,事务里操作的行越少越好(减少锁范围),并在代码里捕获 1213 错误做指数退避重试。重试时加锁顺序仍然一致,就不会再次死锁。
坑 3:隔离级别选错
场景 / 错误示范:
-- 错误示范:不知道默认隔离级别,也不设置
-- MySQL 8.0 默认是 REPEATABLE READ(RR)
-- 业务其实只需要 READ COMMITTED(RC),结果间隙锁一堆,并发插入阻塞
SELECT @@transaction_isolation; -- 默认 REPEATABLE-READ
为什么是坑:隔离级别越高,一致性越好,但并发越低。RR 级别下 InnoDB 用 Next-Key Lock 防止幻读,会锁住索引区间,导致其他事务插入被阻塞。很多互联网公司(包括早期阿里)把线上隔离级别降到 RC,就是为了减少锁范围、提升并发。选错级别要么性能差,要么数据一致性出问题。
正确做法:
-- 正确做法:根据业务选择,互联网高并发一般用 RC
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
-- 写进配置
-- [mysqld]
-- transaction_isolation = READ-COMMITTED
-- RC 级别下:
-- 1. 没有间隙锁(只有记录锁),插入不阻塞
-- 2. 每次快照读读最新已提交版本,业务更直观
-- 3. binlog 必须用 row 格式(RC 下 statement 复制会丢数据)
# my.cnf 配套
[mysqld]
transaction_isolation = READ-COMMITTED
binlog_format = ROW
加分点:RC 不是银弹——它会让同一个事务里两次读同一行得到不同结果(不可重复读)。如果业务严格要求"事务内多次读一致",还是要 RR。选之前把业务一致性需求讲清楚。
坑 4:乐观锁 version 字段没正确用
场景 / 错误示范:
-- 表:商品库存,加了 version 字段做乐观锁
CREATE TABLE product (
id BIGINT PRIMARY KEY,
stock INT,
version INT
);
-- 错误示范:更新时忘记带 version 条件
UPDATE product SET stock = stock - 1 WHERE id = 1;
-- 并发下两个事务都扣成功,库存扣成负数
-- 或者:带了 version,但没检查 affected rows
UPDATE product SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;
-- affected rows=0 说明被别人改了,代码却没判断,继续往下走
为什么是坑:乐观锁的本质是"更新时带版本号条件,更新失败就重试"。忘了带 version 条件,乐观锁就等于没有;带了却不检查影响行数,等于没重试,业务照样错。乐观锁不是加个字段就完事,是一整套"读版本→更新→失败重试"的模式。
正确做法:
-- 正确做法:读取当前版本,更新时带版本条件,检查影响行数
-- 第一步:读出库存和版本
SELECT stock, version FROM product WHERE id = 1; -- 假设 version=5
-- 第二步:更新,带版本条件
UPDATE product
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;
-- affected rows=1 成功;=0 说明版本变了,要重试
-- 代码里:
-- if (affectedRows == 0) { 重新读 version,重试 N 次 }
# Python 伪代码示意乐观锁重试
import pymysql
conn = pymysql.connect(host='127.0.0.1', user='root', password='***', database='shop')
for attempt in range(3):
cur = conn.cursor()
cur.execute("SELECT stock, version FROM product WHERE id=1")
stock, ver = cur.fetchone()
if stock <= 0:
raise Exception('库存不足')
cur.execute(
"UPDATE product SET stock=stock-1, version=version+1 WHERE id=1 AND version=%s",
(ver,)
)
conn.commit()
if cur.rowcount == 1:
break # 成功
# rowcount=0 版本冲突,循环重试
conn.close()
加分点:高并发扣库存用乐观锁冲突率高(同一商品多人抢),不如用 UPDATE ... SET stock = stock - 1 WHERE id=1 AND stock > 0 这种原子扣减,让数据库保证不超卖,不用自己管 version。
坑 5:事务里包 rpc 和外部调用
场景 / 错误示范:
# 错误示范:事务里调用了短信服务、积分服务
conn.begin()
conn.execute("UPDATE orders SET status=1 WHERE id=%s", order_id)
# 事务还没提交,去调一个耗时的外部接口
sms_client.send(order.mobile, '下单成功') # 网络抖动卡 3 秒
point_client.add(order.user_id, 100) # 又卡 2 秒
conn.commit()
# 整个事务持锁 5 秒,并发全堵在这
为什么是坑:事务持有的锁,要到 COMMIT 才释放。你在事务里调 RPC,网络一抖动,锁就被多占几秒。这段时间里,其他事务想改这些行全部排队。外部服务越慢,数据库锁持有时间越长,雪崩越快。
正确做法:
# 正确做法:事务只包数据库操作,外部调用移到事务外
conn.begin()
conn.execute("UPDATE orders SET status=1 WHERE id=%s", order_id)
conn.commit() # 立刻提交,锁马上释放
# 事务提交后,再做外部调用(可以用消息队列异步化)
sms_client.send(order.mobile, '下单成功')
point_client.add(order.user_id, 100)
# 失败也不影响主事务,靠本地消息表或重试机制补偿
加分点:跨服务一致性别靠"一个大事务包到底",那是分布式事务的活儿。本地事务管数据库,外部调用走 MQ + 本地消息表,最终一致比强一致在互联网场景更现实。
坑 6:误以为 autocommit 默认行为
场景 / 错误示范:
-- 错误示范:以为每条 SQL 都自动在事务里
-- 其实 MySQL 默认 autocommit=1,每条语句自己就是一个事务
-- 程序员写了三条 UPDATE,以为是一个原子事务,实际是三个独立事务
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
-- 第一条成功第二条失败,钱就没了,没法回滚
为什么是坑:MySQL 默认 autocommit 开启,单条语句自动提交。你不显式 BEGIN/START TRANSACTION,多条 SQL 就不是一个原子操作。中间任何一条失败,前面的已经提交,数据不一致。这个坑在面试和生产都出现过无数次。
正确做法:
-- 正确做法:需要原子性的操作,显式开事务
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE id = 1;
UPDATE account SET balance = balance + 100 WHERE id = 2;
COMMIT; -- 要么全成功,要么全回滚
-- 查一下当前 autocommit
SELECT @@autocommit; -- 1 表示自动提交
# Python 里用连接的事务上下文,别依赖默认 autocommit
conn.begin() # 显式开事务
try:
cur.execute("UPDATE account SET balance=balance-100 WHERE id=1")
cur.execute("UPDATE account SET balance=balance+100 WHERE id=2")
conn.commit()
except Exception:
conn.rollback()
raise
加分点:在 Spring 里用 @Transactional 注解,框架帮你管事务边界;但要注意它只对 RuntimeException 回滚,checked exception 默认不回滚,记得配 rollbackFor = Exception.class。
坑 7:间隙锁导致插入莫名阻塞
场景 / 错误示范:
-- 表:id 有索引,现在有 id=1, 5, 10 三行
CREATE TABLE t (id INT PRIMARY KEY);
INSERT INTO t VALUES(1),(5,10);
-- 事务 A(RR 级别):范围查询
START TRANSACTION;
SELECT * FROM t WHERE id BETWEEN 5 AND 8 FOR UPDATE;
-- 这会锁住 (5, 10) 这个间隙!
-- 事务 B:想插入 id=7
INSERT INTO t VALUES(7);
-- 阻塞!要等事务 A 提交
为什么是坑:RR 级别下,InnoDB 用 Next-Key Lock(记录锁 + 间隙锁)防止幻读。范围查询不只是锁住存在的行,还锁住行之间的间隙。其他事务想往这个间隙插数据,就会被挡住。这个行为在 RC 级别下没有,所以很多人切到 RC 后恍然大悟。
正确做法:
-- 正确做法一:业务允许就切到 RC,去掉间隙锁
SET GLOBAL transaction_isolation = 'READ-COMMITTED';
-- 正确做法二:必须在 RR 下,就缩小锁定范围,用等值查询代替范围
-- 等值查询命中唯一索引,只加记录锁,不加间隙锁
SELECT * FROM t WHERE id = 5 FOR UPDATE; -- 只锁 id=5
-- 正确做法三:监控锁等待,发现长等就 Kill 并优化 SQL
SELECT * FROM information_schema.innodb_lock_waits;
加分点:死锁和锁等待日志(SHOW ENGINE INNODB STATUS)里看到 gap lock 就是间隙锁在起作用。想避开它,要么切 RC,要么让查询尽量走唯一索引等值定位。
坑 8:异常后忘记 rollback
场景 / 错误示范:
# 错误示范:代码里抛异常,但没 rollback
conn.begin()
try:
cur.execute("UPDATE account SET balance=balance-100 WHERE id=1")
# 某行抛异常(比如除零)
1 / 0
cur.execute("UPDATE account SET balance=balance+100 WHERE id=2")
conn.commit()
except Exception:
pass # 吞了异常,也没 rollback
# 事务还挂在那,连接被归还到连接池,下一个使用者接着用,数据错乱
为什么是坑:异常后不显式 rollback,事务就一直挂着,持有的锁不释放。连接被归还到池子里时,事务状态还在,下一个业务拿到这个连接,可能不小心把半个事务 COMMIT 了,造成数据错乱。这是连接池使用里最经典的坑。
正确做法:
# 正确做法:finally 里确保 rollback(或用上下文管理器)
conn.begin()
try:
cur.execute("UPDATE account SET balance=balance-100 WHERE id=1")
cur.execute("UPDATE account SET balance=balance+100 WHERE id=2")
conn.commit()
except Exception:
conn.rollback() # 异常必须回滚
raise
# 连接池也要配置:回收连接时自动 rollback 未提交事务
// Spring 的 @Transactional 已经帮你做了这件事
// 但如果你手动管理连接,try-catch-finally 里务必 rollback
加分点:连接池(HikariCP)建议配置 connectionTestQuery=SELECT 1 和归还连接时检查事务状态,发现未提交自动回滚。别把脏连接还给业务。
坑 9:快照读和当前读混淆
场景 / 错误示范:
-- 事务 A(RR 级别)
START TRANSACTION;
SELECT balance FROM account WHERE id = 1; -- 快照读,读到 100
-- 事务 B 同时把 balance 改成 200 并提交
-- 事务 A 再次普通 SELECT:
SELECT balance FROM account WHERE id = 1; -- 还是 100(快照读)
-- 但事务 A 想基于最新值更新:
UPDATE account SET balance = balance - 50 WHERE id = 1;
-- 这条 UPDATE 是当前读,会读到 B 提交后的 200,扣完变 150
-- 程序员以为还是基于 100,实际基于 200,逻辑错了
为什么是坑:普通 SELECT 是快照读(读事务开始时的版本),但 UPDATE/DELETE/SELECT ... FOR UPDATE/LOCK IN SHARE MODE 是当前读(读最新已提交版本)。在同一个事务里,这两种读看到的数据可能不一样。搞混了就会在代码里算出错误的业务值。
正确做法:
-- 正确做法一:需要最新值,就用当前读,别用普通 SELECT
START TRANSACTION;
SELECT balance FROM account WHERE id = 1 FOR UPDATE; -- 当前读,加锁
-- 之后基于这个最新值计算
UPDATE account SET balance = balance - 50 WHERE id = 1;
COMMIT;
-- 正确做法二:用 RC 级别,普通 SELECT 也读最新版本,少踩这个坑
-- 正确做法三:业务上意识到"快照读 vs 当前读"差异,
-- 写代码时想清楚这一步到底要读哪个版本
加分点:面试常问:"RR 下同一个事务两次 SELECT 结果一样吗?"答案是普通 SELECT 一样(快照),FOR UPDATE 不一样(当前读)。能讲清这个,就比背八股的强一档。
坑 10:并发插入唯一键重复报错
场景 / 错误示范:
-- 表:phone 上有唯一索引
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
phone VARCHAR(20) UNIQUE
);
-- 并发场景:两个请求同时给同一个手机号注册
INSERT INTO user(phone) VALUES('13800001111');
-- 一个成功,另一个报 ERROR 1062 Duplicate entry
-- 应用直接 500 给用户
为什么是坑:唯一索引保证同一时刻只有一个插入成功,另一个必然冲突。这是数据库在保护你,但应用如果不处理这个错误,用户就会看到一个吓人的 500。高并发下"先 SELECT 再 INSERT"两步之间有窗口,照样会撞唯一键。
正确做法:
-- 正确做法一:插入撞唯一键时,改成"已存在"提示或做更新
INSERT INTO user(phone) VALUES('13800001111');
-- 捕获 1062 错误,转成"该手机号已注册"
-- 正确做法二:用 INSERT ... ON DUPLICATE KEY UPDATE(8.0 也叫 upsert)
INSERT INTO user(phone, name) VALUES('13800001111', '张三')
ON DUPLICATE KEY UPDATE name = VALUES(name);
-- 存在就更新,不存在就插入,原子完成
-- 正确做法三:或用 INSERT IGNORE(忽略重复错误,不报错)
INSERT IGNORE INTO user(phone) VALUES('13800001111');
加分点:ON DUPLICATE KEY UPDATE 在高并发下有坑:它会给行加间隙锁,可能导致死锁。简单场景用它,复杂并发还是老老实实捕获 1062 错误再查一次。
坑 11:大事务导致 undo 膨胀
场景 / 错误示范:
-- 错误示范:一个事务里更新了 100 万行
START TRANSACTION;
UPDATE orders SET status = 2 WHERE create_time < '2026-01-01';
-- 100 万行的旧版本都要写到 undo log
-- undo 表空间暴涨,ibdata1 撑大,回滚段不够用
-- 执行 10 分钟还没完,其他事务的 undo 查询变慢
COMMIT;
为什么是坑:InnoDB 的 undo log 记录每行被修改前的版本,用于回滚和 MVCC 一致性读。一个大事务修改几百万行,undo log 就写几百万份,不仅占空间,还让 purge 线程跟不上。事务回滚时,还要把这几百万份反向重做,回滚时间可能比执行时间还长。
正确做法:
-- 正确做法:拆成小批量事务,每批 1000 行
-- 循环执行:
START TRANSACTION;
UPDATE orders SET status = 2
WHERE create_time < '2026-01-01'
LIMIT 1000;
COMMIT;
-- 反复跑,直到 affected rows=0
-- 每批提交后 undo 就能被 purge,磁盘不暴涨
-- 监控 undo 状态
SELECT * FROM information_schema.innodb_metrics
WHERE name LIKE 'trx%undo%';
-- 或者看 undo 表空间大小
SHOW VARIABLES LIKE 'innodb_undo%';
加分点:大表做数据订正、历史数据清理,永远走小批量 + 脚本循环,别指望一条 SQL 干完。生产库的 undo 表空间暴涨后很难回缩(8.0 支持 truncate undo 日志文件,但要配置)。
坑 12:rr 下幻读没真的消失
场景 / 错误示范:
-- 错误示范:以为 RR 级别完全解决了幻读
-- 事务 A(RR):
START TRANSACTION;
SELECT * FROM t WHERE id BETWEEN 1 AND 10; -- 查到 5 行
-- 事务 B 插入一行 id=7 并提交
INSERT INTO t VALUES(7, 'x');
COMMIT;
-- 事务 A 再用普通 SELECT:
SELECT * FROM t WHERE id BETWEEN 1 AND 10; -- 还是 5 行(快照读,没幻读)
-- 但事务 A 想更新这个范围:
UPDATE t SET name = 'y' WHERE id BETWEEN 1 AND 10;
-- 会把事务 B 刚插入的 id=7 也更新了!再 SELECT 就变成 6 行
-- 这就是"幻读"在当前读场景下还是出现了
为什么是坑:MySQL 的 RR 级别只解决了"快照读"的幻读(普通 SELECT),没解决"当前读"的幻读(UPDATE/DELETE/FOR UPDATE)。很多教程说 RR 解决幻读,其实是有前提的。你在事务里先快照读再当前读,还是会看到新插入的行。
正确做法:
-- 正确做法:需要避免当前读幻读,就用 SELECT ... FOR UPDATE 加范围锁
START TRANSACTION;
SELECT * FROM t WHERE id BETWEEN 1 AND 10 FOR UPDATE;
-- 这条当前读会加 Next-Key Lock,阻止其他事务在这个范围插入
-- 之后再做 UPDATE,就不会幻读
UPDATE t SET name = 'y' WHERE id BETWEEN 1 AND 10;
COMMIT;
-- 或者干脆业务上接受"最终一致",用 RC + 乐观锁,别在 RR 里死磕幻读
加分点:这个点是 MySQL 面试高频题。标准答案:"RR 级别通过 MVCC 解决快照读幻读,通过 Next-Key Lock 解决当前读幻读,但如果先快照读后当前读,幻读仍可能出现。" 能讲到这一层就够了。
速查表:事务并发坑点速查
| 坑号 | 坑点 | 典型症状 | 修法 |
|---|---|---|---|
| 1 | 长事务 | undo 膨胀、锁不释放 | 事务只包 DB 操作,监控长事务 |
| 2 | 加锁顺序不一致 | 死锁 1213 | 统一按主键升序加锁 |
| 3 | 隔离级别选错 | 间隙锁一堆 | 高并发选 RC + row binlog |
| 4 | 乐观锁忘查版本 | 并发覆盖更新 | 带 version 条件 + 检查 rowcount |
| 5 | 事务里调 RPC | 锁持有秒级 | 外部调用移到事务外,走 MQ |
| 6 | 忘显式开事务 | 多条 SQL 非原子 | BEGIN/COMMIT,Spring 用 @Transactional |
| 7 | 间隙锁 | 插入莫名阻塞 | 切 RC 或唯一等值定位 |
| 8 | 异常忘 rollback | 脏连接回池 | except 里 rollback,连接池兜底 |
| 9 | 快照读 vs 当前读 | 算出错误值 | 需要最新值用 FOR UPDATE |
| 10 | 并发唯一键冲突 | 1062 报错 | 捕获异常或 ON DUPLICATE KEY UPDATE |
| 11 | 大事务 undo 暴涨 | ibdata 撑大 | 小批量 LIMIT 1000 循环 |
| 12 | RR 下当前读幻读 | 先快照后当前读不一致 | 范围 FOR UPDATE 或接受 RC |
注意事项:设计事务时的几条铁律
1. 事务越短越好:只包数据库读写,RPC、HTTP、用户交互、文件 IO 全部移出事务。
2. 加锁顺序统一:所有事务操作多行时,按主键升序加锁,避免循环等待。
3. 批量写拆小:一次更新别超过 1000~5000 行,循环提交。
4. 异常必须回滚:代码层 try-catch-finally,连接池层兜底检查。
5. 隔离级别想清楚:互联网高并发默认 RC,强一致需求才 RR。
6. 死锁要重试:捕获 1213 错误,按相同加锁顺序重试,别直接给用户报错。
把这 6 条写进团队的数据库规范,能挡掉生产环境 80% 的并发事故。
练习题:动手自测与参考
练习 1:转账两个账户,怎么写事务才不会死锁?
<details>
<summary>参考答案</summary>
把两个 id 排序,永远先锁 id 小的再锁 id 大的。两个并发转账都遵守这个顺序,就不会循环等待。代码里 ids = sorted([a, b]),然后依次 UPDATE。
</details>
练习 2:为什么事务里不要放 RPC 调用?
<details>
<summary>参考答案</summary>
事务持有的行锁要到 COMMIT 才释放。RPC 网络抖动几秒,锁就多占几秒,其他事务全部排队。正确做法是事务只包 DB 操作,RPC 移到 COMMIT 之后,或走 MQ 异步。
</details>
练习 3:RR 级别下,事务 A 普通 SELECT 5 行,事务 B 插入 1 行并提交,事务 A 再 UPDATE 这个范围,会发生什么?
<details>
<summary>参考答案</summary>
UPDATE 是当前读,会把 B 插入的那行也更新掉。之后再普通 SELECT 还是 5 行(快照),但 UPDATE 影响了 6 行——这就是 RR 没解决的"当前读幻读"。要避免,在事务开头用 SELECT ... FOR UPDATE 加范围锁。
</details>
下一篇预告:第 37 章 MySQL踩坑:性能与慢查询篇
文章回复
0 条公开回复