对于高并发环境下,超卖问题一直是非常常见的问题,对于有限的库存和突发的高请求下,避免产生超卖是重中之重。 主要是一直看面试题,结果面试当场,脑袋忘记了,于是亲自动手简单做一遍,避免下次发生。以下解决方案暂时没有MQ参与的解决
方法1:使用MySQL悲观锁,实现约束,update操作,在MySQL默认事务隔离下,是会加上X锁进行写限制的。
func UpdateGoodsWithTrans(goodsId string, numToPurchase int64) (int64, error) {
tx, err := globalDb.db.Begin()
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %v", err)
}
defer tx.Rollback()
result, err := tx.Exec("UPDATE goods SET goods_remains = goods_remains - ? WHERE goods_id = ? and goods_remains >= ?", numToPurchase, goodsId, numToPurchase)
if err != nil {
return 0, fmt.Errorf("failed to update goods: %v", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("failed to get rows affected: %v", err)
}
if rowsAffected == 0 {
return 0, fmt.Errorf("no goods found with goods_id: %s", goodsId)
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("failed to commit transaction: %v", err)
}
return numToPurchase, nil
}
方法2:MySQL的乐观锁实现,常见的有依靠版本号或时间戳
func UpdateGoodsWithVersion(goodsId string, numToPurchase int64) (int64, error) {
tx, err := globalDb.db.Begin()
goodsRemains, version, err := GetGoods(goodsId)
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %v", err)
}
defer tx.Rollback()
if goodsRemains < numToPurchase {
return 0, fmt.Errorf("failed to update goods: %v", err)
}
result, err := tx.Exec("UPDATE goods SET goods_remains = goods_remains - ?, version = version + 1 WHERE goods_id = ? and version = ?", numToPurchase, goodsId, version)
if err != nil {
return 0, fmt.Errorf("failed to update goods: %v", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("failed to get rows affected: %v", err)
}
if rowsAffected == 0 {
return 0, fmt.Errorf("no goods found with goods_id: %s", goodsId)
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("failed to commit transaction: %v", err)
}
return numToPurchase, nil
}
方法3:使用lua脚本, 在redis中实现原子操作。redis中也支持简单事务,但是看网上说不推荐使用。
#public包下
const LuaUpdateRemains = `
local currentRemains = redis.call("GET", KEYS[1])
if currentRemains == false then
return 0
end
if tonumber(currentRemains) + tonumber(ARGV[1]) < 0 then
return -1
end
return redis.call("INCRBY", KEYS[1], ARGV[1])
`
#redis包下
func UpdateProductRemains(productId string, num int) error {
key := public.Product_key + productId
result, err := rdb.Eval(context.Background(), public.LuaUpdateRemains, []string{key}, num).Result()
if err != nil {
return fmt.Errorf("更新商品库存失败: %v", err)
}
if result == int64(-1) {
return fmt.Errorf("商品库存不足,无法减少库存")
}
return nil
}
整体代码依照包区分开来
data_base包
package data_base
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
"time"
)
type DB struct {
db *sql.DB
}
var globalDb *DB
func init() {
dsn := "root:000529@tcp(localhost:3306)/over_buy?charset=utf8mb4&parseTime=True&loc=Local"
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatal("连接数据库失败: ", err)
}
//defer db.Close()
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(30 * time.Minute)
// 测试连接是否成功
err = db.Ping()
if err != nil {
log.Fatal("数据库连接失败: ", err)
} else {
fmt.Println("数据库连接池创建成功!")
}
globalDb = &DB{
db: db,
}
}
func CloseDB() {
globalDb.db.Close()
}
func GetGoods(goodsId string) (int64, int64, error) {
goodsRemains := int64(0)
version := int64(0)
err := globalDb.db.QueryRow("SELECT goods_id, goods_remains, version FROM goods WHERE goods_id = ?",
goodsId).Scan(&goodsId, &goodsRemains, &version)
return goodsRemains, version, err
}
func UpdateGoods(goodsId string, goodsRemains int64) (int64, error) {
result, err := globalDb.db.Exec("UPDATE goods SET goods_remains = ?, version = version + 1 WHERE goods_id = ?", goodsRemains, goodsId)
if err != nil {
return 0, err
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, err
}
if rowsAffected == 0 {
return 0, fmt.Errorf("no goods found with goods_id: %s", goodsId)
}
return goodsRemains, nil
}
// 悲观锁
func UpdateGoodsWithTrans(goodsId string, numToPurchase int64) (int64, error) {
tx, err := globalDb.db.Begin()
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %v", err)
}
defer tx.Rollback()
result, err := tx.Exec("UPDATE goods SET goods_remains = goods_remains - ? WHERE goods_id = ? and goods_remains >= ?", numToPurchase, goodsId, numToPurchase)
if err != nil {
return 0, fmt.Errorf("failed to update goods: %v", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("failed to get rows affected: %v", err)
}
if rowsAffected == 0 {
return 0, fmt.Errorf("no goods found with goods_id: %s", goodsId)
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("failed to commit transaction: %v", err)
}
return numToPurchase, nil
}
// 乐观锁形式实现
func UpdateGoodsWithVersion(goodsId string, numToPurchase int64) (int64, error) {
tx, err := globalDb.db.Begin()
goodsRemains, version, err := GetGoods(goodsId)
if err != nil {
return 0, fmt.Errorf("failed to begin transaction: %v", err)
}
defer tx.Rollback()
if goodsRemains < numToPurchase {
return 0, fmt.Errorf("failed to update goods: %v", err)
}
result, err := tx.Exec("UPDATE goods SET goods_remains = goods_remains - ?, version = version + 1 WHERE goods_id = ? and version = ?", numToPurchase, goodsId, version)
if err != nil {
return 0, fmt.Errorf("failed to update goods: %v", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("failed to get rows affected: %v", err)
}
if rowsAffected == 0 {
return 0, fmt.Errorf("no goods found with goods_id: %s", goodsId)
}
if err := tx.Commit(); err != nil {
return 0, fmt.Errorf("failed to commit transaction: %v", err)
}
return numToPurchase, nil
}
package data_base
import (
"context"
"fmt"
redis "github.com/go-redis/redis/v8"
"log"
"over_buy/public"
)
var (
rdb *redis.Client // Redis 客户端实例
ctx = context.Background()
)
// 初始化 Redis 连接池
func init() {
rdb = redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Redis 服务器地址
Password: "", // 密码为空
DB: 0, // 默认数据库
})
// 测试连接是否成功
_, err := rdb.Ping(ctx).Result()
if err != nil {
log.Fatalf("无法连接到 Redis: %v", err)
}
fmt.Println("成功连接到 Redis")
}
func RestoreProductInfo(productId string, remains int64) error {
key := public.Product_key + productId
// 使用 SET 命令设置库存
err := rdb.Set(ctx, key, remains, 0).Err()
if err != nil {
return fmt.Errorf("设置商品库存失败: %v", err)
}
return nil
}
func UpdateProductRemains(productId string, num int) error {
key := public.Product_key + productId
result, err := rdb.Eval(context.Background(), public.LuaUpdateRemains, []string{key}, num).Result()
if err != nil {
return fmt.Errorf("更新商品库存失败: %v", err)
}
if result == int64(-1) {
return fmt.Errorf("商品库存不足,无法减少库存")
}
return nil
}
public包
package public
const (
Product_key = "product"
)
// Lua 脚本:获取库存,检查库存是否足够,若足够则更新库存
const LuaUpdateRemains = `
local currentRemains = redis.call("GET", KEYS[1])
if currentRemains == false then
return 0
end
if tonumber(currentRemains) + tonumber(ARGV[1]) < 0 then
return -1
end
return redis.call("INCRBY", KEYS[1], ARGV[1])
`
main
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"log"
"over_buy/data_base"
)
func buyProduct1(c *gin.Context) {
num, err := data_base.UpdateGoodsWithTrans("1", 1)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("success decrease : %d", num)
}
}
func buyProduct2(c *gin.Context) {
num, err := data_base.UpdateGoodsWithVersion("1", 1)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("success decrease : %d", num)
}
}
func buyProduct3(c *gin.Context) {
err := data_base.UpdateProductRemains("1", -1)
if err != nil {
fmt.Println(err)
} else {
fmt.Println("success decrease")
num, _, _ := data_base.GetGoods("1")
data_base.UpdateGoods("1", num-1)
}
}
func main() {
defer data_base.CloseDB()
r := gin.Default()
goodsRemains, _, err := data_base.GetGoods("1")
if err != nil {
fmt.Println("获取库存失败:%v", err)
}
data_base.RestoreProductInfo("1", goodsRemains)
// API 路由
r.POST("/buy_with_lock1", buyProduct1)
r.POST("/buy_with_lock2", buyProduct2)
r.POST("/buy_with_lock3", buyProduct3)
// 启动服务器
if err = r.Run(":8080"); err != nil {
log.Fatal("Unable to start server: ", err)
}
}
结果详情
使用JMeter进行测试,开启十个线程十次循环,同时数据库最终结果为0,成功防止了货存降为负数。


301

被折叠的 条评论
为什么被折叠?



