在并发的情况下扣减库存会出现库存超卖的现象。
这里使用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},
})
}
由于下面例子执行速度过快,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()
}
自带的锁可以解决单体应用下的超卖问题,如果是分布式的就无能为力了。
下面例子会有三个扣除失败
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()
}
依靠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()
}
查看我的文章《go 使用reids分布式锁》