• 解决库存超卖问题


    前言

    在并发的情况下扣减库存会出现库存超卖的现象。
    这里使用golang进行讲解,数据库操作使用gorm + mysql。
    先运行下面的代码将测试表和数据创建好。

    package main
    
    import (
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/logger"
    	"log"
    	"os"
    	"time"
    )
    
    type Inventory struct {
    	gorm.Model
    	ID  uint `gorm:"primarykey"`
    	Num *int `gorm:"type:bigint not null;default:20"`
    }
    
    func (i Inventory) TableName() string {
    	return "inventory"
    }
    
    func getConnect() *gorm.DB {
    	//换成自己的数据库连接
    	dsn := "root:Xrx@1994113@tcp(127.0.0.1:3306)/proxy?charset=utf8mb4&parseTime=True&loc=Local"
    	newLogger := logger.New(
    		//os.Stdout 标准输出,控制台打印
    		// 以\r\n来作为打印间隔
    		// log.LstdFlags 前面这串: 2022/08/13 15:22:34
    		log.New(os.Stdout, "\r\n", log.LstdFlags),
    		logger.Config{
    			SlowThreshold:             time.Second, // 慢 SQL 阈值
    			LogLevel:                  logger.Info, // 日志级别
    			IgnoreRecordNotFoundError: true,        // 是否忽略ErrRecordNotFound(记录未找到)错误
    			Colorful:                  true,        //是否开启彩色打印
    		},
    	)
    
    	// 全局模式
    	open, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    		//禁止创建外键
    		DisableForeignKeyConstraintWhenMigrating: true,
    		Logger:                                   newLogger,
    	})
    	if err != nil {
    		panic(err)
    	}
    	return open
    }
    
    func CreateTable(connect *gorm.DB, model interface{}) {
    
    	//迁移时设置引擎和默认字符集
    	err := connect.Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET UTF8").AutoMigrate(model)
    	if err != nil {
    		panic(err)
    	}
    }
    
    func main() {
    	connect := getConnect()
    	CreateTable(connect, &Inventory{})
    	num := 20
    	connect.Create(&[]Inventory{
    		{ID: 1, Num: &num},
    		{ID: 2, Num: &num},
    		{ID: 3, Num: &num},
    		{ID: 4, Num: &num},
    	})
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    超卖复现

    由于下面例子执行速度过快,20个修改前的查询数据结果差不多,导致20个协程减库存成功。但是数据值只减少了1。导致多卖出19件。

    package main
    
    import (
    	"fmt"
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/logger"
    	"log"
    	"math/rand"
    	"os"
    	"sync"
    	"time"
    )
    
    type Inventory struct {
    	gorm.Model
    	ID  uint `gorm:"primarykey"`
    	Num *int `gorm:"type:bigint not null;default:20"`
    }
    
    func (i Inventory) TableName() string {
    	return "inventory"
    }
    
    func getConnect() *gorm.DB {
    	//换成自己的数据库连接
    	dsn := "root:Xrx@1994113@tcp(127.0.0.1:3306)/proxy?charset=utf8mb4&parseTime=True&loc=Local"
    	newLogger := logger.New(
    		//os.Stdout 标准输出,控制台打印
    		// 以\r\n来作为打印间隔
    		// log.LstdFlags 前面这串: 2022/08/13 15:22:34
    		log.New(os.Stdout, "\r\n", log.LstdFlags),
    		logger.Config{
    			SlowThreshold:             time.Second, // 慢 SQL 阈值
    			LogLevel:                  logger.Info, // 日志级别
    			IgnoreRecordNotFoundError: true,        // 是否忽略ErrRecordNotFound(记录未找到)错误
    			Colorful:                  true,        //是否开启彩色打印
    		},
    	)
    
    	// 全局模式
    	open, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    		//禁止创建外键
    		DisableForeignKeyConstraintWhenMigrating: true,
    		Logger:                                   newLogger,
    	})
    	if err != nil {
    		panic(err)
    	}
    	return open
    }
    
    func main() {
    	connect := getConnect()
    	var wg sync.WaitGroup
    	wg.Add(23)
    	for i := 0; i < 23; i++ {
    		go func() {
    			id := 2
    			inv := Inventory{}
    			connect.Where(Inventory{ID: uint(id)}).Find(&inv)
    			//判断是否查询,异常,自己去完善,略......
    			if *inv.Num < 1 {
    				fmt.Println("库存不足无法扣除")
    				return
    			}
               
               //随机休眠,模拟同时查询到数据,不同时更改的情况
    			time.Sleep(time.Millisecond * time.Duration(rand.Intn(1000)))
    			result := connect.Model(&Inventory{}).Where("id = ?", id).Update("num", *inv.Num-1)
    			if result.Error != nil || result.RowsAffected == 0{
    				fmt.Println("库存扣减失败")
    			} else {
    				fmt.Println("库存扣减成功")
    			}
    
    			defer wg.Done()
    		}()
    	}
    	wg.Wait()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    使用go自带的锁解决超卖

    自带的锁可以解决单体应用下的超卖问题,如果是分布式的就无能为力了。
    下面例子会有三个扣除失败

    package main
    
    import (
    	"fmt"
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/logger"
    	"log"
    	"os"
    	"sync"
    	"time"
    )
    
    type Inventory struct {
    	gorm.Model
    	ID  uint `gorm:"primarykey"`
    	Num *int `gorm:"type:bigint not null;default:20"`
    }
    
    func (i Inventory) TableName() string {
    	return "inventory"
    }
    
    func getConnect() *gorm.DB {
    	//换成自己的数据库连接
    	dsn := "root:Xrx@1994113@tcp(127.0.0.1:3306)/proxy?charset=utf8mb4&parseTime=True&loc=Local"
    	newLogger := logger.New(
    		//os.Stdout 标准输出,控制台打印
    		// 以\r\n来作为打印间隔
    		// log.LstdFlags 前面这串: 2022/08/13 15:22:34
    		log.New(os.Stdout, "\r\n", log.LstdFlags),
    		logger.Config{
    			SlowThreshold:             time.Second, // 慢 SQL 阈值
    			LogLevel:                  logger.Info, // 日志级别
    			IgnoreRecordNotFoundError: true,        // 是否忽略ErrRecordNotFound(记录未找到)错误
    			Colorful:                  true,        //是否开启彩色打印
    		},
    	)
    
    	// 全局模式
    	open, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    		//禁止创建外键
    		DisableForeignKeyConstraintWhenMigrating: true,
    		Logger:                                   newLogger,
    	})
    	if err != nil {
    		panic(err)
    	}
    	return open
    }
    
    func main() {
    	connect := getConnect()
    	var wg sync.WaitGroup
    	var l sync.Mutex
    	wg.Add(23)
    	for i := 0; i < 23; i++ {
    		go func() {
    			id := 3
    			inv := Inventory{}
    			l.Lock()
    			defer l.Unlock()
    			defer wg.Done()
    			connect.Where(Inventory{ID: uint(id)}).Find(&inv)
    			//判断是否查询,异常,自己去完善,略......
    			if *inv.Num < 1 {
    				fmt.Println("库存不足无法扣除")
    				return
    			}
    
    			result := connect.Model(&Inventory{}).Where("id = ?", id).Update("num", *inv.Num-1)
    			if result.Error != nil || result.RowsAffected == 0 {
    				fmt.Println("库存扣减失败")
    			} else {
    				fmt.Println("库存扣减成功")
    			}
    		}()
    	}
    	wg.Wait()
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    常用的基于乐观锁的实现

    依靠mysql set + where 语句进行库存扣减,保证库存不会扣除超过某个值(这里以a作为目标,扣减数量为b,扣除商品id为c),那么sql语句为: UPDATE inventory SET num= (num - b) WHERE (id = c and num >= a + b)。下面例子总共23个库存,23个协程去扣除,会有3个扣除失败。库存最后为0

    package main
    
    import (
    	"fmt"
    	"gorm.io/driver/mysql"
    	"gorm.io/gorm"
    	"gorm.io/gorm/logger"
    	"log"
    	"os"
    	"sync"
    	"time"
    )
    
    type Inventory struct {
    	gorm.Model
    	ID  uint `gorm:"primarykey"`
    	Num *int `gorm:"type:bigint not null;default:20"`
    }
    
    func (i Inventory) TableName() string {
    	return "inventory"
    }
    
    func getConnect() *gorm.DB {
    	//换成自己的数据库连接
    	dsn := "root:Xrx@1994113@tcp(127.0.0.1:3306)/proxy?charset=utf8mb4&parseTime=True&loc=Local"
    	newLogger := logger.New(
    		//os.Stdout 标准输出,控制台打印
    		// 以\r\n来作为打印间隔
    		// log.LstdFlags 前面这串: 2022/08/13 15:22:34
    		log.New(os.Stdout, "\r\n", log.LstdFlags),
    		logger.Config{
    			SlowThreshold:             time.Second, // 慢 SQL 阈值
    			LogLevel:                  logger.Info, // 日志级别
    			IgnoreRecordNotFoundError: true,        // 是否忽略ErrRecordNotFound(记录未找到)错误
    			Colorful:                  true,        //是否开启彩色打印
    		},
    	)
    
    	// 全局模式
    	open, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
    		//禁止创建外键
    		DisableForeignKeyConstraintWhenMigrating: true,
    		Logger:                                   newLogger,
    	})
    	if err != nil {
    		panic(err)
    	}
    	return open
    }
    
    func main() {
    	connect := getConnect()
    	var wg sync.WaitGroup
    	wg.Add(23)
    	for i := 0; i < 23; i++ {
    		go func() {
    			result := connect.Model(&Inventory{}).Where("id = ? and num >= 1", 1).Update("num", connect.Raw("num - 1"))
    			if result.Error != nil || result.RowsAffected == 0 {
    				fmt.Println("库存扣减失败")
    			} else {
    				fmt.Println("库存扣减成功")
    			}
    			defer wg.Done()
    		}()
    	}
    	wg.Wait()
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    使用redis分布式锁解决超卖问题

    查看我的文章《go 使用reids分布式锁》

  • 相关阅读:
    超实用!产品经理如何提高自己的工作效率?
    eclipse svn插件安装
    智云通CRM:如何进行大客户复杂关系的识别?
    系统移植部署开发阶段
    【学懂数据结构】顺序表?链表?我全都要(入门学习)
    Live800:服务不是销售的终点,而是营销的起点
    BS系统的登录鉴权流程演变
    FastReport在线设计器新版2024.1,新增多元素填充,条码元素拖放~
    Arcpy入门教程01:从零开始制作一个arcpy脚本
    PicoRV32-on-PYNQ-Z2: An FPGA-based SoC System——RISC-V On PYNQ项目复现
  • 原文地址:https://blog.csdn.net/qq_29744347/article/details/126456078