订单创建必须用 start transaction 包裹,因autocommit=1无法保证多表写入(orders/order_items/inventory)的原子性;库存扣减须用select ... for update防超卖;order_id应避免auto_increment,推荐应用层生成如雪花算法;幂等需request_id+事务内select ... for update校验;隔离级别宜用read committed。

订单创建必须用 START TRANSACTION 包裹,不能只靠 AUTOCOMMIT=1
MySQL 默认开启自动提交,但订单涉及多表写入(如 orders、order_items、inventory),一旦中间出错,比如库存扣减成功但订单没插入,就会产生脏数据。自动提交会让每条语句独立生效,无法回滚。
实操建议:
- 显式执行
START TRANSACTION,所有写操作(INSERT/UPDATE)放在同一事务内 - 最后统一
COMMIT或ROLLBACK,不要依赖客户端连接池的“隐式事务”行为 - 应用层需设置超时(如
wait_timeout和innodb_lock_wait_timeout),避免长事务阻塞 - 注意:PHP 的
mysqli默认不自动开启事务,PDO需调用beginTransaction(),不是设个ATTR_AUTOCOMMIT => false就完事
库存扣减必须用 SELECT ... FOR UPDATE,别信 UPDATE ... WHERE stock > ?
直接 UPDATE inventory SET stock = stock - 1 WHERE sku_id = ? AND stock >= 1 看似原子,但在高并发下会漏判——两个事务同时读到 stock = 1,都通过 WHERE 条件,结果扣成 -1。
实操建议:
- 先查再改:用
SELECT stock FROM inventory WHERE sku_id = ? FOR UPDATE锁住该行,再判断并更新 - 确保查询条件走主键或唯一索引,否则可能升级为间隙锁甚至表锁
- 不要在
FOR UPDATE后加ORDER BY或LIMIT(除非确定只锁一行),否则可能锁住不相关记录 - 事务里不要做 HTTP 调用、文件读写等耗时操作,锁持有时间越短越好
order_id 别用 AUTO_INCREMENT 主键暴露业务规律
用户看到订单号是连续递增的(如 10001 → 10002),容易推测日单量、促销热度,甚至被恶意遍历下单。更麻烦的是分库分表后,自增 ID 失去全局唯一性。
实操建议:
- 生成逻辑放应用层:用雪花算法(
snowflake)、UUIDv7 或数据库REPLACE INTO ... SELECT MAX(id)+1+ 重试(慎用) - 如果坚持用 DB 生成,至少加随机盐:
CONCAT(DATE_FORMAT(NOW(), '%y%m%d'), LPAD(FLOOR(RAND()*10000), 4, '0'), LPAD(id, 6, '0')) -
order_id字段类型推荐VARCHAR(32),别用BIGINT,避免下游系统误当数字处理 - 注意 MySQL 8.0+ 的
UUID_TO_BIN()可优化索引效率,但需要应用层配合转换
幂等性不能只靠 UNIQUE KEY,得结合业务状态机
用户连点“提交订单”,前端重复发请求,光靠 UNIQUE(order_no) 只能拦住第二条插入,但若第一条因网络超时没返回,用户以为失败又重试,结果创建了两个订单——而数据库只报了一个 Duplicate entry 错误。
实操建议:
- 每个下单请求带唯一
request_id(如 UUID),存入requests表,状态为pending - 事务内先查
request_id是否已存在且状态为success,是则直接返回原订单号 - 成功后更新
requests状态,并在orders表加request_id字段用于关联追溯 - 别把幂等校验放在事务外:先查再插,中间有竞态;必须和订单插入同事务,用
SELECT ... FOR UPDATE锁住request_id行
事情说清了就结束。最常被跳过的其实是事务隔离级别——默认 REPEATABLE READ 在某些场景下会导致幻读被忽略,而订单系统真正需要的是 READ COMMITTED 配合行锁,这点很多人连 explain 都没跑过。










