查看: 95|回复: 0

从并发脏写、锁升级到死锁,开发避坑全指南

[复制链接]

849

主题

1

回帖

2602

积分

超级版主

积分
2602
发表于 2026-8-15 09:45:38 | 显示全部楼层 |阅读模式
前言
       开发中经常遇到这些线上问题:抢购商品提示库存不足、转账报错、接口长时间转圈超时、定时任务批量更新直接拖垮整张表,甚至偶发 Deadlock 死锁异常。这些现象背后,全部和数据库锁机制强相关。
       本文从生活化场景切入,通俗讲解锁的存在意义、共享锁 / 排他锁工作逻辑,深度拆解锁升级、死锁两大性能杀手,结合 MySQL InnoDB 实战给出可落地的开发规范,新手、后端开发、运维都能看懂并直接用于项目优化。

一、什么是数据库锁?为什么必须有锁?

1. 无锁会发生什么?脏写问题

举个工作场景:公司预算表余额 5000 元,员工 A、员工 B 同时打开表格填写报销:
  • A 扣除 3000,预期余额 2000
  • B 扣除 4000,预期余额 1000两人同时读取原始余额 5000,先后保存,最终只会保留后一次修改,前面的扣款直接丢失,数据逻辑完全错乱。
数据库锁就是用来解决多事务并发修改同一数据的并发控制机制,相当于给数据贴上「请勿打扰」标识,同一时间只允许有序操作,牺牲一点并发等待,保证最终数据准确。
2. 锁的核心定位

锁是数据库实现事务隔离、保障数据一致性的底层手段,广泛用于库存扣减、账户转账、订单更新、商品秒杀等高并发场景。只要多条业务同时读写同一条记录,锁就会自动生效。
二、两种核心锁:共享锁(读锁)& 排他锁(写锁)

数据库锁分为两大类,二者互斥规则决定了并发读写的排队逻辑,附兼容性对照表方便理解。
1. 共享锁 S(读锁)

  • 触发场景:普通查询、SELECT ... LOCK IN SHARE MODE 锁定读
  • 特性:共享兼容,多条事务可以同时加共享锁读取数据;
  • 限制:只要存在任意共享锁,其他事务无法加排他锁执行修改,必须等全部读锁释放。
  • 类比:多人同时翻看同一本书,互不干扰,但没人能涂改书页。
2. 排他锁 X(写锁)

  • 触发场景:UPDATE / DELETE / INSERT、SELECT ... FOR UPDATE
  • 特性:完全独占、互斥,只要一条事务持有排他锁,其他事务不管读、写全部阻塞等待;
  • 适用场景:库存扣减、转账、订单状态变更,必须保证修改过程无其他事务介入。
  • 类比:一个人拿到笔修改书本,其他人既不能看也不能改,只能排队等候。
锁兼容性矩阵

表格
[td]
当前持有锁请求共享锁 (S)请求排他锁 (X)
共享锁 (S)放行阻塞等待
排他锁 (X)阻塞等待阻塞等待
三、两大线上性能灾难:锁升级 + 死锁

(一)锁升级:行锁直接变表锁,全表阻塞

1. 什么是锁升级

InnoDB 默认使用行锁,仅锁定修改的目标行,其他行不受影响,并发性能高。但数据库维护大量行锁会消耗内存,当单次事务锁定行数超过阈值、或 SQL 无索引触发全表扫描时,数据库会自动把大量行锁合并为一张表锁,这个过程就是锁升级。
2. 生活化举例

你只想维修房间一盏灯,物业嫌逐间管控麻烦,直接拉断整层电闸,整层所有人都无法用电。对应到数据库:明明只改几十行数据,却锁死整张表,所有读写请求全部排队,系统大面积超时、卡顿。
3. 常见触发原因

  • 更新语句无有效索引,执行全表扫描,数据库无法精准定位行,直接锁整张表;
  • 单次事务批量更新上千行,超过数据库锁数量阈值,触发锁升级;
  • 使用范围查询(between、like)产生大量间隙锁,锁范围扩散。
(二)死锁:事务互相等待,永久阻塞

1. 死锁定义

两个及以上事务,各自持有对方需要的排他锁,形成循环等待,若无外部干预会永久卡住,这就是死锁。死锁发生必须同时满足 4 个条件,缺一不可:
  • 互斥条件:写锁独占资源,不能共享;
  • 持有并等待:事务已经持有部分锁,同时申请其他锁;
  • 不可剥夺:已持有的锁不能被系统强行收回;
  • 循环等待:事务之间形成环形等待链路。
2. 经典转账案例(最容易出现死锁)

账户表 t_account,id=1、id=2 两个账户余额:
  • 事务 T1:1 号转给 2 号,先锁 id=1,再申请 id=2 的行锁
  • 事务 T2:2 号转给 1 号,先锁 id=2,再申请 id=1 的行锁
执行时序:
  • T1 获取 id=1 排他锁;
  • T2 获取 id=2 排他锁;
  • T1 等待 T2 释放 id=2 的锁;
  • T2 等待 T1 释放 id=1 的锁;
双方互相卡住,形成死循环,业务请求直接报错。
3. 数据库如何自动处理死锁

InnoDB 内置死锁检测器,会定时扫描事务等待链路,一旦识别死锁环,会自动选择代价最小的事务(修改行数少、执行时间短)强制回滚,释放锁资源,保证其他事务正常执行。开发中偶尔出现 Deadlock found when trying to get lock 报错,不是代码 BUG,是数据库主动止损的保护机制。
四、生产环境 3 条核心规范,彻底规避死锁与锁阻塞

原则 1:事务尽可能短小,缩短锁持有时间

锁从事务第一条修改语句加锁,直到 COMMIT 提交才统一释放(两阶段锁协议 2PL)。事务越长,锁占用时间越久,冲突、死锁概率指数上升。优化手段:
  • 只把更新数据库的 SQL 放进事务;
  • 接口日志、第三方接口调用、文件 IO、消息推送全部移到事务外部;
  • 拆分超大批量更新,分批提交,避免一次性锁定大量行。
原则 2:所有事务统一固定顺序访问资源

死锁根源是加锁顺序不一致,只要全系统统一加锁顺序,就能直接破坏「循环等待」条件,从根源杜绝死锁。以上面转账场景改造:所有转账逻辑,优先锁定 id 更小的账户,再处理 id 更大的账户。
  1. -- 无论谁转给谁,都先锁小id
  2. START TRANSACTION;
  3. UPDATE t_account SET balance=balance-100 WHERE id=1;
  4. UPDATE t_account SET balance=balance+100 WHERE id=2;
  5. COMMIT;
复制代码
此时 T2 操作时会先等待 id=1 的锁释放,不会形成循环等待。
原则 3:完善索引,避免行锁升级为表锁

无索引的更新语句会全表扫描,数据库无法精准定位行,直接升级表锁,压垮线上并发。落地要求:
  • UPDATE / DELETE 的 WHERE 条件必须建立主键 / 普通索引;
  • 避免索引失效操作:函数运算、隐式类型转换、!=、or 导致索引失效;
  • 范围查询尽量缩小区间,减少间隙锁扩散范围。
五、补充:线上锁阻塞排查常用 SQL(MySQL InnoDB)

线上出现大量超时、锁等待时,执行以下语句定位阻塞事务:

  1. -- 查询当前运行中的事务
  2. SELECT * FROM information_schema.INNODB_TRX;
  3. -- 查询当前存在的锁
  4. SELECT * FROM information_schema.INNODB_LOCKS;
  5. -- 查询锁等待关系(谁在等谁)
  6. SELECT * FROM information_schema.INNODB_LOCK_WAITS;
复制代码
通过事务 ID、锁定行、等待资源,快速定位慢事务、批量更新引发的锁阻塞问题。
六、总结

  • 锁是数据库并发一致性的基础,核心分为共享读锁、排他写锁,二者互斥规则决定排队逻辑;
  • 行锁并发性能高,但无索引、批量更新会触发锁升级,整张表被阻塞;
  • 死锁是事务循环等待资源,数据库会自动回滚其中一个事务,但会影响用户体验;
  • 开发层面三大最优实践:短事务、统一加锁顺序、完善索引,能解决 90% 以上锁相关线上故障;
  • 日常业务中抢购、转账、库存扣减等高并发场景,必须提前考虑锁竞争问题,避免上线后出现大量超时与死锁报错。
简单理解:页面转圈、操作失败,本质都是数据库里的锁在排队;合理控制锁的范围、持有时间,才能在数据准确和系统性能之间找到平衡。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|天翼网

相关侵权、举报、投诉及建议等,请发 E-mail:2026@typc.net

Powered by Discuz! X5.0 © 2001-2026 Discuz! Team.|晋ICP备2026008270号-1|晋公网安备14010602111293号

QQ客服返回顶部