Redis队列实现商城秒杀 - 简书

秒杀的核心问题是在大并发的情况下不会超出库存的购买,这里主要使用Redis的List类型实现秒杀

实现思路如下:

  • 秒杀程序把请求写入Redis (userId、毫秒级时间戳)
  • 检查Redis已经存放的数据长度,超出上限的直接丢弃,直接返回秒杀结束
  • 死循环处理存入Redis的数据并写入数据库

redis类

<?php

/**
 * Class PRedis
 */
class PRedis
{
    public $redis;
    const HOST = '127.0.0.1';
    const PORT = 6379;
    const TIMEOUT = 5;
    private static $_instance = null;

    /**
     * 防止被克隆
     */
    private function __clone()
    {

    }

    /**
     * 定义公用的静态方法
     * @return PRedis|null
     * @throws Exception
     */
    public static function getInstance()
    {
        if (self::$_instance == null) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    /**
     * PRedis constructor.
     * @throws Exception
     */
    private function __construct()
    {
        // 实例化redis
        $this->redis = new Redis();
        $result = $this->redis->connect(self::HOST, self::PORT, self::TIMEOUT);
        if ($result === false) {
            throw new Exception('redis connect failed');
        }
    }

    /**
     * 获取指定键的列表长度
     * @param $key
     * @return int
     */
    public function lLen($key)
    {
        return $this->redis->lLen($key);
    }

    /**
     * redis结尾处插入数据
     * @param $key
     * @param $value
     * @return bool|int
     */
    public function rPush($key, $value)
    {
        return $this->redis->rPush($key, $value);
    }

    /**
     * 从左侧取出一个值
     * @param $key
     * @return string
     */
    public function lPop($key)
    {
        return $this->redis->lPop($key);
    }
}

秒杀程序

<?php

include 'PRedis.php';

// 秒杀记录的redis键
$secKillKey = 'secKillKey';
// 参与秒杀的总数量
$totalNum = 5;
// 实例化redis
$redis = PRedis::getInstance();
// 模拟多个用户,因为for按顺序执行,所以只能模拟高压力,不能模拟高并发
for ($i = 0; $i < 20; $i++) {
    // 模拟的随机用户id
    $userId = rand(10000, 99999);
    // 获取redis保存的长度
    $redisLen = $redis->lLen($secKillKey);
    if ($redisLen < $totalNum) {
        // 末尾插入数据
        $redis->rPush($secKillKey, $userId . '&' . microtime());
        echo '秒杀成功' . PHP_EOL;
    } else {
        echo '秒杀已结束' . PHP_EOL;
    }
}

入库程序

<?php

include 'PRedis.php';

// 秒杀记录的redis键
$secKillKey = 'secKillKey';
$redis = PRedis::getInstance();
// 死循环处理
while (1) {
    // 从队列左侧取出一个值
    $userInfo = $redis->lPop($secKillKey);
    if (!$userInfo || $userInfo == 'nil') {
        continue;
    }
    // 切割
    $userArr = explode('&', $userInfo);
    if (count($userArr) != 2) {
        continue;
    }
    $insert = [
        'user_id' => $userArr[0],
        'microtime' => $userArr[1],
    ];
    // 插入数据库 todo

    // 如果插入失败,需要回滚机制
    $flag = true; // 插入失败为false 成功为TRUE
    if ($flag === false) {
        $redis->rPush($secKillKey, $userInfo);
    }

    echo $userArr[0] . '插入成功' . PHP_EOL;
}