You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Golang并发请求下商品库存扣减异常问题求助

并发场景下商品库存超卖问题解决

问题描述

我的项目支持用户购买商品,要求商品库存需大于等于购买请求的数量;若满足条件,先保存购买记录,再更新对应商品的库存。但当通过Golang应用发送20个并发请求(每个请求购买20件库存为100的A商品)时,出现异常:生成了20条购买记录,库存被更新为60,而正常情况下应在5次请求后因库存不足报错。以下是处理请求的方法(仅调用GORM的保存和更新操作),无法调整方法实现预期效果,该如何解决?

var wg sync.WaitGroup
wg.Add(1)

errs := make(chan error, 1)
go func(ctx context.Context, request *dto.CreateTicketPurchaseRequest) {
    defer wg.Done()
    productStockCount, err := t.stockRepo.GetTicketStockCount(productId) // first check available stock count
    if err != nil {
        errs <- err
        return
    }

    if productStockCount.StockCount < payload.Quantity {
        err = fmt.Errorf("no enough products")
        errs <- err
        return
    }

    u := new(entity.TicketPurchases)
    u.Quantity = payload.Quantity
    u.UserId = payload.UserId
    u.StockId = productId
    err = t.purchaseRepo.PurchaseTicket(u) // if stock count is ok, then purchase product
    if err != nil {
        errs <- err
        return
    }

    productStockCount.StockCount = productStockCount.StockCount - u.Quantity
    err = t.stockRepo.RemoveStockCount(productStockCount) // update stock count for product

    if err != nil {
        errs <- err
        return
    }
    close(errs)

}(ctx, payload)
wg.Wait()
if err = <-errs; err != nil {
    return c.JSON(http.StatusBadRequest, nil)
}

解决方案

1. 数据库事务+行级悲观锁

核心思路是在查询库存时就给数据行加锁,阻止其他事务读取或修改,确保整个购买流程(库存检查→创建记录→扣减库存)的原子性。

  • 修改库存查询方法,添加行级锁:
// stockRepo.GetTicketStockCount 实现示例
func (r *stockRepo) GetTicketStockCount(productId uint) (*entity.ProductStock, error) {
    var stock entity.ProductStock
    // 使用FOR UPDATE加锁,其他事务必须等待当前事务提交后才能操作该行
    err := r.db.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", productId).First(&stock).Error
    return &stock, err
}
  • 将整个业务逻辑包裹在数据库事务中:
tx := t.db.Begin()
defer func() {
    if r := recover(); r != nil {
        tx.Rollback()
    }
}()

if tx.Error != nil {
    errs <- tx.Error
    return
}

// 加锁查询库存,避免并发篡改
productStockCount, err := t.stockRepo.WithTx(tx).GetTicketStockCount(productId)
if err != nil {
    tx.Rollback()
    errs <- err
    return
}

if productStockCount.StockCount < payload.Quantity {
    tx.Rollback()
    errs <- fmt.Errorf("no enough products")
    return
}

// 创建购买记录
u := new(entity.TicketPurchases)
u.Quantity = payload.Quantity
u.UserId = payload.UserId
u.StockId = productId
if err := t.purchaseRepo.WithTx(tx).PurchaseTicket(u); err != nil {
    tx.Rollback()
    errs <- err
    return
}

// 扣减库存
productStockCount.StockCount -= u.Quantity
if err := t.stockRepo.WithTx(tx).RemoveStockCount(productStockCount); err != nil {
    tx.Rollback()
    errs <- err
    return
}

if err := tx.Commit().Error; err != nil {
    errs <- err
    return
}
close(errs)

注:需要让Repo支持传入事务对象(实现WithTx方法),GORM可通过tx.Scopes()或直接传递tx实例来复用事务上下文。

2. 乐观锁(版本号机制)

通过给库存表添加版本号字段,每次更新时校验版本号,确保只有当前版本的记录被修改,避免并发覆盖。适合并发量中等的场景。

  • 库存实体添加版本号字段:
type ProductStock struct {
    ID         uint
    StockCount int
    Version    int // 乐观锁版本号,每次更新自增1
}
  • 修改库存扣减方法,加入版本号校验:
// stockRepo.RemoveStockCount 实现示例
func (r *stockRepo) RemoveStockCount(stock *entity.ProductStock) error {
    // GORM会自动处理版本号校验,若版本不匹配则返回ErrRecordNotFound
    return r.db.Model(stock).Where("version = ?", stock.Version).Updates(map[string]interface{}{
        "stock_count": stock.StockCount,
        "version":     stock.Version + 1,
    }).Error
}
  • 业务逻辑中增加重试机制处理并发冲突:
maxRetries := 3
var err error

for i := 0; i < maxRetries; i++ {
    productStockCount, err := t.stockRepo.GetTicketStockCount(productId)
    if err != nil {
        errs <- err
        return
    }

    if productStockCount.StockCount < payload.Quantity {
        errs <- fmt.Errorf("no enough products")
        return
    }

    // 创建购买记录
    u := new(entity.TicketPurchases)
    u.Quantity = payload.Quantity
    u.UserId = payload.UserId
    u.StockId = productId
    if err := t.purchaseRepo.PurchaseTicket(u); err != nil {
        errs <- err
        return
    }

    productStockCount.StockCount -= u.Quantity
    err = t.stockRepo.RemoveStockCount(productStockCount)
    
    // 更新成功则结束流程
    if err == nil {
        close(errs)
        return
    }

    // 乐观锁冲突(版本不匹配)则重试
    if errors.Is(err, gorm.ErrRecordNotFound) {
        continue
    }

    errs <- err
    return
}

errs <- fmt.Errorf("too many concurrent requests, please try again later")

3. 原子更新语句(数据库层面直接校验+扣减)

跳过先查询库存的步骤,用一条SQL完成库存检查和扣减,直接利用数据库的原子性保证操作安全,是最高效的方案。

  • 新增库存扣减方法:
// stockRepo.TryDeductStock 尝试扣减库存,返回是否成功
func (r *stockRepo) TryDeductStock(productId uint, quantity int) (bool, error) {
    result := r.db.Model(&entity.ProductStock{}).
        Where("id = ? AND stock_count >= ?", productId, quantity).
        UpdateColumn("stock_count", gorm.Expr("stock_count - ?", quantity))
    
    if result.Error != nil {
        return false, result.Error
    }
    // RowsAffected>0说明扣减成功,否则库存不足
    return result.RowsAffected > 0, nil
}
  • 调整业务逻辑:
// 先尝试原子扣减库存
success, err := t.stockRepo.TryDeductStock(productId, payload.Quantity)
if err != nil {
    errs <- err
    return
}
if !success {
    errs <- fmt.Errorf("no enough products")
    return
}

// 库存扣减成功后创建购买记录
u := new(entity.TicketPurchases)
u.Quantity = payload.Quantity
u.UserId = payload.UserId
u.StockId = productId
err = t.purchaseRepo.PurchaseTicket(u)
if err != nil {
    // 这里需要添加补偿机制,比如回滚库存或记录异常待人工处理
    t.stockRepo.AddStock(productId, payload.Quantity)
    errs <- err
    return
}

close(errs)

内容的提问来源于stack exchange,提问作者oakkose

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 08:03:33