redis 8 做个秒杀系统

###秒杀的要点

  1. 对流量进行控制,逐步减少流量,使得最终到接口的流量是较小的。(流量控制不是说不要用户访问,而是对流量进行引导,保证有效请求的最大化)
  2. 尽量不要用锁,锁就意味着资源的内耗
  3. 整个过程可以分秒杀前,秒杀时,秒杀后三个步骤来思考,每一步都解耦出来。秒杀前对流量进行控制,秒杀时快速结束战斗,并且不超卖,订单处理,库存扣减可以放到秒杀后处理。

###流量控制
从用户点击秒杀到最终请求下单接口,整个过程可以参考下图:

0. 可以将秒杀接口单独抽离出来,放一个集群。

  1. 在前端界面做好请求控制,秒杀开始后,可以控制下请求接口的频率。比如用户3秒内点了10多次,但实际的请求可以只发送1次。
  2. 商品的秒杀全部走redis
  3. 预先将库存放到redis中,将秒杀成功的用户放到一个redis队列中
  4. 另外起一个进程来消费该队列,将结果写入数据库
  5. redis用主从,写在主,读在从
  6. 数据库不用锁

###实现
redis的key设计:

goodsId_start #标记是否可以秒杀,为0的时候,不能秒杀
goodsId_count #库存
goodsId_access #秒杀进来的用户
order #用于存储秒杀成功的用户

过程
goodsId_start 不为0时开始秒杀,每进来一个用户,goodsId_access + 1,同时将该用户的uid放入redis队列,当goodsId_access == goodsId_count 时,秒杀结束,后面的流程不用再走了。
同时起一个进程,不断地轮询消费order,取出uid,并写入数据库中。

要点
在计算goodsId_access时,必须保证goodsId_access+1,只插入一条记录到order中。也就是这两步必须是原子性的,这个可以用lua来完成,代码如下:

local v = tonumber(redis.call('get','goodsId_access')) --获取当前的goodsId_access
if (v >= tonumber(KEYS[1])) then --将goodsId_count传进来,超过它时,秒杀结束,后面的流程不需要了
    return 0
end
redis.call('set','goodsId_access',v+1) --秒杀成功,+1
redis.call('lpush','order',KEYS[2]) --秒杀成功,将uid放入order队列中

完整代码参考
index.php

<?php

require_once "RedisTool.php";

class index
{
    public function ms()
    {
        session_start();
        $uid = session_id();
        $redisR = new RedisTool(true);

        $goodsStart = $redisR->get('goodsId_start'); //用于标记秒杀是否开始
        if(!$goodsStart){
            return '还没开始';
        }
        $goodsCount = $redisR->get('goodsId_count'); //总库存
        $goodsAccess = $redisR->get('goodsId_access'); //已使用的库存
        if(($goodsCount - $goodsAccess) == 0){
            return '抢完了';
        }

        //把成功秒杀的用户放到redis队列中,同时将goodsId_access + 1,不超卖,整个过程用lua,保证原子性
        $redisW = new RedisTool();
        $script = "local v = tonumber(redis.call('get','goodsId_access'))
        if (v >= tonumber(KEYS[1])) then 
            return 0
        end
        redis.call('set','goodsId_access',v+1)
        redis.call('lpush','order',KEYS[2])";
        $redisW->lua($script,2,$goodsCount,$uid.' '.$_SERVER['REMOTE_ADDR']);
        return '恭喜抢到';
    }
}

$t = new index();
print($t->ms());

RedisTool.php

<?php

$dir = dirname(__FILE__);
require($dir . '/vendor/autoload.php');


class RedisTool
{
    public function __construct($slave=null)
    {
        $config = [
            'host' => '127.0.0.1',
            'port' => 6379, //主库,用于写
        ];
        if($slave){
            $config['port'] = 6389; //从库,用于读
        }
        $redis = new \Predis\Client($config);
        $this->ser = $redis;
    }

    public function set($key,$val)
    {
        return $this->ser->set($key,$val);
    }

    public function get($key)
    {
        return $this->ser->get($key);
    }

    public function lpush($key,$values)
    {
        return $this->ser->lpush($key,$values);
    }

    public function lpop($key)
    {
        return $this->ser->lpop($key);
    }

    public function lua($script, $numkeys,$arg1,$arg2)
    {
        return $this->ser->eval($script,$numkeys,$arg1,$arg2);
    }
}

测试
条件有限,服务和ab都在本机,ab测试结果符合预期:

127.0.0.1:6379> set goodsId_count 10
OK
127.0.0.1:6379> set goodsId_access 0
OK
127.0.0.1:6379> set goodsId_start 1
OK
[root@localhost tmp]# ab -n 5000 -c 120 http://172.20.10.10:88/index.php
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/

Benchmarking 172.20.10.10 (be patient)
Completed 500 requests
Completed 1000 requests
Completed 1500 requests
Completed 2000 requests
Completed 2500 requests
Completed 3000 requests
Completed 3500 requests
Completed 4000 requests
Completed 4500 requests
Completed 5000 requests
Finished 5000 requests


Server Software:        nginx/1.13.8
Server Hostname:        172.20.10.10
Server Port:            88

Document Path:          /index.php
Document Length:        12 bytes

Concurrency Level:      120
Time taken for tests:   14.322 seconds
Complete requests:      5000
Failed requests:        4990
   (Connect: 0, Receive: 0, Length: 4990, Exceptions: 0)
Write errors:           0
Total transferred:      1700030 bytes
HTML transferred:       45030 bytes
Requests per second:    349.11 [#/sec] (mean)
Time per request:       343.729 [ms] (mean)
Time per request:       2.864 [ms] (mean, across all concurrent requests)
Transfer rate:          115.92 [Kbytes/sec] received

Connection Times (ms)
              min  mean[+/-sd] median   max
Connect:        1  143 949.8     38    9169
Processing:     2  197 973.5     70    9230
Waiting:        2  194 973.7     68    9226
Total:          3  339 1349.1    112    9271

Percentage of the requests served within a certain time (ms)
  50%    112
  66%    122
  75%    133
  80%    139
  90%    174
  95%    422
  98%   8777
  99%   8882
 100%   9271 (longest request)
127.0.0.1:6389> lrange order 0 -1
 1) "o9ulu8llhakot9i4a14297a68g 172.20.10.10"
 2) "qq9k1u8ngqoop48glmkfe7d3pd 172.20.10.10"
 3) "hucjmfm34ql1cdmcc29ovv6fql 172.20.10.10"
 4) "1m4fmg6kq3t3jr5g38or3mp4a7 172.20.10.10"
 5) "j1qbqak9g8gvitd3kitkj9ckd8 172.20.10.10"
 6) "gct81lunmooebs8tu6rif3nunu 172.20.10.10"
 7) "a1cpdjbrremnkc6k1k02umdbbb 172.20.10.10"
 8) "f15jotvsqm8mlvv9nndp9dhsl3 172.20.10.10"
 9) "sq8emhtja81b94dlkij1m06f2q 172.20.10.10"
10) "8k5nukkdt50jjr8i6jshdrif48 172.20.10.10"

###参考
使用 Redis 搭建电商秒杀系统
秒杀系统架构解决之道

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值