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事务逻辑)
        if ($redis->decr('stock') <= 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

centos8 yum安装php7.3

执行命令 $ yum install epel-release $ rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm $ dnf install -y https://rpms...

阅读全文

php面试题

php魔术方法 __get( $property ) 当调用一个未定义的属性时访问此方法 __set( $property, $value ) 给一个未定义的属性赋值时调用 __isset( $proper...

阅读全文

php-rdkafka测试笔记

参考:https://learnku.com/articles/15176/laravel-implementation-of-kafka-message-push-and-receive-processing 安装 安装librdkafka 库 git clone https...

阅读全文

欢迎留言