
这篇文章的内容都是基于我们GoFrame微服务电商项目的实践,感兴趣的朋友可以点击查看
下面的问题是咱们组织里同学的提问,咱们可以一起探讨一下,欢迎在文章下面评论:
他说:
核心是结合分布式事务和Redis原子操作来设计同步调用流程。
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 订单服务 │ │ 库存服务 │ │ 优惠券服务 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
├────── Try(预扣减)────►│ │
│ ├────── Try(预扣减)────►│
│ │ │
│ │◄──── 预扣减结果 ────────│
│◄──── 预扣减结果 ────────│ │
│ │ │
│ 预扣减成功 → 创建订单 │ │
│ │ │
├──── Confirm(确认扣减)─►│ │
│ ├──── Confirm(确认扣减)─►│
│ │ │
│ │◄──── 确认结果 ────────│
│◄──── 确认结果 ────────│ │
│ │ │
│ 订单创建完成 │ │
│ │ │
│ 异常情况处理: │ │
│ - 调用Cancel回滚预扣减 │ │
│ - 异步补偿机制 │ │
│ │ │
▼ ▼ ▼
库存服务 - Redis预扣减Lua脚本:
// 库存预扣减Lua脚本:仅当可用库存≥请求数量时,原子扣减并返回剩余库存
var stockDeductScript = redis.NewScript(`
local stockKey = KEYS[1]
local reservedStockKey = KEYS[2]
local quantity = tonumber(ARGV[1])
local now = ARGV[2]
-- 获取当前可用库存
local availableStock = tonumber(redis.call('get', stockKey))
if not availableStock then
return -1 -- 库存不存在
end
-- 检查可用库存是否足够
if availableStock < quantity then
return -2 -- 库存不足
end
-- 原子扣减可用库存
redis.call('decrby', stockKey, quantity)
-- 记录预扣减库存(用于超时回滚)
redis.call('hincrby', reservedStockKey, 'reserved', quantity)
redis.call('hset', reservedStockKey, 'expire_time', now + 3600) -- 1小时超时
return availableStock - quantity
`)
优惠券服务 - Redis预扣减Lua脚本:
// 优惠券预扣减Lua脚本:仅当用户拥有该优惠券且未使用时,原子标记为已使用
var couponDeductScript = redis.NewScript(`
local userCouponKey = KEYS[1]
local couponStockKey = KEYS[2]
local couponID = ARGV[1]
local orderID = ARGV[2]
local now = ARGV[3]
-- 检查用户是否拥有该优惠券且未使用
local couponStatus = redis.call('hget', userCouponKey, couponID)
if not couponStatus or couponStatus ~= 'available' then
return -1 -- 优惠券不可用
end
-- 原子标记优惠券为已使用
redis.call('hset', userCouponKey, couponID, 'used')
redis.call('hset', userCouponKey, couponID..':order_id', orderID)
redis.call('hset', userCouponKey, couponID..':use_time', now)
-- 扣减优惠券全局库存
redis.call('decrby', couponStockKey, 1)
return 0
`)
库存服务 - MySQL持久化与同步逻辑:
// Try阶段:记录预扣减日志到MySQL
func (s *StockService) Try(ctx context.Context, req *StockDeductReq) error {
// 1. 执行Redis预扣减
res, err := stockDeductScript.Run(ctx, redisClient,
[]string{
fmt.Sprintf("stock:%d", req.ProductID),
fmt.Sprintf("reserved_stock:%d", req.ProductID),
},
req.Quantity, time.Now().Unix(),
).Result()
if err != nil || res.(int64) < 0 {
return errors.New("库存预扣减失败")
}
// 2. 记录预扣减日志到MySQL
return db.Create(&StockDeductLog{
ProductID: req.ProductID,
OrderID: req.OrderID,
Quantity: req.Quantity,
Status: "reserved",
ExpireTime: time.Now().Add(time.Hour),
}).Error()
}
// Confirm阶段:确认扣减库存到MySQL
func (s *StockService) Confirm(ctx context.Context, req *StockDeductReq) error {
// 1. 更新MySQL实际库存
err := db.Transaction(func(tx *gorm.DB) error {
// 更新产品表库存
if err := tx.Model(&Product{}).Where("id = ?", req.ProductID).
Update("stock", gorm.Expr("stock - ?", req.Quantity)).Error; err != nil {
return err
}
// 更新预扣减日志状态为confirmed
return tx.Model(&StockDeductLog{}).Where("order_id = ?", req.OrderID).
Update("status", "confirmed").Error
})
if err != nil {
return err
}
// 2. 异步删除Redis预扣减记录(可选,到期自动失效)
go func() {
redisClient.Del(ctx, fmt.Sprintf("reserved_stock:%d", req.ProductID))
}()
return nil
}
// Cancel阶段:回滚预扣减库存
func (s *StockService) Cancel(ctx context.Context, req *StockDeductReq) error {
// 1. 回滚Redis库存
redisClient.Eval(ctx, `
local stockKey = KEYS[1]
local reservedStockKey = KEYS[2]
local quantity = tonumber(ARGV[1])
redis.call('incrby', stockKey, quantity)
redis.call('hincrby', reservedStockKey, 'reserved', -quantity)
`, []string{
fmt.Sprintf("stock:%d", req.ProductID),
fmt.Sprintf("reserved_stock:%d", req.ProductID),
}, req.Quantity)
// 2. 更新预扣减日志状态为cancelled
return db.Model(&StockDeductLog{}).Where("order_id = ?", req.OrderID).
Update("status", "cancelled").Error
}
// 订单创建流程
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderReq) (*Order, error) {
// 1. 初始化TCC事务协调器
tccCtx, err := tcc.NewContext(ctx, req.OrderID)
if err != nil {
return nil, err
}
defer func() {
if err != nil {
// 异常回滚
tccCtx.Cancel()
}
}()
// 2. 调用库存服务预扣减
if err = stockService.Try(tccCtx, &StockDeductReq{
ProductID: req.ProductID,
OrderID: req.OrderID,
Quantity: req.Quantity,
}); err != nil {
return nil, err
}
// 3. 调用优惠券服务预扣减
if err = couponService.Try(tccCtx, &CouponDeductReq{
UserID: req.UserID,
CouponID: req.CouponID,
OrderID: req.OrderID,
}); err != nil {
return nil, err
}
// 4. 创建订单记录到MySQL
order := &Order{
UserID: req.UserID,
ProductID: req.ProductID,
Quantity: req.Quantity,
CouponID: req.CouponID,
TotalAmt: req.TotalAmt,
Status: "pending_payment",
}
if err = db.Create(order).Error; err != nil {
return nil, err
}
// 5. 确认所有预扣减操作
if err = tccCtx.Confirm(); err != nil {
return nil, err
}
// 6. 返回订单信息给前端
return order, nil
}
问题 | 解决方案 |
|---|---|
Redis宕机 | 降级为直接查询MySQL |
预扣减超时 | 定时任务自动回滚 |
Binlog同步延迟 | 增加Redis缓存过期时间,优先读Redis |
分布式事务协调失败 | 人工补偿+监控告警 |
通过以上设计,既能保证用户“下单成功即能付款”的体验,又能通过Redis提升性能,同时解决了Redis与MySQL的数据一致性问题。