6-21 2,574 views
1.创建商品和订单表模拟商品交易操作
fa_goods商品表,stock商品库存,test_order=订单表
2.添加一个测试商品数据(商品数量为150个)
测试并发购买的方法(golang)
package main
import (
"fmt"
"net/http"
"sync"
)
func main() {
wait := sync.WaitGroup{}
//模拟1000个人买150个商品
for i := 0; i <= 1000; i++ {
wait.Add(1)
go func(w *sync.WaitGroup) {
defer w.Done()
resp, err := http.Get("http://test.myapp.com/index/index/buy?id=1")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(resp)
// defer resp.Body.Close()
// body, _ := ioutil.ReadAll(resp.Body)
// fmt.Println(string(body))
}(&wait)
}
wait.Wait()
}
测试减库存方法
1.不使用事务和锁(错误操作)
我是在thinkphp5.1中模拟的
//buy
public function buy()
{
$id = $this->request->get('id');
$stock = Db::name('goods')->where('id', $id)->value('stock');
if ($stock <= 0) {
return '卖光了';
}
$re = Db::name('goods')
->where('id', $id)
->setDec('stock');
Db::name('test_order')
->insert([
'oid' => uniqid(),
'create_time' => date('Y-m-d H:i:s'),
'goods_id' => $id
]);
return 'success';
}
测试结果
$ go run test_buy.go
可以看到超卖了14个商品
2.使用innodb行锁
public function buy2()
{
$id = $this->request->get('id');
// 启动事务
Db::startTrans();
try {
$stock = Db::name('goods')->where('id', $id)->lock(true)->value('stock');
if ($stock <= 0) {
throw new \Exception('卖光了');
}
$re = Db::name('goods')
->where('id', $id)
->setDec('stock');
Db::name('test_order')
->insert([
'oid' => uniqid(),
'create_time' => date('Y-m-d H:i:s'),
'goods_id' => $id
]);
// 提交事务
Db::commit();
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
}
return 'success';
}
测试结果
//将go脚本中的buy方法改成buy2
$ go run test_buy.go
测试结果正常,没有出现超卖现象,但是这种行锁方式的效率不是很高。下面测试用redis锁的方式测试
3.使用redis测试
首先安装扩展
$ composer require predis/predis
public function buy3()
{
$id = $this->request->get('id');
$lock_key = 'lock:' . $id;
$random = uniqid();
while (true)
{
if($this->redisLock($lock_key,$random))
{
//业务内容
$stock = Db::name('goods')->where('id', $id)->value('stock');
if ($stock <= 0) {
$this->unlock($lock_key,$random);
break;
}
$re = Db::name('goods')
->where('id', $id)
->setDec('stock');
Db::name('test_order')
->insert([
'oid' => uniqid(),
'create_time' => date('Y-m-d H:i:s'),
'goods_id' => $id
]);
$this->unlock($lock_key,$random);
break;
}
}
return 'success';
}
function redisLock($key, $random, $ex = 10)
{
//从 Redis 2.6.12 版本开始, SET 命令的行为可以通过一系列参数来修改:
//EX second :设置键的过期时间为 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。
//PX millisecond :设置键的过期时间为 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecond value 。
//NX :只在键不存在时,才对键进行设置操作。 SET key value NX 效果等同于 SETNX key value 。
//XX :只在键已经存在时,才对键进行设置操作。
return $this->getRedis()->set($key, $random, "ex", $ex, "nx");
}
function unlock($key, $random)
{
if ($this->getRedis()->get($key) == $random) {
return $this->getRedis()->del($key);
}
}
function getRedis()
{
if (empty(self::$redis) || is_null(self::$redis)){
self::$redis = new \Predis\Client();;
}
return self::$redis;
}
经测试不会出现超卖,效率还可以
4.使用加锁类 malkusch/lock(predis方式)
安装使用参考:http://www.koukousky.com/back/2778.html
首先安装扩展
$ composer require malkusch/lock
$ composer require predis/predis
public function buy4()
{
$id = $this->request->get('id');
$redis = new \Predis\Client(array(
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
));
#传入一个predis实例,设置redis key名为lock+id的锁,设置锁释放时间为10秒(10秒之后当前程序释放,新的实例获取此锁)
$mutex = new \malkusch\lock\mutex\PredisMutex([$redis], 'lock:'.$id,10);
$mutex->synchronized(function () use ($id) {
//代码逻辑
$stock = Db::name('goods')->where('id', $id)->value('stock');
if ($stock <= 0) {
throw new \Exception('卖完了');
}
$re = Db::name('goods')
->where('id', $id)
->setDec('stock');
Db::name('test_order')
->insert([
'oid' => uniqid(),
'create_time' => date('Y-m-d H:i:s'),
'goods_id' => $id
]);
echo 'success';
});
}
经测试,不会出现超卖情况
5.使用redis预存商品库存,使用(redis的原子性的递增递减)
#商品在redis中的库存应该在商品的增删改查阶段预存在字段里面,这里预设的是1000个库存,但实际是只有150个库存。
public function buy5()
{
$id = $this->request->get('id');
$redis = new \Predis\Client(array(
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
));
//从redis中取库存(取到了就进行下面的mysql事务逻辑)
$script = <<<script
local buyNum = ARGV[1]
local goodsKey = KEYS[1]
local goodsNum = redis.call('get',goodsKey)
if tonumber(goodsNum) >= tonumber(buyNum)
then redis.call('decrby',goodsKey,tonumber(buyNum))
return buyNum
else
return '0'
end
script;
//使用lua脚本保证原子一致性
if ($redis->eval($script,1,'stock','1') <= 0) {
throw new \Exception('卖光了');
}
// 启动事务
Db::startTrans();
try {
$stock = Db::name('goods')->where('id', $id)->lock(true)->value('stock');
if ($stock <= 0) {
throw new \Exception('卖光了');
}
$re = Db::name('goods')
->where('id', $id)
->update([
'stock' => $stock - 1
]);
Db::name('test_order')
->insert([
'oid' => uniqid(),
'create_time' => date('Y-m-d H:i:s'),
'goods_id' => $id
]);
// 提交事务
Db::commit();
} catch (\Exception $e) {
//如果失败
$stock = Db::name('goods')->where('id', $id)->lock(true)->value('stock');
#同步redis库存
$redis->set('stock', $stock);
// 回滚事务
Db::rollback();
}
}
经测试不会出现超卖情况
参考:https://learnku.com/articles/57959
秒杀系统参考:https://developer.51cto.com/art/201909/602864.htm