news 2026/6/10 16:38:47

MySQL高并发下 SELECT ... FOR UPDATE 性能差的庖丁解牛

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL高并发下 SELECT ... FOR UPDATE 性能差的庖丁解牛

MySQL 高并发下SELECT ... FOR UPDATE性能差,本质是行锁竞争 + 事务持有时间过长导致的锁等待与吞吐下降。它并非“不好用”,而是在错误场景下被滥用


一、核心原理:FOR UPDATE如何工作?

▶ 1.加锁机制
  • InnoDB 行锁
    • SELECT ... FOR UPDATE会对结果集所有行排他锁(X Lock)
    • 其他事务无法读取(若未开启 MVCC 快照读)或修改这些行
  • 锁范围
    • 若无索引 →全表扫描 + 锁所有行(灾难!)
    • 若有索引 →仅锁命中行
▶ 2.事务生命周期
MySQL事务2事务1MySQL事务2事务1BEGINSELECT * FROM seats WHERE id=100 FOR UPDATE返回数据 + 加锁SELECT * FROM seats WHERE id=100 FOR UPDATE阻塞(等待 T1 释放锁)COMMIT返回数据

💡核心认知
FOR UPDATE的性能 = 锁粒度 × 事务时长 × 并发度


二、性能瓶颈:三大致命问题

▶ 1.锁粒度过大
  • 场景
    -- 无索引字段查询SELECT*FROMordersWHEREuser_id=123FORUPDATE;
  • 后果
    • 全表扫描 → 锁住所有行(即使只返回 1 行)
    • 并发度 ≈ 1(其他事务全部阻塞)
▶ 2.事务持有时间过长
  • 场景
    $pdo->beginTransaction();$seat=$pdo->query("SELECT * FROM seats WHERE id=100 FOR UPDATE")->fetch();// 调用第三方支付 API(耗时 2 秒!)$paymentResult=callPaymentAPI($seat);if($paymentResult){$pdo->exec("UPDATE seats SET status=2 WHERE id=100");}$pdo->commit();
  • 后果
    • 行锁持有 2 秒 → 其他请求排队等待
    • 吞吐量从 1000 QPS 降至 50 QPS
▶ 3.死锁风险
  • 场景
    • 事务 A 锁 seat 100 → 尝试锁 seat 101
    • 事务 B 锁 seat 101 → 尝试锁 seat 100
  • 后果
    • MySQL 检测到死锁 → 回滚其中一个事务 → 重试成本高

三、工程优化:四层解决方案

▶ 方案 1:缩小锁粒度(最有效)
  • 必须为 WHERE 字段加索引
    -- 添加索引ALTERTABLEseatsADDINDEXidx_train_status(train_id,status);-- 优化查询SELECTidFROMseatsWHEREtrain_id=100ANDstatus=0LIMIT1FORUPDATE;-- 仅锁 1 行
▶ 方案 2:缩短事务时长
  • 两阶段提交
    // 阶段1:锁定座位(短事务)$pdo->beginTransaction();$stmt=$pdo->prepare("SELECT id FROM seats WHERE train_id=? AND status=0 LIMIT 1 FOR UPDATE");$stmt->execute([$trainId]);$seatId=$stmt->fetchColumn();if($seatId){$pdo->exec("UPDATE seats SET status=1 WHERE id=?",[$seatId]);// 标记为已锁定}$pdo->commit();// 阶段2:异步处理支付(无锁)if($seatId){dispatchPaymentJob($seatId);// 放入队列}
▶ 方案 3:无锁设计(终极方案)
  • 预分配座位池
    -- 余票计数表CREATETABLEtrain_inventory(train_idINTPRIMARYKEY,availableINTNOTNULL);
  • 原子扣减
    // 乐观锁扣减库存$stmt=$pdo->prepare(" UPDATE train_inventory SET available = available - 1 WHERE train_id = ? AND available > 0 ");if($stmt->execute([$trainId])&&$stmt->rowCount()>0){// 分配具体座位(无锁)assignSeat($trainId);}
▶ 方案 4:Redis Lua 脚本(超高并发)
  • Lua 脚本保证原子性
    -- check_and_lock.lualocalavailable=redis.call('GET',KEYS[1])iftonumber(available)>0thenredis.call('DECR',KEYS[1])return1endreturn0
  • PHP 调用
    $locked=$redis->eval(file_get_contents('check_and_lock.lua'),["train:100:seats"],1);

四、避坑指南

陷阱破局方案
无索引使用 FOR UPDATE必须为 WHERE 字段加联合索引
事务中调用外部 API用两阶段提交分离锁与业务逻辑
盲目增加超时时间优化锁粒度比调大innodb_lock_wait_timeout更有效

五、终极心法

**“FOR UPDATE 不是枷锁,
而是精度的标尺——

  • 当你缩小粒度
    你在释放并发;
  • 当你缩短持有
    你在提升吞吐;
  • 当你拥抱无锁
    你在铸造韧性。

真正的高并发,
始于对锁的敬畏,
成于对细节的精控。”


结语

从今天起:

  1. 所有FOR UPDATE查询必须有索引
  2. 事务内禁止调用外部 API
  3. 超高并发场景优先考虑 Redis 无锁方案

因为最好的并发控制,
不是加更多锁,
而是精准控制每一比特的竞争。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 15:53:20

Java springboot基于微信小程序的海产品加工销售一体化管理系统(源码+文档+运行视频+讲解视频)

文章目录 系列文章目录目的前言一、详细视频演示二、项目部分实现截图三、技术栈 后端框架springboot前端框架vue持久层框架MyBaitsPlus微信小程序介绍系统测试 四、代码参考 源码获取 目的 随着海洋经济的蓬勃发展,海产品加工销售行业迎来新的机遇。本系统采用Ja…

作者头像 李华
网站建设 2026/6/10 20:33:23

基于SpringBoot+Vue的智能停车场管理系统(源码+lw+部署文档+讲解等)

课题介绍 随着城市机动车保有量的快速增长,传统停车场存在车位利用率低、车辆进出效率慢、缴费繁琐、车位查询不便、管理人员工作量大等问题,严重影响车主停车体验,也制约了停车场的规范化运营。为此,本课题旨在设计并实现一套基于…

作者头像 李华
网站建设 2026/6/10 17:30:57

GLM-4-9B-Chat-1M效果惊艳:百万token输入下首尾信息保留率实测报告

GLM-4-9B-Chat-1M效果惊艳:百万token输入下首尾信息保留率实测报告 1. 这不是“能读长文”,而是“真记得住开头和结尾” 你有没有试过让大模型读一篇50页的PDF,然后问它:“第3页提到的那个关键假设,和最后结论之间是…

作者头像 李华