1. 程式人生 > >redis 8 做個秒殺系統

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
[[email protected]t 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"