单行update带条件扣减是最稳妥起点,但qps超1000会因过度守规矩退化为串行瓶颈;select for update在秒杀中更卡因其将请求堵在查库存环节,导致tps断崖下跌60%以上。

单行 UPDATE 带条件扣减是当前最稳妥的起点,但一旦 QPS 超过 1000,它就会迅速退化为串行瓶颈——这不是锁没生效,而是它太“守规矩”了。
为什么 SELECT FOR UPDATE 在秒杀里反而更卡
它确实加了行锁,但把所有请求强制堵在“查库存”这一步,等于自己造了个单点排队口。真实压测中 TPS 常断崖下跌 60% 以上。
- 事务未提交或
autocommit=1→ 锁瞬间释放,后续请求读到旧值 - 查询条件没走索引(比如
WHERE status = 1但status无索引)→ 升级为表锁或间隙锁全扫,死锁概率飙升 - 多个事务按不同顺序访问多行(A 先锁 10086 再锁 10087,B 反过来)→
Deadlock found when trying to get lock日志刷屏 - 事务里调 HTTP 或 sleep → 锁持有时间拉长,拖垮整条链路
用 UPDATE ... WHERE stock >= ? 防超卖的硬性前提
这条语句能原子完成判断与扣减,但前提是它真能落到一行上,而不是扫全表或锁住一片间隙。
- 必须建联合索引:
INDEX idx_goods_id_stock (goods_id, stock),只建goods_id单字段索引不够 -
stock字段必须是INT或BIGINT,禁用FLOAT/DECIMAL—— 浮点误差会导致stock >= 1判定失效,扣出负数 - 执行后必须检查
ROW_COUNT():返回0表示该行已空或不匹配,不能当成失败忽略,得重试或换路径 - 无需显式
START TRANSACTION——autocommit=1下单条UPDATE也具备行锁和原子性
分段加锁(virtual sharding)落地时最容易漏掉的三件事
把一个 goods_id 拆成 10 个 (goods_id, shard_id) 槽位,本质是把“一行热”打散为“多行温”,但光建表不够。
- 初始化必须均分库存:比如总库存 1000,要插 10 条记录,每条
stock = 100,不能只插一条然后靠应用层随机选——否则还是热点 -
shard_id生成要稳定可重现:推荐用user_id % 10,而不是FLOOR(RAND() * 10);后者在重试时无法保证选同一槽,导致重复扣减或漏扣 - 每次
UPDATE后必须校验影响行数:返回0表示当前槽位已空,需换下一个shard_id重试;若直接返回失败,就浪费了其他槽位的余量
分段不是万能解药——它把锁竞争从“一行”挪到“多行”,但每行仍可能成为新热点;更麻烦的是,SUM(stock) 查总库存无法满足强一致性要求,金融类场景慎用。











