PHP + Redis 实现商品秒杀实例

前言

用原生PHP来实现商品的秒杀,实现的原理是,首先把所有的商品ID存进redis列表里,然后是一个把reids列表中商品ID取出来和用户ID绑定的过程。绑定成功也就是秒杀成功,秒杀成功的同时把用户ID和商品ID存进reids哈希表里。并记录秒杀失败的人数。

1.环境与技术

1.Nginx(Apache也行,目前我的服务器是Nginx)

2.PHP 5.6以上

3.Redis

4.压力测试工具ab

2.代码

2.1连接redis(redis.php)

为了省事,把连接redis的步骤写入一个文件里,没有密码的可以不写auth,这里选了redis索引为1的库,默认是redis索引为0的库。

$redis = new Redis();
$redis->connect('127.0.0.1',6379);
$redis->auth(password);
$redis->select(1);

2.2商品ID存入列表(crontab.php)

假设秒杀的商品有30件,把30件的商品ID都存进redis 列表里。

include_once "redis.php";
//设定商品数量
$count = 30;
$listKey = "goods_list";
for($i=1;$i<=$count;$i++){
//将商品id ,push到列表中
$redis->rpush($listKey,$i);
}

2.3 商品秒杀(kill.php)

这里的用户ID都是随机生成的,每请求一次都会生成一个唯一的用户ID和列表中的一个商品ID绑定,存进哈希表里。之后需要用压力测试工具ab模拟用户进场秒杀。

include_once "redis.php";
$uuid = md5(uniqid('user').time());
$listKey = "goods_list";
$orderKey = "buy_order";
$dailUserNum = "fail_num";
if($goodsId=$redis->lpop($listKey)){
//秒杀成功即存在redis的哈希列表中
$redis->hset($orderKey,$goodsId,$uuid);
}else{
//秒杀失败,记录失败用户个数,方便得知有效
$redis->incr($dailUserNum);
}
echo "SUCCESS";

3.操作

3.1 执行crontab.php文件

执行crontab.php文件,让商品ID存进redis列表。这个文件应该是一个定时任务定时执行的,现在我们手动执行一下。

% php /usr/local/var/www/redis/crontab.php

检测商品ID是否放入成功,连接redis客户端查看。

% redis-cli
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> lrange goods_list 0 -1
1) "1"
2) "2"
3) "3"
4) "4"
5) "5"
6) "6"
7) "7"
8) "8"
9) "9"
10) "10"
11) "11"
12) "12"
13) "13"
14) "14"
15) "15"
16) "16"
17) "17"
18) "18"
19) "19"
20) "20"
21) "21"
22) "22"
23) "23"
24) "24"
25) "25"
26) "26"
27) "27"
28) "28"
29) "29"
30) "30"

3.2.压力测试工具ab模拟秒杀

apache自带ab工具,我的是mac,mac自带apache,可以先检测一下

% ab --v
ab: wrong number of arguments
Usage: ab [options] [http[s]://]hostname[:port]/path
Options are:
-n requests Number of requests to perform
-c concurrency Number of multiple requests to make at a time
-t timelimit Seconds to max. to spend on benchmarking
This implies -n 50000
-s timeout Seconds to max. wait for each response
Default is 30 seconds
-b windowsize Size of TCP send/receive buffer, in bytes
-B address Address to bind to when making outgoing connections
-p postfile File containing data to POST. Remember also to set -T
-u putfile File containing data to PUT. Remember also to set -T
-T content-type Content-type header to use for POST/PUT data, eg.
'application/x-www-form-urlencoded'
Default is 'text/plain'
-v verbosity How much troubleshooting info to print
-w Print out results in HTML tables
-i Use HEAD instead of GET
-x attributes String to insert as table attributes
-y attributes String to insert as tr attributes
-z attributes String to insert as td or th attributes
-C attribute Add cookie, eg. 'Apache=1234'. (repeatable)
-H attribute Add Arbitrary header line, eg. 'Accept-Encoding: gzip'
Inserted after all normal header lines. (repeatable)
-A attribute Add Basic WWW Authentication, the attributes
are a colon separated username and password.
-P attribute Add Basic Proxy Authentication, the attributes
are a colon separated username and password.
-X proxy:port Proxyserver and port number to use
-V Print version number and exit
-k Use HTTP KeepAlive feature
-d Do not show percentiles served table.
-S Do not show confidence estimators and warnings.
-q Do not show progress when doing more than 150 requests
-l Accept variable document length (use this for dynamic pages)
-g filename Output collected data to gnuplot format file.
-e filename Output CSV file with percentages served
-r Don't exit on socket receive errors.
-m method Method name
-h Display usage information (this message)
-I Disable TLS Server Name Indication (SNI) extension
-Z ciphersuite Specify SSL/TLS cipher suite (See openssl ciphers)
-f protocol Specify SSL/TLS protocol
(TLS1, TLS1.1, TLS1.2 or ALL)
-E certfile Specify optional client certificate chain and private key
开始测试,100并发 1000访问量
% ab -c 100 -n 1000 http://localhost:8080/redis/kill.php
This is ApacheBench, Version 2.3 
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Completed 1000 requests
Finished 1000 requests
Server Software: nginx/1.19.0
Server Hostname: localhost
Server Port: 8080
Document Path: /redis/kill.php
Document Length: 7 bytes
Concurrency Level: 100
Time taken for tests: 0.792 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 169000 bytes
HTML transferred: 7000 bytes
Requests per second: 1263.25 [#/sec] (mean)
Time per request: 79.161 [ms] (mean)
Time per request: 0.792 [ms] (mean, across all concurrent requests)
Transfer rate: 208.49 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 1.7 1 9
Processing: 16 73 10.8 76 84
Waiting: 16 73 10.8 76 84
Total: 25 74 9.3 77 86
Percentage of the requests served within a certain time (ms)
50% 77
66% 78
75% 78
80% 79
90% 80
95% 80
98% 81
99% 82
100% 86 (longest request)
4.效果
查看抢到商品的用户
127.0.0.1:6379[1]> hgetall buy_order
1) "1"
2) "722c8badaa1e2b53405672c9684d1822"
3) "2"
4) "e6820dd50508cd09cf65b03e928410d1"
5) "3"
6) "066097efb74efc587df98729bcff6418"
7) "5"
8) "1d077d9850abe2f99d0fd4b306de9149"
9) "4"
10) "bc2f8e8f9a2e77c315ace1bac5d48764"
11) "6"
12) "3f1647a2cc2b9ebe4d4af4d43d95bbc0"
13) "7"
14) "b71d278030b62a2972a7236cb4f51f9c"
15) "8"
16) "fec4dde48c48d5dbb617181e26c31cde"
17) "9"
18) "77760c6251e0c4c572789741899c12e0"
19) "11"
20) "e548411b6bc294dd1d10f96ca4d552c6"
21) "10"
22) "6d63a2ca724b3fdcc2f49f71ebf37c70"
23) "12"
24) "873521e8fdf465949dc8a48ded10ce5e"
25) "13"
26) "33e6cdb1df95219fd3e46d779591941b"
27) "14"
28) "033c644345628a13c45bab586af6d0e4"
29) "15"
30) "480afe21c920b0a02ebbe4c75f96b80b"
31) "16"
32) "87c79d0d1b448a4003785f6dde05ee3a"
33) "17"
34) "e6102eb6c4389074f0e7ebc0d574f0c9"
35) "18"
36) "719ee437a332d5a8f668474a6e204136"
37) "19"
38) "71d43aadafb97972bb2023119d80727f"
39) "20"
40) "875612b1b1a46088af7a79169b8a744f"
41) "22"
42) "c149527bfe7573f70c57e6eeef70483d"
43) "23"
44) "1bb2c03355b3f83b0106207a0ade98be"
45) "21"
46) "0cc81a86769d96f09ddf2388015cd8d8"
47) "24"
48) "a5ce3e992c6e36a0c692e317a356dfd9"
49) "25"
50) "9d92dad9a4c0212dd9cab0a9db169928"
51) "26"
52) "d37ee45647706d3e40a8cb8e78703782"
53) "27"
54) "399ac4d653541c74ef68620da86473a1"
55) "28"
56) "d00da588ff71dc46bab8a2efeacb4a0b"
57) "29"
58) "46550a2617e38508a0350bc6dfc95069"
59) "30"
60) "c9e97b821f8374c7ef37d38735f1ca74"
查看秒杀失败的人数
127.0.0.1:6379[1]> get fail_num
"970"
这时列表里的商品就空了
127.0.0.1:6379[1]> lrange goods_list 0 -1
(empty list or set)

5.注意

这过程可能会出现一些问题,

socket: Too many open files (24
lijiwei@lijiweideAir ~ % ab -c 100 -n 1000 http://localhost:8080/redis/kill.php
This is ApacheBench, Version 2.3 
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
socket: Too many open files (24)

解决方法:

看网上说是调整可以打开的文件数,操作一波

% ulimit -n 65535

ulimit: setrlimit failed: invalid argument

失败了,继续网上查阅,说是设置的数字特别大,可以用下面两个命令看下系统最大数

% sysctl kern.maxfiles
kern.maxfiles: 12288
% sysctl kern.maxfilesperproc
kern.maxfilesperproc: 10240

假如系统最大数字不满足你的要求,就修改下系统最大数,命令如下(数字自己设定,但是不要太大,有的电脑系统貌似有限定):

% sudo sysctl -w kern.maxfiles=65536
Password:
kern.maxfiles: 12288 -> 65536
% sudo sysctl -w kern.maxfilesperproc=65536
kern.maxfilesperproc: 10240 -> 65536

然后继续执行,就成功了

% ulimit -n 65535

2.apr_socket_recv: Connection reset by peer (54)

当我的并发量和访问人数比较大时,出现过这个问题,网上查了一些,但是没有解决,哪为大佬看到了请指教一下。

% ab -c 300 -n 3000 http://localhost:8080/redis/kill.php
This is ApacheBench, Version 2.3 
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking localhost (be patient)
apr_socket_recv: Connection reset by peer (54)
Total of 1 requests completed