Jzin 2023-02-03 21:32 采纳率: 55%
浏览 65
已结题

go语言gorm事务中使用redsync锁,关于mysql事务的bug

【mysql库存不一致性 20个用户同样对一件商品买了2件 结果扣减的数量不对】
有注释写的详细

模拟高并发扣减库存
批量扣减库存 - 一个用户开始扣减 要么全成功 要么全失败 不能有一半扣减成功一半扣减失败(事务)

这个bug好几天没搞明白了,模拟20个用户同时批量扣减2件 只有第一件有bug
已知事务的bug 移除事务会恢复正常

img

package main

import (
    goredislib "github.com/go-redis/redis/v8"
    "github.com/go-redsync/redsync/v4"
    "github.com/go-redsync/redsync/v4/redis/goredis/v8"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
    "gorm.io/gorm/schema"

    "fmt"
    "log"
    "os"
    "sync"
    "time"
)

//表结构自己生成一下并添加数据
/*
添加数据: Goods:421,Stocks:100
         Goods:422,Stocks:100
         Goods:423,Stocks:100
         Goods:424,Stocks:100
*/
type BaseModel struct {
    ID        int32          `gorm:"primary_key;comment:ID" json:"id"`
    CreatedAt time.Time      `gorm:"column:add_time;comment:创建时间" json:"-"`
    UpdatedAt time.Time      `gorm:"column:update_time;comment:更新时间" json:"-"`
    DeletedAt gorm.DeletedAt `gorm:"comment:删除时间" json:"-"`
    IsDeleted bool           `gorm:"comment:是否删除" json:"-"`
}
type Inventory struct {
    BaseModel
    Goods   int32 `gorm:"type:int;index;comment:商品id"`
    Stocks  int32 `gorm:"type:int;comment:仓库"`
    Version int32 `gorm:"type:int;comment:分布式锁-乐观锁"` //这个没用到  可以不要
}

var DB *gorm.DB
var RedisRs *redsync.Redsync

func InitDB() {
    //自己的mysql   辛苦一下建个数据库:utf8mb4
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        "mysql用户名", "mysql密码", "mysql的IP建议localhost", mysql端口建议3306, "数据库名称")
    newLogger := logger.New(
        log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容——译者注)
        logger.Config{
            SlowThreshold: time.Second, // 慢 SQL 阈值
            //LogLevel:      logger.Info, // 日志级别
            LogLevel: logger.Silent, // 日志级别
            //IgnoreRecordNotFoundError: true,        // 忽略ErrRecordNotFound(记录未找到)错误
            Colorful: true, // 禁用彩色打印
        },
    )
    // 参考 https://github.com/go-sql-driver/mysql#dsn-data-source-name 获取详情
    var err error
    DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
        NamingStrategy: schema.NamingStrategy{
            SingularTable: true,
        },
        Logger: newLogger,
    })
    if err != nil {
        panic(err)
    }
}

//初始化redis
func InitRedis() {
    client := goredislib.NewClient(&goredislib.Options{
        //自己的redis
        Addr: fmt.Sprintf("%s:%d", "IP", Port),
    })
    pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...)
    RedisRs = redsync.New(pool)
}

type GoodsInvInfo struct {
    GoodsId int32
    Num     int32
}

//批量扣减库存 - 一个用户开始扣减   要么全成功  要么全失败   不能有一半扣减成功一半扣减失败(事务)
func inventory(goods []GoodsInvInfo) {
    //事务开始
    tx := DB.Begin()
    for _, good := range goods {
        //redis锁初始化
        mutex := RedisRs.NewMutex(fmt.Sprintf("goods_%d", good.GoodsId))
        //获取锁
        if err := mutex.Lock(); err != nil {
            //return nil, status.Errorf(codes.Internal, "获取redis分布式锁异常")
            panic(err)
        }
        //库存信息
        var inv Inventory
        //查询库存是否存在
        if result := DB.Where(&Inventory{Goods: good.GoodsId}).First(&inv); result.RowsAffected == 0 {
            panic("库存不存在")
        }
        //查询库存是否充足
        if inv.Stocks < good.Num {
            panic("库存不足")
        }
        //扣减库存
        inv.Stocks -= good.Num
        tx.Save(&inv)
        //释放锁
        if ok, err := mutex.Unlock(); !ok || err != nil {
            tx.Rollback() //回滚之前的操作
            panic(err)
        }

        /*
            这里就会有bug了
            在扣减第一件商品后另一个用户在获取第一件商品的时候数量还是扣减之前的    为什么???
            只有第一件会发生扣减少的情况   其他的商品不会
            只有第一件?
        */
    }
    tx.Commit()
}

func main() {
    //初始化redis 和 mysql
    InitDB()
    InitRedis()
    //设置20个协程来模拟用户
    gNum := 20
    var wg sync.WaitGroup
    wg.Add(gNum)
    for i := 0; i < gNum; i++ {
        go func() {
            defer wg.Done()
            //每个用户批量购买商品2件
            inventory([]GoodsInvInfo{
                {GoodsId: 421, Num: 2},
                {GoodsId: 422, Num: 2},
                {GoodsId: 423, Num: 2},
                {GoodsId: 424, Num: 2},
            })
            fmt.Println("扣减成功")
        }()
    }
    wg.Wait()
}
  • 写回答

4条回答 默认 最新

  • 社区专家-Monster-XH 2023-02-03 22:18
    关注

    来自chatgtp给的示例:可以通过分布式锁解决。分布式锁最常见的方式是使用数据库事务或乐观锁,但这种方法在高并发环境下性能不够优秀,可以使用 Redis 实现分布式锁。

    在代码中,可以使用 go-redsync 库来管理分布式锁。每当扣减库存时,先尝试获取 Redis 分布式锁,如果获取成功,再去扣减库存,并在操作完成后释放锁。

    在获取锁和扣减库存的过程中,可以使用 Go 协程+Go WaitGroup 来实现并发操作。

    下面是代码示例:

    
    var mutex = redsync.New([]redsync.Pool{goredis.New(goredislib.NewClient(&goredislib.Options{
    Addr: "localhost:6379",
    Password: "",
    DB: 0,
    }))})
     
    func decreaseInventory(goodsID, count int32) {
    // 创建分布式锁
    lock := mutex.NewMutex("inventory_lock_" + strconv.Itoa(int(goodsID)))
    defer lock.Unlock()
     
    go
     
    // 获取锁
    if err := lock.Lock(); err != nil {
        return
    }
     
    // 扣减库存
    tx := DB.Begin()
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        }
    }()
     
    if err := tx.Error; err != nil {
        tx.Rollback()
        return
    }
     
    var inventory Inventory
    if err := tx.Where("goods = ?", goodsID).First(&inventory).Error; err != nil {
        tx.Rollback()
        return
    }
     
    if inventory.Stocks < count {
        tx.Rollback()
        return
    }
     
    if err := tx.Model(&inventory).Update("stocks", gorm.Expr("stocks - ?", count)).Error; err != nil {
        tx.Rollback()
        return
    }
     
    tx.Commit()
     
    }
     
     
    
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论
查看更多回答(3条)

报告相同问题?

问题事件

  • 系统已结题 2月11日
  • 已采纳回答 2月3日
  • 修改了问题 2月3日
  • 创建了问题 2月3日

悬赏问题

  • ¥15 微服务假死,一段时间后自动恢复,如何排查处理
  • ¥15 cplex运行后参数报错是为什么
  • ¥15 之前不小心删了pycharm的文件,后面重新安装之后软件打不开了
  • ¥15 vue3获取动态宽度,刷新后动态宽度值为0
  • ¥15 升腾威讯云桌面V2.0.0摄像头问题
  • ¥15 关于Python的会计设计
  • ¥15 聚类分析 设计k-均值算法分类器,对一组二维模式向量进行分类。
  • ¥15 stm32c8t6工程,使用hal库
  • ¥15 找能接spark如图片的,可议价
  • ¥15 博通raid 的写入速度很高也很低