SQL 如何处理“热点行”更新导致的行锁竞争

9次阅读

UPDATE同一行卡住服务是因为InnoDB行锁在热点场景下成为串行瓶颈;需通过拆分表、分桶设计、redis预扣减+异步落库等“拆”和“绕”策略缓解锁争用。

SQL 如何处理“热点行”更新导致的行锁竞争

为什么 UPDATE 同一行会卡住整个服务?

不是锁不够细,而是所有请求在抢同一把“门禁卡”——InnoDB 的行锁在热点场景下反而成了串行瓶颈。比如 UPDATE goods SET stock = stock - 1 WHERE id = 123,哪怕 QPS 只有 2000,只要集中在同一 id,事务就得排队等前一个提交释放 X 锁。更糟的是,如果事务里还夹着发消息、查用户信息、调外部 API,那这把锁可能被攥着上百毫秒,等待队列指数膨胀,CPU 直奔 95%。

怎么让 mysql 别老盯着那一行?

核心思路是“拆”和“绕”:把单点压力打散,或把写操作从数据库里挪出去。实操上优先落地成本低、见效快的几条:

  • INSERT INTO ... ON DUPLICATE KEY UPDATE 替代先 selectUPDATE,避免额外加读锁 + 持锁时间翻倍
  • 高频更新字段(如库存、积分)必须拆到独立子表,例如 goods_stock 单独建,别和 goods 主表挤在一起
  • 对确定的热点行(如秒杀商品),直接改用分桶设计:goods_id + shard_id 联合主键,10 个分片就摊薄 90% 锁冲突
  • 事务内只做 DB 操作,删掉日志打印、远程调用、非必要 SELECT for UPDATEUPDATE 尽量放到事务最后执行,缩短锁持有时间

redis 预扣减 + 异步落库,真能不用锁?

能,但得接受短暂不一致。这不是妥协,而是权衡:用户看到“已抢到”,数据库还没写完,中间差个几十毫秒,对秒杀这种场景完全可接受。关键要稳住两头:

  • 用 Redis lua 脚本做原子预扣:DECR 成功才放行,失败直接返回,绝不穿透到 MySQL
  • 异步写 DB 用消息队列(如 rocketmq),消费端控制速率(如每秒 300 条),避免反压打垮数据库
  • 加幂等保障:消息体带唯一业务 ID,DB 层用 INSERT IGNOREON DUPLICATE KEY UPDATE 防重

哪些“优化”反而会让锁问题更隐蔽?

最容易踩坑的是自以为在优化,实则放大了争用:

  • ORM 自动生成的 UPDATE 没走索引,导致行锁升级成间隙锁,一把锁锁住一整段索引范围
  • 连接池 maximumPoolSize 设成 100,结果瞬间涌进 800 个请求,锁队列雪崩式积,应用线程全卡在 getConnection()
  • 缓存没设过期或没做穿透保护,缓存失效瞬间,所有请求批量击穿到 DB,制造突发热点
  • 长期开着 REPEATABLE READ 隔离级别却没意识到它会多持锁(尤其 Next-Key Lock),而业务其实只需要 READ COMMITTED

真正治本的从来不是调参或换数据库,而是把“必须强一致”的假设打碎——先问一句:这里真的不能等 100ms 吗?

text=ZqhQzanResources