订单事务卡在INSERT...SELECT或UPDATE...WHERE order_status='unpaid'时,应拆分为无锁SELECT校验+带status条件的UPDATE,失败则直接返回;订单号改用REPLACE INTO+LAST_INSERT_ID()生成;状态更新必须含version或status约束;核心链路(扣库存、写订单)须同步,其余异步至Kafka。

MySQL 订单事务卡在 INSERT ... SELECT 或 UPDATE ... WHERE order_status = 'unpaid' 上怎么办
电商订单创建和状态更新频繁走行锁,尤其当多个请求同时竞争同一库存记录或订单号生成逻辑时,SELECT FOR UPDATE 会直接串行化。这不是索引能解决的问题——本质是「写冲突集中」。真实压测中,UPDATE orders SET status='paid' WHERE id=12345 AND status='unpaid' 在并发 200+ 时平均等待超 800ms,因为 InnoDB 的 next-key lock 锁住了间隙,而业务又没做重试兜底。
实操建议:
- 把「校验 + 更新」拆成两步:先用
SELECT status FROM orders WHERE id=?(无锁读),再用带条件的UPDATE ... WHERE id=? AND status='unpaid';影响行数为 0 就说明已被抢,直接返回失败,不进重试循环 - 订单号生成必须脱离数据库自增主键,改用
REPLACE INTO order_seq (dummy) VALUES (0)+LAST_INSERT_ID()配合唯一键,避免SELECT MAX(id)+1类查询成为单点瓶颈 - 所有订单状态变更操作,WHERE 条件里必须包含
version字段(乐观锁)或status(状态机约束),禁止无条件UPDATE orders SET status='shipped'
为什么不能直接用 Redis 队列替代 MySQL 事务
见过太多团队把「下单」整个流程扔进 LPUSH order_queue,以为削峰就完成了。结果用户点击下单后页面转圈 3 秒才提示“已受理”,实际订单还没落库;更糟的是,消费者进程崩溃时,Redis 里堆积的 order:789 消息丢了,用户付了钱却没生成订单。
关键不是“要不要异步”,而是“哪部分能异步、哪部分必须同步”。订单核心链路只有两个强一致性节点:扣减库存 和 写入订单主表,其余如发券、推消息、更新搜索索引,全可异步。
实操建议:
- MySQL 写完
orders表后,立刻发一条轻量消息到 Kafka,topic 命名为order_created_v2,不要用 Redis List 做队列——它没 ACK、没分区、没重试语义 - 库存扣减必须走 MySQL 事务内完成,哪怕你用了 Redis 分布式锁,最终也要落地到
UPDATE inventory SET stock=stock-1 WHERE sku_id=? AND stock>=1并检查ROW_COUNT() - 如果用内存队列(如 Java 的
LinkedBlockingQueue),只限单机内短生命周期任务,比如“写完订单后立即触发短信模板渲染”,且必须设置offer()超时,拒绝阻塞主线程
innodb_buffer_pool_size 设太大反而导致订单插入变慢
有团队把 64G 内存服务器的 innodb_buffer_pool_size 设成 48G,结果高峰期 INSERT INTO orders RT 翻倍。不是因为内存不够,而是 Buffer Pool 太大导致 LRU 链表管理开销剧增,且脏页刷盘时大量 fsync 打满磁盘 IOPS。
电商订单写多读少,热点数据其实很窄:最近 2 小时的订单、热门 SKU 库存记录、用户最近地址。盲目堆内存,不如精准优化访问模式。
实操建议:
- 用
SELECT * FROM information_schema.INNODB_BUFFER_POOL_STATS查看pages_made_young和pages_not_made_young比值,若长期低于 0.3,说明热数据没被有效缓存,优先调优查询而非扩 buffer pool - 对
orders表按天分表(如orders_20240520),配合ALTER TABLE ... ORDER BY created_at物理聚簇,让新订单连续写入,减少随机 IO - 关闭
innodb_adaptive_hash_index——订单查询极少走等值二级索引查找,这个功能反而在高并发更新时抢 mutex,实测关掉后INSERTQPS 提升 12%
异步任务失败后怎么保证订单状态不丢不重
最常出问题的是“支付成功回调更新订单状态”这个环节。上游支付平台可能重复推送通知,下游如果没做幂等,就会出现 UPDATE orders SET status='paid' WHERE order_id='abc' 执行两次;更麻烦的是,第一次更新因网络超时没收到响应,但 MySQL 实际已提交,第二次又来,就变成“状态覆盖丢失”。
解决方案不在队列层,而在数据库设计层。所有异步驱动的状态变更,都必须可重入。
实操建议:
- 订单表加字段
pay_notify_time DATETIME NULL,更新语句必须是UPDATE orders SET status='paid', pay_notify_time=NOW() WHERE order_id=? AND pay_notify_time IS NULL - 消费 Kafka 的服务启动时,先查
SELECT order_id FROM orders WHERE pay_notify_time IS NULL AND status='paid'做一次补偿扫描,避免启动瞬间消息积压漏处理 - 不要用「订单ID + 支付流水号」双唯一索引防重,支付流水号可能为空或被篡改;真正可靠的幂等键是「业务动作 + 业务单据号 + 业务时间戳」三元组,存在单独的
idempotent_log表里
真正的瓶颈往往不在 MySQL 参数或队列选型,而在于没想清楚哪一步「可以错、哪一步不能错」。比如库存扣减失败必须立刻告诉用户“缺货”,而发优惠券失败完全可以后台重试三次再告警——混在一起做异步,只会让问题更难定位。


















