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

一、基本数据准备

  • 1、数据表的创建

    mysql 复制代码
    -- ----------------
    -- 库存表
    -- ----------------
    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的方法

    go 复制代码
    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、模拟并发下单扣减库存

    go 复制代码
    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的锁的机制来实现并发安全

    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、使用行锁,类似这样的

    mysql 复制代码
    SELECT * FROM user WHERE id = 1 FOR UPDATE;
  • 2、在gorm中使用行锁

    go 复制代码
    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、使用悲观锁的另外一种实现方式

    go 复制代码
    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的数据类型

    go 复制代码
    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、具体乐观锁的实现

    go 复制代码
    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、简单的案例实现

    go 复制代码
    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、简单粗暴的实现

    go 复制代码
    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("全部执行完成")
    }
相关推荐
人间打气筒(Ada)3 小时前
「码动四季·开源同行」go语言:如何追踪分布式系统调用链路的问题?
开发语言·golang·开源·分布式链路追踪
古城小栈3 小时前
Go 牵手 ES
elasticsearch·golang·iphone
Lufeidata4 小时前
go语言学习记录-入门阶段2
学习·microsoft·golang
ywf121517 小时前
Go基础之环境搭建
开发语言·后端·golang
好家伙VCC1 天前
**CQRS模式实战:用Go语言构建高并发读写分离架构**在现代分布式系统中,随着业务复杂度的提升和用户量的增长,传统的单数据库模型逐
java·数据库·python·架构·golang
l1o3v1e4ding1 天前
Java网站项目集成GO-FLY开源在线客服系统功能,集成IM即时通信
java·golang·开源
呆萌很1 天前
【GO】创建包练习题
golang
@atweiwei1 天前
基于Go语言构建轻量级微服务框架的设计与实现
开发语言·微服务·golang