php 解决超卖的几种方案(redis锁、mysql悲观锁)

php 解决超卖的几种方案(redis锁、mysql悲观锁)
php 解决超卖的几种方案(redis锁、mysql悲观锁)
测试并发购买的方法(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

php 解决超卖的几种方案(redis锁、mysql悲观锁)

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

COPY
测试结果
//将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;
}

COPY
经测试不会出现超卖,效率还可以

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';
    });
}

COPY
经测试,不会出现超卖情况

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();
    }

}

COPY
经测试不会出现超卖情况

参考:https://learnku.com/articles/57959
秒杀系统参考:https://developer.51cto.com/art/201909/602864.htm

上一篇:微信小程序--摸索之旅


下一篇:dataframe实例可视化