Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > go 并发安全扣减库存

go语言里面实现并发安全扣减库存的几种方式小结

作者:水痕01

本文介绍了GORM框架中的几种高级查询技巧,包括随机返回数据的方法,锁机制的使用,Upsert操作,在gorm-gen中使用原生SQL查询这几种方法,具有一定的参考价值,感兴趣的可以了解一下

一、基本数据准备

1、数据表的创建

-- ----------------
-- 库存表
-- ----------------
DROP TABLE IF EXISTS `inventory`;
CREATE TABLE `inventory` (
    `id` int NOT NULL AUTO_INCREMENT primary key COMMENT '主键id',
    `goods_id` int(11) default 1 comment '商品id',
    `stocks` int(11) default 1 comment '商品库存',
    `version` int(11) default 0 comment '商品版本号',
    `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `deleted_at` timestamp NULL DEFAULT NULL COMMENT '软删除时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';

2、根据实体类创建数据模型

3、手动在数据库中插入商品库存数据

4、创建一个基本的连接gorm的方法

package utils
import (
	"fmt"
	_ "github.com/go-sql-driver/mysql"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
	"gorm.io/gorm/schema"
)
var GormDb *gorm.DB
func init() {
	var err error
	sqlStr := "root:123456@tcp(localhost:3306)/gorm_demo?charset=utf8mb4&parseTime=true&loc=Local"
	GormDb, err = gorm.Open(mysql.Open(sqlStr), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
		//DisableForeignKeyConstraintWhenMigrating: true, // 禁止创建外键
		NamingStrategy: schema.NamingStrategy{
			SingularTable: true,
			// 全部的表名前面加前缀
			//TablePrefix: "mall_",
		},
	})
	if err != nil {
		fmt.Println("数据库连接错误", err)
		return
	}
}

二、模拟并发下单

1、模拟并发下单扣减库存

type InventoryDto struct {
	GoodsID int64 `json:"goodsId"` // 商品id
	Num     int64 `json:"num"`     // 下单数量
}
func Sell(ws *sync.WaitGroup) {
	reqList := make([]InventoryDto, 0)
	reqList = append(reqList, InventoryDto{
		GoodsID: 1,
		Num:     1,
	})
	defer ws.Done()
	tx := utils.GormDb.Begin()
	for _, item := range reqList {
		// 扣减库存
		inventoryEntity := model.InventoryEntity{}
		if err := tx.Where("goods_id = ?", item.GoodsID).First(&inventoryEntity).Error; err != nil {
			fmt.Println("查询错误")
			tx.Rollback()
			return
		}
		// 库存减少
		stocks := inventoryEntity.Stocks - item.Num
		if stocks < 0 {
      fmt.Println("库存不足..")
			tx.Rollback()
			return
		}
		fmt.Println("开始扣减库存...")
		if err := tx.Model(&model.InventoryEntity{}).Where("goods_id = ?", item.GoodsID).UpdateColumn("stocks", stocks).Error; err != nil {
			fmt.Println("扣减库存失败", err)
			tx.Rollback()
			return
		}
		fmt.Println("扣减库存成功...")
	}
	tx.Commit()
}
func main() {
	// 开始模拟下单
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go Sell(&wg)
	}
	wg.Wait()
}

2、查看数据库库存信息,执行了20个并发,但是实际没有扣减20个库存

三、使用加锁的方式来实现并发安全

1、使用go的锁的机制来实现并发安全

package main
type InventoryDto struct {
	GoodsID int64 `json:"goodsId"` // 商品id
	Num     int64 `json:"num"`     // 下单数量
}
var m = sync.Mutex{}
func Sell(ws *sync.WaitGroup) {
	reqList := make([]InventoryDto, 0)
	reqList = append(reqList, InventoryDto{
		GoodsID: 1,
		Num:     1,
	})
	defer ws.Done()
	tx := utils.GormDb.Begin()
	for _, item := range reqList {
		m.Lock()
		defer m.Unlock()
		// 扣减库存
		inventoryEntity := model.InventoryEntity{}
		if err := tx.Where("goods_id = ?", item.GoodsID).First(&inventoryEntity).Error; err != nil {
			fmt.Println("查询错误")
			tx.Rollback()
			return
		}
		// 库存减少
		stocks := inventoryEntity.Stocks - item.Num
		if stocks < 0 {
      fmt.Println("库存不足..")
			tx.Rollback()
			return
		}
		fmt.Println("开始扣减库存...")
		if err := tx.Model(&model.InventoryEntity{}).Where("goods_id = ?", item.GoodsID).UpdateColumn("stocks", stocks).Error; err != nil {
			fmt.Println("扣减库存失败", err)
			tx.Rollback()
			return
		}
		fmt.Println("扣减库存成功...")
	}
	tx.Commit()
}
func main() {
	// 开始模拟下单
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go Sell(&wg)
	}
	wg.Wait()
}

2、查看数据库库存这次是每次减少20个了

四、使用mysql的悲观锁来实现并发安全

1、使用行锁,类似这样的

SELECT * FROM user WHERE id = 1 FOR UPDATE;

2、在gorm中使用行锁

package main
type InventoryDto struct {
	GoodsID int64 `json:"goodsId"` // 商品id
	Num     int64 `json:"num"`     // 下单数量
}
func Sell(ws *sync.WaitGroup) {
	reqList := make([]InventoryDto, 0)
	reqList = append(reqList, InventoryDto{
		GoodsID: 1,
		Num:     1,
	})
	defer ws.Done()
	tx := utils.GormDb.Begin()
	for _, item := range reqList {
		// 扣减库存
		inventoryEntity := model.InventoryEntity{}
		if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("goods_id = ?", item.GoodsID).First(&inventoryEntity).Error; err != nil {
			fmt.Println("查询错误")
			tx.Rollback()
			return
		}
		// 库存减少
		stocks := inventoryEntity.Stocks - item.Num
		if stocks < 0 {
      fmt.Println("库存不足..")
			tx.Rollback()
			return
		}
		fmt.Println("开始扣减库存...")
		if err := tx.Model(&model.InventoryEntity{}).Where("goods_id = ?", item.GoodsID).UpdateColumn("stocks", stocks).Error; err != nil {
			fmt.Println("扣减库存失败", err)
			tx.Rollback()
			return
		}
		fmt.Println("扣减库存成功...")
	}
	tx.Commit()
}
func main() {
	// 开始模拟下单
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go Sell(&wg)
	}
	wg.Wait()
}

3、使用悲观锁的另外一种实现方式

package main
type InventoryDto struct {
	GoodsID int64 `json:"goodsId"` // 商品id
	Num     int64 `json:"num"`     // 下单数量
}
func Sell(ws *sync.WaitGroup) {
	reqList := make([]InventoryDto, 0)
	reqList = append(reqList, InventoryDto{
		GoodsID: 1,
		Num:     1,
	})
	defer ws.Done()
	tx := utils.GormDb.Begin()
	for _, item := range reqList {
		fmt.Println("开始扣减库存...")
		result := tx.Model(&model.InventoryEntity{}).
			Where("goods_id = ? and stocks >= ?", item.GoodsID, item.Num).
			Update("stocks", gorm.Expr("stocks - ?", item.Num))
		if result.Error != nil {
			fmt.Println("扣减库存失败", result.Error)
			tx.Rollback()
			return
		}
		fmt.Println("扣减库存成功...")
	}
	tx.Commit()
}
func main() {
	// 开始模拟下单
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go Sell(&wg)
	}
	wg.Wait()
}

五、使用mysql的乐观锁来实现

1、官方插件optimisticlock

2、修改数据库模型version的数据类型

const TableNameInventoryEntity = "inventory"
// InventoryEntity 库存表
type InventoryEntity struct {
	ID        int64          `gorm:"column:id;type:int;primaryKey;autoIncrement:true;comment:主键id" json:"id,string"` // 主键id
	GoodsID   int64          `gorm:"column:goods_id;type:int;default:1;comment:商品id" json:"goodsId"`                 // 商品id
	Stocks    int64          `gorm:"column:stocks;type:int;default:1;comment:商品库存" json:"stocks"`                    // 商品库存
	Version   optimisticlock.Version        `gorm:"column:version;type:int;comment:商品版本号" json:"version"`                           // 商品版本号
	CreatedAt LocalTime      `gorm:"column:created_at;comment:创建时间" json:"createdAt"`                                // 创建时间
	UpdatedAt LocalTime      `gorm:"column:updated_at;comment:更新时间" json:"updatedAt"`                                // 更新时间
	DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;type:timestamp;comment:软删除时间" json:"-"`                        // 软删除时间
}
// TableName InventoryEntity's table name
func (*InventoryEntity) TableName() string {
	return TableNameInventoryEntity
}

3、具体乐观锁的实现

package main
type InventoryDto struct {
	GoodsID int64 `json:"goodsId"`
	Num     int64 `json:"num"`
}
func Sell(ws *sync.WaitGroup) {
	defer ws.Done()
	reqList := []InventoryDto{
		{GoodsID: 1, Num: 1},
	}
	for _, item := range reqList {
		for {
			entity := model.InventoryEntity{}
			if err := utils.GormDb.Where("goods_id = ?", item.GoodsID).First(&entity).Error; err != nil {
				fmt.Println("查询错误:", err)
				break
			}
			stocks := entity.Stocks - item.Num
			if stocks < 0 {
				fmt.Println("库存不足..")
				break
			}
			fmt.Println("开始扣减库存...")
			result := utils.GormDb.Model(&model.InventoryEntity{}).
				Where("goods_id = ? AND version = ?", item.GoodsID, entity.Version).
				Updates(map[string]interface{}{
					"stocks":  stocks,
					"version": gorm.Expr("version + 1"),
				})
			if result.Error != nil {
				fmt.Println("更新错误:", result.Error)
			}
			if result.RowsAffected != 0 {
				break
			}
		}
	}
}
func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go Sell(&wg)
	}
	wg.Wait()
}

六、使用redis分布式锁来实现

1、文档地址

2、简单的案例实现

package main
import (
	"fmt"
	"sync"
	"time"
	goredislib "github.com/go-redis/redis/v8"
	"github.com/go-redsync/redsync/v4"
	"github.com/go-redsync/redsync/v4/redis/goredis/v8"
)
func main() {
	// 配置redis连接
	client := goredislib.NewClient(&goredislib.Options{
		Addr: "localhost:6379",
	})
	// 关闭客户端连接
	defer client.Close()
	// 创建 redsync 需要的池
	pool := goredis.NewPool(client)
	// 创建 redsync 实例
	rs := redsync.New(pool)
	// 设置锁名称
	mutexname := "my-global-mutex"
	gNum := 2
	var wg sync.WaitGroup
	wg.Add(gNum)
	for i := 0; i < gNum; i++ {
		go func(id int) {
			defer wg.Done()
			mutex := rs.NewMutex(mutexname,
				redsync.WithExpiry(10*time.Second),
				redsync.WithTries(50), // 重试直到成功
				redsync.WithRetryDelay(200*time.Millisecond))
			fmt.Printf("goroutine %d: 开始获取锁...\n", id)
			if err := mutex.Lock(); err != nil {
				fmt.Printf("goroutine %d: 获取锁失败: %v\n", id, err)
				return
			}
			fmt.Printf("goroutine %d: 获取锁成功...\n", id)
			time.Sleep(time.Second * 5)
			fmt.Printf("goroutine %d: 开始释放锁...\n", id)
			if ok, err := mutex.Unlock(); !ok || err != nil {
				fmt.Printf("goroutine %d: 释放锁失败: %v\n", id, err)
				return
			}
			fmt.Printf("goroutine %d: 释放锁成功\n", id)
		}(i)
	}
	wg.Wait()
	fmt.Println("主进程结束..")
}

3、简单粗暴的实现

package main
import (
	"fmt"
	"gin-admin-api/model"
	goredislib "github.com/go-redis/redis/v8"
	"github.com/go-redsync/redsync/v4"
	"github.com/go-redsync/redsync/v4/redis/goredis/v8"
	"sync"
)
type InventoryDto1 struct {
	GoodsID int64 `json:"goodsId"`
	Num     int64 `json:"num"`
}
// DeductStock1 使用Redis分布式锁扣减库存(优化版)
func DeductStock1(ws *sync.WaitGroup) {
	client := goredislib.NewClient(&goredislib.Options{
		Addr: "localhost:6379",
	})
	pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...)
	rs := redsync.New(pool)
	// 锁名要加上单号
	mutex := rs.NewMutex("cell_order")
	if err := mutex.Lock(); err != nil {
		fmt.Println("获取锁失败")
		return
	}
	tx := utils.GormDb.Begin()
	defer ws.Done()
	reqList := []InventoryDto1{
		{GoodsID: 1, Num: 2},
	}
	for _, item := range reqList {
		inventoryEntity := model.InventoryEntity{}
		if err := utils.GormDb.Where("goods_id = ?", item.GoodsID).First(&inventoryEntity).Error; err != nil {
			fmt.Println("查询错误")
			tx.Rollback()
			break
		}
		if inventoryEntity.Stocks < item.Num {
			fmt.Printf("库存不足,剩余%d个,按实际扣减%d个\n", inventoryEntity.Stocks, item.Num)
			tx.Rollback()
			break
		}
		fmt.Println("开始扣减库存...")
		stocks := inventoryEntity.Stocks - item.Num
		result := tx.Model(&model.InventoryEntity{}).
			Where("goods_id = ?", item.GoodsID).
			Updates(map[string]interface{}{
				"stocks": stocks,
			})
		if result.Error != nil {
			fmt.Println("扣减库存失败", result.Error)
			tx.Rollback()
			break
		}
	}
	tx.Commit() // 先提交事务
	mutex.Unlock()
}
func main() {
	wg := sync.WaitGroup{}
	// 模拟20并发
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go DeductStock1(&wg)
	}
	wg.Wait()
	fmt.Println("全部执行完成")
}

到此这篇关于go语言里面实现并发安全扣减库存的几种方式小结的文章就介绍到这了,更多相关go 并发安全扣减库存内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文