秒杀场景的防护措施

124 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第5天,点击查看活动详情

秒杀场景

  1. 拼夕夕的苹果手机618有秒杀活动,可能手机只有1000台,但同时进入的流量可能是成百上千万。如果没有防护措施,就会出项库存1000台,买到的人远远超出1000人,那这样就造成很严重的问题了。
  2. 12306抢票,一列火车的票是有限的,库存一份,并发流量非常多,大家都读相同的库存。但是火车的座位就那个些,如果发生了超卖,那到时候火车都装不下那么多人。
  3. 稀土掘金的福利兑现功能,每周都会开放特定惊喜好物打折售卖,但是库存也是有限的,如果没有采取防范措施让掘友们一哄而上抢过了,那没货发怎么办呢?

防护措施

  1. 使用redis实现分布式锁。
  2. 使用redis实现队列。

redis实现分布式锁

直接上代码

<?php

namespace app\lib\redis;

class RedisLock
{
    private $acquireTimeout = 2;
    protected $connection;

    public function __construct()
    {
        $config = [
            'host'     => VITEC_CONFIG['aliyunRedisHost'],
            'port'     => VITEC_CONFIG['aliyunRedisPort'],
            'password' => VITEC_CONFIG['aliyunRedisAuth'],
            'dbindex'  => VITEC_CONFIG['redisSelect'],
        ];
        $redis = new \Redis();
        if ($redis->connect($config['host'], $config['port']) === false) {
            throw new RedisException($redis->getLastError(), 500);
        }
        if (!empty($config['password'])) {
            if ($redis->auth($config['password']) === false) {
                throw new RedisException($redis->getLastError(), 500);
            }
        }
        if (!empty($config['dbindex'])) {
            if ($redis->select($config['dbindex']) === false) {
                throw new RedisException($redis->getLastError(), 500);
            }
        }
        $this->connection = $redis;
    }

    public function redisLock($key, $expireTime = 2, $isNegtive = false)
    {
        if ($isNegtive) {
            while ($this->connection->setnx($key, time() + $expireTime) == 0) {
                if (time() > $this->connection->get($key) && time() > $this->connection->getSet(
                        $key,
                        time() + $expireTime
                    )) {
                    break;
                } else {
                    usleep(100);
                }
            }
            return true;
        } else {
            $res = $this->connection->setnx($key, time() + $expireTime);
            if ($res) {
                return true;
            }
            return false;
        }
    }

    public function delRedisLock($key)
    {
        $this->connection->del($key);
    }
  1. 以上代码分别实现了乐观锁和悲观锁。
  2. setnx的作用为如果不存在则设置,如果任务标识key的值不存在且设置成功则函数返回1,失败则返回0.
  3. 乐观锁:我只尝试取一次锁,如果使用setnx设置锁,成功返回true,失败返回false。
  4. 悲观锁:循环阻塞式地使用setnx去取锁,如果取不到锁会判断锁是否超时,key对应的值就是超时时间,可以与当前时间进行对比,如果小于当前时间则表示锁超时,同时执行getset方法再去获取旧值设置新的超时时间并获取锁,执行成功返回true,执行失败则睡个一段时间继续循环去取锁。

redis队列

这种方式简单很多了,我们先把库存从数据库中读出来写到redis的列表里面。然后开始秒杀的时候让用户并发请求,我们就直接从redis的队列中把商品拿出来就可以了,因为redis的所有操作都是原子性的,即使有多个操作同时到达也是按照顺序来执行的,那这样就有效的避免了超卖的问题。

从数据库获取库存到redis

<?php
$store=5000;
$redis=new Redis();
$result=$redis->connect('127.0.0.1',6379);
for($i=1;$i<=$store;$i++){
    $redis->lpush('good_ids',$i);
}
return true;

秒杀时从redis队列获取库存

<?php
$redis=new Redis();
$result=$redis->connect('127.0.0.1',6379);
$count=$redis->lpop('good_ids');
if(!$count){
    return false;
}
//获取到了商品的id继续执行后面的售卖逻辑
.....
return true;

谢谢观看!streetlamp敬上!