首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >数据库和缓存一致性问题&做到下单成功即代表可成功付款的体验

数据库和缓存一致性问题&做到下单成功即代表可成功付款的体验

作者头像
王中阳AI编程
发布2026-03-17 19:54:39
发布2026-03-17 19:54:39
2840
举报
文章被收录于专栏:Go语言学习专栏Go语言学习专栏

这篇文章的内容都是基于我们GoFrame微服务电商项目的实践,感兴趣的朋友可以点击查看

问题

下面的问题是咱们组织里同学的提问,咱们可以一起探讨一下,欢迎在文章下面评论:

他说:

  • 想要做到 “成功下单,即代表可以成功付款,不用担心退单” 的这种体验的话。
  • 创建订单 和 优惠券扣减、库存扣减 好像应该用同步调用,因为创建订单后返回、用mq异步等待消费的话,当优惠券或库存服务出问题的时候,用户的体验可能会变成,下单正常 -》 订单创建中 -》 用户等了好一段时间后(优惠券/库存服务扣减失败,重试好多次),最终订单返回创建失败
  • 假如要优化性能的话,优惠券和库存服务确实需要通过redis做预扣减,而且得是 创建订单接口 向两个服务的预扣减接口 发起同步调用,等待一个明确的成功与否。
  • 但是这又回到了上一个问题,该怎么保证redis中和mysql中的库存数据不乱呢?

方案参考(也欢迎大家交流讨论)

核心是结合分布式事务Redis原子操作来设计同步调用流程。

核心设计原则

  1. 同步预扣减:订单服务同步调用库存和优惠券服务的预扣减接口
  2. 原子性保证:使用Redis Lua脚本实现库存/优惠券的原子预扣减
  3. 分布式事务:采用TCC(Try-Confirm-Cancel)模式保证跨服务一致性
  4. 最终一致性:通过异步补偿机制确保Redis与MySQL数据一致

详细实现方案

1. TCC分布式事务流程设计

代码语言:javascript
复制
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│    订单服务     │     │    库存服务     │     │   优惠券服务    │
└─────────────────┘     └─────────────────┘     └─────────────────┘
        │                        │                        │
        ├────── Try(预扣减)────►│                        │
        │                        ├────── Try(预扣减)────►│
        │                        │                        │
        │                        │◄──── 预扣减结果 ────────│
        │◄──── 预扣减结果 ────────│                        │
        │                        │                        │
        │  预扣减成功 → 创建订单  │                        │
        │                        │                        │
        ├──── Confirm(确认扣减)─►│                        │
        │                        ├──── Confirm(确认扣减)─►│
        │                        │                        │
        │                        │◄──── 确认结果 ────────│
        │◄──── 确认结果 ────────│                        │
        │                        │                        │
        │  订单创建完成          │                        │
        │                        │                        │
        │  异常情况处理:        │                        │
        │  - 调用Cancel回滚预扣减 │                        │
        │  - 异步补偿机制        │                        │
        │                        │                        │
        ▼                        ▼                        ▼

2. Redis原子预扣减实现(Lua脚本)

库存服务 - Redis预扣减Lua脚本

代码语言:javascript
复制
// 库存预扣减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脚本

代码语言:javascript
复制
// 优惠券预扣减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
`)

3. MySQL持久化与数据同步

库存服务 - MySQL持久化与同步逻辑

代码语言:javascript
复制
// 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
}

4. Redis与MySQL最终一致性保障

  1. 定时任务补偿
  • 库存服务定时扫描MySQL中的预扣减日志,对超时未确认的记录执行回滚
  • 优惠券服务同样定时清理超时未使用的预扣减记录
  • Binlog同步机制
  • 使用Canal监听MySQL Binlog,实时将库存/优惠券的变更同步到Redis
  • 确保Redis中的数据始终与MySQL保持一致
  • 幂等性设计
  • 所有预扣减、确认、回滚操作都需要设计为幂等
  • 避免重复扣减或重复回滚

5. 订单服务协调流程

代码语言:javascript
复制
// 订单创建流程
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
}

方案优势

  1. 用户体验:同步调用确保下单成功后即可付款,避免后续失败
  2. 性能优化:Redis预扣减比直接查询MySQL快10-100倍
  3. 数据一致性:TCC+Redis原子操作+定时补偿确保数据一致
  4. 可扩展性:各服务独立,便于水平扩展
  5. 防超卖:Redis Lua脚本保证原子预扣减,避免并发超卖

潜在问题与解决方案

问题

解决方案

Redis宕机

降级为直接查询MySQL

预扣减超时

定时任务自动回滚

Binlog同步延迟

增加Redis缓存过期时间,优先读Redis

分布式事务协调失败

人工补偿+监控告警

通过以上设计,既能保证用户“下单成功即能付款”的体验,又能通过Redis提升性能,同时解决了Redis与MySQL的数据一致性问题。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-11-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 王中阳 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题
  • 方案参考(也欢迎大家交流讨论)
    • 核心设计原则
    • 详细实现方案
      • 1. TCC分布式事务流程设计
      • 2. Redis原子预扣减实现(Lua脚本)
      • 3. MySQL持久化与数据同步
      • 4. Redis与MySQL最终一致性保障
      • 5. 订单服务协调流程
    • 方案优势
    • 潜在问题与解决方案
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档