1.创建商品和订单表模拟商品交易操作

file
file

fa_goods商品表,stock商品库存,test_order=订单表

2.添加一个测试商品数据(商品数量为150个)

file

测试并发购买的方法(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

file
file

可以看到超卖了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

file
file

测试结果正常,没有出现超卖现象,但是这种行锁方式的效率不是很高。下面测试用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

解决 laravel-admin between datetime 假如数据库是时间戳int类型无法筛选。

laravel-admin默认的between->datetime(),查询默认是datetime类型,但是假如数据库是时间戳类型就会报错,又不想改底层文件的话可以试试加自定义筛选功能...

阅读全文

php解析英文语句,自动分解。

参考:https://www.php.net/manual/en/function.str-split.php 最近碰到一个问题,客户的英文地址太长,超出接口api字段长度,所以需要解析下语句分解发送。 ...

阅读全文

记录一个laravel-excel导出表格值为0导出excel显示空的解决方法。

最近在使用laravel-excel导出表格的时候,发现假如字段值为0的情况下,导出的excel中直接显示为空,找到一个方法解决,如下. 在laravel-excel的config配置中...

阅读全文

欢迎留言