go_分布式锁

场景:高并发下商品库存扣减不正确的问题

目录

一、通过go语言自带的锁(sync.Mutex)解决并发问题

二、mysql的for update语句实现悲观锁(行锁)

三、mysql的乐观锁实现

四、基于redsync分布式锁实现


一、通过go语言自带的锁(sync.Mutex)解决并发问题

// 库存扣减  本地事务(购物车)
// 数据库基本的一个应用场景:数据库事务(数据的一致性)
// 并发情况下,可能会出现超卖,出现数据不一致的问题--》解决方法:加锁,分布式锁
var m sync.Mutex

func (*InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
    tx := global.DB.Begin() //开启事务
    m.Lock()   //获取锁  这把锁有问题吗? 假设有10w的并发,这里并不是请求的同件商品 ;这个锁就没有问题了吗?
    for _, goodInfo := range req.GoodsInfo {

       var inv model.Inventory
       if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
          tx.Rollback() //回滚到之前的操作
          return nil, status.Errorf(codes.NotFound, "该商品库存信息不存在")
       }
       if inv.Stocks < goodInfo.Num {
          tx.Rollback() //回滚到之前的操作
          return nil, status.Errorf(codes.ResourceExhausted, "该商品库存不足")
       }
       //扣减
       inv.Stocks -= goodInfo.Num
       tx.Save(&inv)

    }
    tx.Commit() //手动提交事务
    m.Unlock()  //释放锁
    return &emptypb.Empty{}, nil
}
func TestSell(wg *sync.WaitGroup) {
    defer wg.Done()
    _, err := InvClient.Sell(context.Background(), &proto.SellInfo{
       GoodsInfo: []*proto.GoodsInvInfo{
          &proto.GoodsInvInfo{GoodsId: 421, Num: 1},
          //&proto.GoodsInvInfo{GoodsId: 422, Num: 100},
       }})
    if err != nil {
       panic(err)
    }
    fmt.Println("库存扣减成功")
}

func main() {
    Init()
    //TestBannerList()
    //TestSetInv(421, 9)
    //for i := 421; i < 841; i++ {
    // TestSetInv(int32(i), 100)
    //}

    //TestInvDetail(422)
    var wg sync.WaitGroup
    wg.Add(80)
    for i := 0; i < 80; i++ {
       go TestSell(&wg)
    }
    wg.Wait()

    conn.Close()

}

在扣减库存这个功能实现中,需要注意:

因在这个功能中使用到了数据库事务操作,所以在go并发协程获取数据库中同一件商品的库存数量时,需要 在事务从数据库获取到数据前,获取到锁(m.Lock),在事务提交到数据库之后再释放锁(tx.commit之后m.Unlock).

但在这个sell()功能中,仍然存在问题:多个不同的商品调用同一个sell方法,不同商品之间也会造成阻塞排队,这是有问题的。

二、mysql的for update语句实现悲观锁(行锁)

 实现悲观锁属于数据库层面的操作

 gorm实现for update 悲观锁

位置:gorm的高级查询

链接:高级查询 | GORM - The fantastic ORM library for Golang, aims to be developer friendly.

说明:

悲观并发控制实际上是“先取锁再访问”的保守策略,为数据处理的安全提供了保证。但是在效率方面,处理加锁的机制会让数据库产生额外的开销,还有增加产生死锁的机会。另外还会降低并行性,一个事务如果锁定了某行数据,其他事务就必须等待该事务处理完才可以处理那行数据。

三、mysql的乐观锁实现

原理:

方法有2种:

1、使用数据版本(version)的方式

这是乐观锁最常用的一种实现 方式。何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。

2、使用时间戳的方式

同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳 (timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。

这里使用数据版本的方式:

// 库存扣减  本地事务(购物车)
// 数据库基本的一个应用场景:数据库事务(数据的一致性)
// 并发情况下,可能会出现超卖,出现数据不一致的问题--》解决方法:加锁,分布式锁
//var m sync.Mutex

func (*InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
    tx := global.DB.Begin() //开启事务
    //m.Lock()                //获取锁  这把锁有问题吗? 假设有10w的并发,这里并不是请求的同一件商品 ;这个锁就没有问题了吗?
    for _, goodInfo := range req.GoodsInfo {

       var inv model.Inventory
       //悲观锁实现
       //if result := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
       // tx.Rollback() //回滚到之前的操作
       // return nil, status.Errorf(codes.NotFound, "该商品库存信息不存在")
       //}
       for {
          //乐观锁实现
          if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
             tx.Rollback() //回滚到之前的操作
             return nil, status.Errorf(codes.NotFound, "该商品库存信息不存在")
          }

          if inv.Stocks < goodInfo.Num {
             tx.Rollback() //回滚到之前的操作
             return nil, status.Errorf(codes.ResourceExhausted, "该商品库存不足")
          }
          //扣减
          inv.Stocks -= goodInfo.Num

          //update inventory set stocks=stocks-1,version=version+1 where goods=goods and version=version
          //这种写法是有瑕疵的,为什么?
          //零值问题,对于int类型来说,默认值是0,这种会被gorm给忽略掉
          if result := tx.Model(&model.Inventory{}).Select("Stocks", "Version").Where("goods = ? and version = ?", goodInfo.GoodsId, inv.Version).Updates(model.Inventory{Stocks: inv.Stocks, Version: inv.Version + 1}); result.RowsAffected == 0 {
             zap.S().Info("扣减库存失败")
          } else {
             break
          }
          //tx.Save(&inv)
       }

    }
    tx.Commit() //手动提交事务
    //m.Unlock()  //释放锁
    return &emptypb.Empty{}, nil
}

乐观锁实现的关键语句为:

//update inventory set stocks=stocks-1,version=version+1 where goods=goods and version=version
//这种写法是有瑕疵的,为什么?
//零值问题,对于int类型来说,默认值是0,这种会被gorm给忽略掉
if result := tx.Model(&model.Inventory{}).Select("Stocks", "Version").Where("goods = ? and version = ?", goodInfo.GoodsId, inv.Version).Updates(model.Inventory{Stocks: inv.Stocks, Version: inv.Version + 1}); result.RowsAffected == 0 {
    zap.S().Info("扣减库存失败")
} else {
    break
}

四、基于redsync分布式锁实现

使用redis的redsync分布式锁实现,此处需要注意 setnx 和 redlock

redis的setnx、redlock的源码-CSDN博客

package main

import (
    "fmt"
    "github.com/go-redsync/redsync/v4"
    "github.com/go-redsync/redsync/v4/redis/goredis/v9"
    goredislib "github.com/redis/go-redis/v9"
    "sync"
    "time"
)

func main() {
    // Create a pool with go-redis (or redigo) which is the pool redisync will
    // use while communicating with Redis. This can also be any pool that
    // implements the `redis.Pool` interface.
    client := goredislib.NewClient(&goredislib.Options{
       Addr: "192.168.1.7:6379",
    })
    pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...)

    // Create an instance of redisync to be used to obtain a mutual exclusion
    // lock.
    rs := redsync.New(pool)

    // Obtain a new mutex by using the same name for all instances wanting the
    // same lock.
    gNum := 2
    mutexname := "421"
    var wg sync.WaitGroup
    wg.Add(gNum)

    for i := 0; i < gNum; i++ {
       go func() {
          defer wg.Done()

          mutex := rs.NewMutex(mutexname)

          // Obtain a lock for our given mutex. After this is successful, no one else
          // can obtain the same lock (the same mutex name) until we unlock it.
          fmt.Println("开始获取锁")
          if err := mutex.Lock(); err != nil {
             panic(err)
          }

          fmt.Println("获取锁成功")
          // Do your work that requires the lock.

          time.Sleep(time.Second * 5)

          fmt.Println("开始释放锁")
          // Release the lock so other processes or threads can obtain a lock.
          if ok, err := mutex.Unlock(); !ok || err != nil {
             panic("unlock failed")
          }
          fmt.Println("释放锁成功")

       }()
    }
    wg.Wait()

}

将redsync集成到库存服务中:

func (*InventoryServer) Sell(ctx context.Context, req *proto.SellInfo) (*emptypb.Empty, error) {
    initialize.InitRedis()
    tx := global.DB.Begin() //开启事务
    //m.Lock()                //获取锁  这把锁有问题吗? 假设有10w的并发,这里并不是请求的同一件商品 ;这个锁就没有问题了吗?
    for _, goodInfo := range req.GoodsInfo {

       var inv model.Inventory
       //悲观锁实现
       //if result := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
       // tx.Rollback() //回滚到之前的操作
       // return nil, status.Errorf(codes.NotFound, "该商品库存信息不存在")
       //}
       //for {

       mutex := global.RS.NewMutex(fmt.Sprintf("goods_%d", goodInfo.GoodsId))
       if err := mutex.Lock(); err != nil {
          return nil, status.Errorf(codes.Internal, "获取redis分布式锁异常")
       }

       if result := global.DB.Where(&model.Inventory{Goods: goodInfo.GoodsId}).First(&inv); result.RowsAffected == 0 {
          tx.Rollback() //回滚到之前的操作
          return nil, status.Errorf(codes.NotFound, "该商品库存信息不存在")
       }

       if inv.Stocks < goodInfo.Num {
          tx.Rollback() //回滚到之前的操作
          return nil, status.Errorf(codes.ResourceExhausted, "该商品库存不足")
       }
       //扣减
       inv.Stocks -= goodInfo.Num

       tx.Save(&inv)

       if ok, err := mutex.Unlock(); !ok || err != nil {
          return nil, status.Errorf(codes.Internal, "释放redis分布式锁异常")
       }

       //update inventory set stocks=stocks-1,version=version+1 where goods=goods and version=version
       //这种写法是有瑕疵的,为什么?
       //零值问题,对于int类型来说,默认值是0,这种会被gorm给忽略掉
       //if result := tx.Model(&model.Inventory{}).Select("Stocks", "Version").Where("goods = ? and version = ?", goodInfo.GoodsId, inv.Version).Updates(model.Inventory{Stocks: inv.Stocks, Version: inv.Version + 1}); result.RowsAffected == 0 {
       // zap.S().Info("扣减库存失败")
       //} else {
       // break
       //}
       //tx.Save(&inv)
       //}

    }
    tx.Commit() //手动提交事务
    //m.Unlock()  //释放锁
    return &emptypb.Empty{}, nil
}

参考资料:

MySql悲观锁(行锁)和乐观锁_mysql 乐观锁-CSDN博客

【MySQl】MySQl中的乐观锁是怎么实现的_mysql 乐观锁-CSDN博客

  • 20
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值