Skip to content

10 - 库存服务与补偿机制 ​

库存服务是 Saga 流程的资源管理核心,承担库存扣减、并发控制和补偿回滚三个关键职责。

乐观锁并发控制 ​

库存扣减是高并发场景中最容易出现数据错误的操作。本系统采用**乐观锁(Optimistic Locking)**而非悲观锁,通过版本号避免库存超卖。

数据模型在 ecommerce/internal/inventory/data/model.go:3-8:

go
type Inventory struct {
    ID        int64  `gorm:"primaryKey;autoIncrement"`
    ProductID string `gorm:"type:varchar(36);uniqueIndex;not null"`
    Stock     int32  `gorm:"not null"`
    Version   int32  `gorm:"not null;default:1"`
}

Version 字段是乐观锁的关键:每次更新时检查版本号,更新成功后版本号+1。如果并发请求读到的版本号相同,先提交的请求成功,后提交的检测到版本不匹配则失败。

扣减实现(ecommerce/internal/inventory/data/repo.go:27-43):

go
func (r *inventoryRepo) Deduct(ctx context.Context, productID string, quantity int32) error {
    return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
        var inv Inventory
        tx.Where("product_id = ?", productID).First(&inv)
        if inv.Stock < quantity {
            return ErrInsufficientStock
        }
        result := tx.Model(&inv).
            Where("version = ?", inv.Version).                // 乐观锁条件
            Updates(map[string]interface{}{
                "stock":   inv.Stock - quantity,
                "version": inv.Version + 1,
            })
        if result.RowsAffected == 0 {
            return errors.New("并发冲突,请重试")               // 版本冲突
        }
        return nil
    })
}

三步完成:读记录(获取当前版本号)→ 检查库存 → 条件更新(version 匹配才执行)。

事件处理流程 ​

库存服务监听两类事件(ecommerce/internal/inventory/biz/event_handler.go:25-69):

OrderCreated → 扣减库存 ​

go
func (h *InventoryEventHandler) HandleOrderCreated(msg *message.Message) error {
    var event eventsv1.OrderCreated
    proto.Unmarshal(msg.Payload, &event)

    for _, item := range event.Items {
        if err := h.uc.Deduct(ctx, item.ProductId, item.Quantity); err != nil {
            // 补偿:回滚之前已扣减的库存
            for _, prev := range event.Items {
                if prev.ProductId == item.ProductId { break }
                h.uc.Restore(ctx, prev.ProductId, prev.Quantity)
            }
            // 发布库存不足事件
            h.pub.Publish(events.TopicInventoryInsufficient, ...)
            return nil
        }
    }
    // 全部扣减成功,发布预留事件
    h.pub.Publish(events.TopicInventoryReserved, ...)
}

补偿逻辑:一个订单可能包含多个商品项,遍历扣减时如果某个商品库存不足,需要将前面已扣减的商品库存恢复。这是 Saga 编舞模式中典型的本地补偿。

InventoryRelease → 释放库存 ​

支付失败后,order-service 发布 OrderCancelled 事件,inventory-service 的 HandleInventoryRelease 方法恢复库存:

go
func (h *InventoryEventHandler) HandleInventoryRelease(msg *message.Message) error {
    var event eventsv1.InventoryRelease
    proto.Unmarshal(msg.Payload, &event)
    for _, item := range event.Items {
        h.uc.Restore(ctx, item.ProductId, item.Quantity)
    }
}

恢复操作使用 UpdateColumn 而非乐观锁更新,因为补偿释放不需要版本控制——它只是安全地将库存加回:

go
func (r *inventoryRepo) Restore(ctx context.Context, productID string, quantity int32) error {
    return r.db.Model(&Inventory{}).
        Where("product_id = ?", productID).
        UpdateColumn("stock", gorm.Expr("stock + ?", quantity)).Error
}

并发安全要点 ​

  • 扣减使用数据库事务 + 乐观锁,保证并发不超卖
  • 恢复使用原子加法(stock + ?),不查版本避免补偿冲突
  • 全部操作在单个数据库事务内完成,失败自动回滚

在 ecommerce/cmd/inventory-service/main.go:65 中,Router 还配置了 Retry 中间件——处理并发冲突返回 error 后,Waitmill 自动重试,下次大概率成功。