mysql事务与索引有什么关系_mysql锁范围解析

来源:这里教程网 时间:2026-02-28 20:37:41 作者:

事务修改数据时,索引不是“旁观者”,而是被实时锁住的参与者

MySQL(InnoDB)中,事务对数据的增删改**必然同步更新所有相关索引**,哪怕事务尚未提交。这不是延迟操作,而是立即发生的物理变更:插入要往B+树里加节点,更新索引列要删旧项+插新项,删除则标记索引条目为待清除。这些变更写入

redo log
保证持久,同时记录在
undo log
支撑回滚——也就是说,索引结构在事务生命周期内已被占用、锁定、甚至分裂。

常见错误现象:

UPDATE user SET status = 1 WHERE id = 123
执行慢、阻塞其他事务,但表很小、CPU不忙——大概率是
id
没建主键或索引,导致全表扫描+行锁升级为表级锁;或者
status
列上有索引却被高频更新,引发大量索引页重组织和锁竞争。

没有索引的
WHERE
条件 → InnoDB无法精确定位,只能扫全表并逐行加锁(
record lock + gap lock
组合成
next-key lock
),锁范围爆炸式扩大
唯一索引上的
INSERT
UPDATE
REPEATABLE READ
下会触发间隙锁,防止幻读,但也容易让并发插入卡在同一个间隙上
长事务持续持有索引页上的锁,其他事务想更新同一索引范围时,直接等待,而不是跳过——锁不是“查完才加”,是“一读就锁”

为什么“加了索引却还是锁表”?关键在查询是否真的走索引

索引存在 ≠ 查询命中索引。事务中一条

SELECT ... FOR UPDATE
UPDATE
若因隐式类型转换、函数包裹、
OR
条件、
LIKE '%xxx'
等导致索引失效,InnoDB就会退化为全表扫描,并对扫描路径上的每一页都加意向锁(
IX
),再对实际匹配行加行锁——此时锁范围远超预期,极易引发死锁或大面积阻塞。

实操建议:

EXPLAIN FORMAT=TRADITIONAL
确认事务内每条DML的
type
是否为
const
/
ref
/
range
,避免
ALL
index
检查
key
列是否显示真实使用的索引名;若为
NULL
,说明没走索引
ORDER BY
GROUP BY
字段建索引时,注意
filesort
是否消失——它不直接影响锁,但延长执行时间,变相延长锁持有时间

复合索引设计不当,会让事务锁得更久、更宽

事务中频繁执行

UPDATE orders SET paid_at = NOW() WHERE user_id = ? AND status = 'unpaid'
,如果只在
user_id
上建单列索引,InnoDB仍需回表过滤
status
,加锁范围是所有该
user_id
的行;而建
(user_id, status)
复合索引后,索引本身就包含
status
值,能直接在二级索引页完成判断,不仅减少回表I/O,还让锁精准落在匹配的几行上。

容易踩的坑:

把高频更新列(如
updated_at
)放在复合索引最左位 → 每次更新都要调整整个索引树结构,写放大严重
忽略最左前缀原则:建了
(a,b,c)
却只查
WHERE c = ?
,索引完全失效
为覆盖查询建冗余索引(如已有
(a,b)
,又单独建
b
索引)→ 写入时多维护一份索引,事务开销翻倍

大事务批量更新索引列,等于主动制造索引碎片和锁风暴

一个事务里执行

UPDATE products SET price = price * 1.1 WHERE category_id IN (1,2,3)
,若
category_id
有索引但匹配数万行,InnoDB会在同一事务内反复分裂B+树页、写大量
undo log
、膨胀
buffer pool
中的脏页——其他事务访问同一索引范围时,轻则等待,重则触发
Lock wait timeout exceeded
错误。

正确做法:

拆成小批次,例如
WHERE id BETWEEN 10000 AND 19999
,每次提交,释放锁与资源
确保
WHERE
条件走的是**高区分度索引**(如主键),避免扫描大量无关索引页
非高峰时段执行,或临时调低隔离级别至
READ COMMITTED
(间隙锁关闭,但需业务接受幻读风险)

最常被忽略的一点:事务提交后,索引页的物理碎片不会自动整理。长期运行的系统里,

SELECT COUNT(*)
变慢、
UPDATE
响应抖动,往往不是SQL问题,而是索引页分裂+空洞堆积所致——定期用
OPTIMIZE TABLE
ALTER TABLE ... FORCE
重建表(注意锁表影响)仍是必要手段。

相关推荐