超卖问题 学习记录 go实现

对于高并发环境下,超卖问题一直是非常常见的问题,对于有限的库存和突发的高请求下,避免产生超卖是重中之重。 主要是一直看面试题,结果面试当场,脑袋忘记了,于是亲自动手简单做一遍,避免下次发生。以下解决方案暂时没有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,成功防止了货存降为负数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值