核心是控制事务粒度而非单纯加事务;需确保WHERE走主键/唯一索引、避免隐式转换与函数导致全表扫描、用游标分批替代OFFSET、编程式控制事务边界、统一多表更新顺序防死锁。

核心不是“怎么加事务”,而是“怎么不让事务变大”。锁表本质是锁住太多行太久,而不是数据量本身大。
确保 WHERE 条件走主键或唯一索引
没索引或索引失效时,InnoDB 会全表扫描并逐行加锁,等效锁表。常见坑包括:
- 字段类型不匹配:如 status 是 VARCHAR,却写
WHERE status = 1,触发隐式转换 - 函数包裹条件:如
WHERE DATE(create_time) = '2026-09-01',应改写为create_time >= '2026-09-01' AND create_time < '2026-09-02' - 前缀索引未覆盖查询值:如索引是
INDEX(name(10)),但查WHERE name = 'verylongusername'(超10字符) - 执行前务必用
EXPLAIN验证type不是ALL,且rows明显小于总行数
按主键范围分批,不用 OFFSET 分页
LIMIT + OFFSET 在大数据偏移时仍会扫描前面所有行,锁范围不可控。正确做法是游标式分批:
- 先查一批 ID:
SELECT id FROM t WHERE ... ORDER BY id LIMIT 500 - 再更新这批 ID:
UPDATE t SET x = y WHERE id IN (1,2,3,...),注意 IN 列表顺序与查询一致 - 更稳妥方式:
WHERE id > ? AND id <= ?,下一批起始值取上一批MAX(id) - 单批控制在 100–500 行;若含大字段或 JOIN,建议 ≤100
- 每批后显式
COMMIT,可加SLEEP(0.01)缓解锁队列堆积
禁用声明式事务,改用编程式控制
@Transactional 容易把查询、远程调用、循环处理全包进一个长事务里。应主动拆开:
立即学习“Java免费学习笔记(深入)”;
- 用
TransactionTemplate.execute()包裹真正需要原子性的写操作 - 把非事务性动作(如查 ID 列表、参数校验、HTTP 调用)移出事务边界
- 避免在事务中混用
SELECT FOR UPDATE,它可能升级为间隙锁,扩大锁定范围 - 确认连接
autocommit状态,必要时显式设为 0,并用BEGIN/COMMIT显式包裹
统一多表更新顺序,防死锁
两个事务以不同顺序更新 order 和 user 表,必然形成死锁环路。这不是概率问题:
- 所有业务代码必须约定唯一顺序:按表名字母序(
order_item → order → user),或按业务流向(order → order_item → payment) - 禁止根据业务类型动态拼接 SQL 顺序,例如不能因
type == 'refund'就先更新user再更新order - 每张表的更新都必须基于主键或唯一索引,避免无索引导致宽范围锁


















