订单创建必须用事务,否则易出现订单与库存不一致;GORM推荐用db.Transaction闭包方式,确保多表操作原子性,手动事务仅适用于需精细控制的场景。

订单创建必须用事务,否则极易出现「订单生成了但库存没扣减」或「库存扣了但订单失败」这类数据不一致问题。GORM 提供两种主流方式:自动事务(db.Transaction)和手动事务(Begin/Commit/Rollback),前者更安全、更常用。
为什么不能直接用 db.Create() 创建订单?
单条 Create() 调用默认走 GORM 自动事务(如果未禁用 SkipDefaultTransaction),但它只包裹当前操作——无法跨表保证原子性。比如下单需同时写 m_order、m_order_goods、扣减 m_goods.stock,这三步必须在同一个事务里,否则中间出错就会留下脏数据。
常见错误现象:
- 订单表插入成功,但商品库存未更新(或更新失败),用户付款后实际无货
- 库存扣减成功,但订单插入因唯一索引冲突失败,导致“货没了但没单”
- 使用多个独立
db.Create()调用,看似顺序执行,实则各自开启又提交事务,完全不隔离
用 db.Transaction 实现订单原子创建
这是最推荐的方式:闭包内所有数据库操作共享同一个事务上下文,任意一步报错即整体回滚。
实操要点:
- 必须使用传入的
tx *gorm.DB实例做所有操作,不能继续用原始db - 每个操作都要检查
.Error,一旦非 nil 就 return err,GORM 会自动 rollback - 闭包末尾 return nil 才会触发 commit
- 注意关联插入顺序:先建主订单(获取
tx.Statement.RowsAffected或order.ID),再插子表(m_order_goods),最后扣库存(用UPDATE ... SET stock = stock - ? WHERE goodsId = ? AND stock >= ?防超卖)
简短示例:
err := db.Transaction(func(tx *gorm.DB) error {
// 1. 创建订单
order := Order{UserID: 123, SalePrice: 999}
if err := tx.Create(&order).Error; err != nil {
return err
}
<pre class="brush:php;toolbar:false;">// 2. 创建订单项(需 order.ID)
item := OrderItem{OrderID: order.ID, GoodsID: 1, BuyNum: 2}
if err := tx.Create(&item).Error; err != nil {
return err
}
// 3. 扣库存(带条件更新,防止超卖)
result := tx.Exec("UPDATE m_goods SET stock = stock - ? WHERE goodsId = ? AND stock >= ?", item.BuyNum, item.GoodsID, item.BuyNum)
if result.RowsAffected == 0 {
return fmt.Errorf("insufficient stock for goods %d", item.GoodsID)
}
return nil // ✅ 成功提交
})
手动事务(Begin/Commit/Rollback)的适用场景
仅在需要精细控制事务生命周期时才用,比如嵌套逻辑中要提前 Commit 某个子流程、或需在 defer 中统一 Rollback。但容易漏写 Rollback() 或重复 Commit(),风险更高。
关键陷阱:
-
defer tx.Rollback()必须放在Begin()后立刻写,且要在判断是否 commit 前执行;否则可能 rollback 已 commit 的事务 - 事务对象
tx不能跨 goroutine 使用,否则 panic - 若中间调用了其他函数并传入
tx,那些函数内部不能再调用db.Begin(),否则会嵌套事务(MySQL 不支持真嵌套)
性能影响:手动事务本身无额外开销,但若忘记 Rollback() 导致连接卡住,会拖慢整个连接池。
事务中如何安全处理并发库存扣减?
单纯用 SELECT ... FOR UPDATE 在 GORM 里写法生硬且易错;更可靠的是用带条件的 UPDATE + 检查 RowsAffected,如上例所示。这是 MySQL 下防超卖的事实标准做法。
注意点:
- WHERE 条件中必须包含
stock >= ?,否则 UPDATE 成功但库存变负数 - 不要依赖 SELECT 后再 UPDATE(即“读-改-写”),存在竞态窗口
- 如果业务要求返回最新库存值,可在 UPDATE 后跟
SELECT stock FROM m_goods WHERE goodsId = ?,但该 SELECT 必须在同一事务内
复杂点往往不在语法,而在于你是否把「库存校验」和「扣减动作」锁在同一个原子操作里——哪怕只差一行代码,都可能让高并发下单变成一场数据灾难。











