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 自动重试,下次大概率成功。