mysql中多事务并发执行时的锁机制与性能优化

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

MySQL 的行锁在什么情况下会升级为表锁

InnoDB 默认加的是行级锁,但不是所有查询都能走索引——一旦

WHERE
条件无法使用索引(比如隐式类型转换、函数包裹字段、
LIKE '%abc'
),InnoDB 就可能退化为扫描全表,进而对所有扫描过的记录加锁,甚至触发锁升级(实际是锁数量爆炸导致性能崩塌,而非真正“升级”)。

检查执行计划:
EXPLAIN SELECT * FROM orders WHERE status = 'pending';
确保
type
ref
或更优,
key
显示用了哪个索引
避免在索引列上做运算:
WHERE YEAR(create_time) = 2024
→ 改成
WHERE create_time >= '2024-01-01' AND create_time 
字符串比较注意字符集和排序规则:不同
COLLATION
可能导致索引失效,用
SHOW CREATE TABLE t
确认字段定义

UPDATE 语句没走索引时的锁行为有多危险

一条没命中索引的

UPDATE
在高并发下极易引发锁等待雪崩。它不只是慢,而是会持续持有大量记录的
X
锁,阻塞其他事务对这些记录的读(需要
S
锁)和写(需要
X
锁),甚至波及无关行——因为 Gap Lock 会锁住索引间隙。

典型错误:
UPDATE users SET balance = balance - 100 WHERE phone = 13800138000;
—— 如果
phone
VARCHAR
类型,而传入数字,触发隐式转换,索引失效
验证方式:开启锁监控:
SELECT * FROM performance_schema.data_locks;
观察
LOCK_DATA
LOCK_MODE
字段
线上紧急缓解:临时加索引(需评估 DDL 阻塞影响),或改用主键分批更新:
UPDATE users SET balance = balance - 100 WHERE id IN (1001,1002,1003);

如何用 SELECT ... FOR UPDATE 安全地实现扣减库存

直接

UPDATE stock SET count = count - 1 WHERE id = 123 AND count > 0
有竞态风险:两个事务同时读到
count=1
,都执行成功,变成
-1
。必须显式加锁并检查结果。

正确顺序:先查再锁,且用唯一索引(如主键或
sku_id
):
SELECT count FROM stock WHERE sku_id = 'ABC123' FOR UPDATE;
应用层判断返回值是否 ≥ 1,再执行
UPDATE
;不要依赖
ROW_COUNT()
做最终校验,因为锁已释放
避免在
FOR UPDATE
查询里 JOIN 其他大表,否则锁范围扩大;必要时拆成两步,用
SELECT ... FOR UPDATE
查主键,再用主键更新
超时控制:设置
innodb_lock_wait_timeout
(默认 50 秒),并在应用层捕获
Lock wait timeout exceeded
错误重试或降级

READ COMMITTED 和 REPEATABLE READ 隔离级别对锁的影响差异

很多人以为 RC 能减少锁,其实只在“不加锁读”上宽松,写锁行为几乎一致。真正的区别在于 Gap Lock 是否启用——RR 下普通

SELECT
不加锁,但
UPDATE/DELETE
会加 Next-Key Lock(Record + Gap),而 RC 下只锁匹配到的记录,不锁间隙。

RC 更适合高并发更新场景(如秒杀),能显著降低死锁概率,但要接受“幻读”——不过业务上往往可接受(比如多插入几单不影响核心逻辑) RR 是 MySQL 默认,安全性高,但若业务大量执行范围条件更新(如
UPDATE logs SET status=1 WHERE created_at > '2024-01-01'
),Gap Lock 会锁住整个时间范围,极易阻塞
切换前务必测试:修改会话级隔离级别仅影响当前连接:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

锁机制本身不复杂,难的是在索引失效、隔离级别、事务粒度、应用重试逻辑之间找到平衡点。最容易被忽略的是:你以为只锁了一行,其实 InnoDB 锁了一片;你以为改了隔离级别就安全了,其实没配好索引照样卡死。

相关推荐