Go/redis-sentinel下如何将DB.Transaction的UpdateColumns改为异步
改造前置核心原则
- 禁止在事务回调函数内部启动异步协程执行DB操作:GORM的事务回调在return的瞬间就会执行提交/回滚,内部启动的协程执行时机完全不可控,极大概率出现事务回滚但异步更新已经落库、或者协程拿不到正确事务上下文的问题,直接产生脏数据。
- 先按一致性优先级拆分操作:库存扣减、订单创建、支付单创建属于交易核心链路,必须保证原子性,绝对不能改成异步;原有代码中更新
cnt表pay_status的操作本身就存在写法问题——用的是全局models.DB句柄而非事务传入的ts句柄,本身就不参与事务原子性,适合拆分出来做异步。 - 不推荐直接用裸协程跑异步任务:直接起
go func()执行操作时,如果服务刚好重启、协程触发panic,任务会直接丢失。结合你当前使用redis-sentinel的场景,优先用redis的stream/list结构做轻量任务队列,单独起后台worker消费执行,可靠性比裸协程高很多,不需要额外引入其他组件。
具体改造步骤
- 第一步:精简事务内的逻辑,只保留必须保证原子性的库存扣减、订单创建、支付单创建三个操作,所有事务内的DB操作统一使用回调传入的
ts句柄,禁止再用全局models.DB句柄。 - 第二步:异步任务的触发时机严格放在事务执行成功、err为nil之后,绝对不能在事务提交前就把异步任务发出去,避免事务回滚后异步更新仍然执行。
- 第三步:异步逻辑必须加panic捕获,同时配套错误日志、失败重试机制,避免单协程panic搞崩整个服务,也方便后续排查异常问题。如果用redis队列实现,消费端要做幂等校验,避免重复消费导致数据异常。
改造后参考代码
// 执行核心事务,仅保留强一致要求的原子操作 err = models.DB.Transaction(func(ts *gorm.DB) error { good_update := map[string]interface{}{ "stk_num": gorm.Expr("stk_num - ?", 1), } if err := ts.Model(&trade).UpdateColumns(good_update).Error; err != nil { return errors.New("Failed, please try later") } if err := ts.Create(order).Error; err != nil { return errors.New("Failed, please try later") } if err := ts.Create(payment).Error; err != nil { return errors.New("Failed, please try later") } return nil }) if err != nil { // 事务失败的原有回滚逻辑保留 rds.Del(c.Cts.Request.Context(), tradeLock).Val() service.DeleteStk(int(res.TradeID)) c.Error(nil, err.Error()) return } // 事务提交成功后再触发异步更新逻辑 // 以下是裸协程的轻量实现示例,生产环境建议替换为redis队列的可靠方案 if res.count > 0 { go func(targetId int64) { // 必须加panic捕获,避免协程异常崩溃 defer func() { if rec := recover(); rec != nil { // 此处替换为你项目里的日志打印方法 log.Printf("async update pay_status panic, target_id: %d, err: %v", targetId, rec) } }() dbErr := models.DB.Model(&cnt).Where("id = ?", targetId).UpdateColumns(map[string]interface{}{"pay_status": 0}).Error if dbErr != nil { // 打印错误日志,必要时触发告警、将任务重新入队重试 log.Printf("async update pay_status failed, target_id: %d, err: %v", targetId, dbErr) } }(res.count) } // 事务成功后的后续逻辑,比如释放分布式锁 rds.Del(c.Cts.Request.Context(), tradeLock).Val()
额外注意事项
- 不要为了“全异步”盲目把核心交易逻辑改成异步,库存、订单、支付三个操作的原子性是资损红线,必须留在事务内同步执行。
- 如果后续要把更多非核心操作改成异步,都遵循“事务成功提交后再发任务”的原则,不要在事务内部做异步触发。
- 用redis做任务队列时,建议给任务加唯一标识,消费端更新前先校验状态,避免重复更新导致数据不符合预期。
内容的提问来源于stack exchange,提问作者Elsa
相关产品推荐
相关产品推荐

