高并发下防超卖需用SELECT...FOR UPDATE行锁,必须命中索引;无强序需求可用唯一约束+ON DUPLICATE KEY UPDATE;严格FIFO应交由Redis队列处理,避免依赖AUTO_INCREMENT或时间戳排序。

用 SELECT ... FOR UPDATE 实现行级排队
在高并发更新同一行数据时(比如库存扣减),直接 UPDATE 可能导致超卖。必须让请求串行化处理该行。核心是先查再锁,且查询必须命中索引——否则会升级为表锁或锁住不相关记录。
-
SELECT stock FROM goods WHERE id = 123 FOR UPDATE必须基于主键或唯一索引,否则 MySQL 可能加间隙锁(Gap Lock)锁住更大范围 - 事务中不能只执行这句就提交,必须紧接着做
UPDATE和COMMIT,否则锁会持续到事务结束,拖慢整体吞吐 - 应用层需设置合理超时,例如 JDBC 的
setQueryTimeout(),避免某请求卡死导致后续全部阻塞
用 INSERT ... SELECT ... ON DUPLICATE KEY UPDATE 做无锁队列写入
当“排队”目标不是强一致性顺序,而是防止重复处理(如防重下单),可用唯一约束 + 插入冲突回退替代显式加锁。它比 FOR UPDATE 更轻量,但不保证全局执行顺序。
- 建表时给业务唯一键(如
order_no)加UNIQUE索引 - 用
INSERT INTO queue_log (order_no, status, created_at) VALUES ('NO123', 'pending', NOW()) ON DUPLICATE KEY UPDATE status = VALUES(status) - 插入成功说明是第一个请求;冲突则说明已有同订单在处理中,当前请求可直接跳过或轮询状态
用 Redis List + Lua 做外部协调队列
MySQL 本身不提供消息队列能力,纯靠数据库实现严格 FIFO 容易成为瓶颈。更常见的做法是把“排队逻辑”下沉到 Redis,MySQL 只负责最终状态落地。
- 用
LPUSH queue:order入队,BRPOP queue:order 0阻塞取任务,天然保序 - 关键操作必须原子:用 Lua 脚本封装「取任务 + 标记处理中 + 设置超时」,避免多个 worker 同时取到同一任务
- MySQL 更新只在 worker 拿到任务后执行,此时已脱离并发争抢,只需保证单次更新正确性即可
为什么不能依赖 AUTO_INCREMENT 或时间戳排序请求
很多人误以为插入时的自增 ID 或 NOW() 时间就能代表请求到达顺序,实际在并发下完全不可靠。
-
AUTO_INCREMENT在批量插入、InnoDB 的innodb_autoinc_lock_mode=2(默认)下可能跳号、乱序分配 -
NOW()精度只有秒级(5.6 及以前)或微秒级(5.7+),但同一微秒内仍可能有多个请求,且写入落盘顺序≠执行顺序 - 主从复制、连接池复用、事务延迟提交都会进一步打乱“看起来的时间顺序”
-- 错误示例:以为按 id 升序就是请求顺序 SELECT * FROM order_log ORDER BY id ASC; -- 实际可能漏掉未提交事务的记录,或被 gap lock 影响可见性
真正需要顺序控制的地方,必须显式引入锁、队列或版本号机制,而不是依赖数据库的隐式行为。










