PHP秒杀设计
# 基础工具与知识介绍
# 压测工具的使用
# 安装ab压测工具
yum -y install httpd-tools
# 测试是否安装成功
ab -v
# 检测接口最大QPS
# -c表示同时访问的并发数,-n表示访问多少次,如下则表示10个用户同时访问100次
ab -n 1000 -c 100 https://www.baidu.com/
# 表示接口承受的最大QPS为17.35,即每秒最大处理的请求数为17个请求
Requests per second: 17.35 [#/sec] (mean)
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 关键指标
参数 | 说明 |
---|---|
Concurrency Level | 并发请求数,多少个用户同时访问 |
Complete requests | 完成的所有请求数 |
Time taken for tests | 完成压测花费的时间 |
Requests per second | 每秒处理的请求数,即QPS:Complete requests / Time taken for tests |
Time per request | 用户请求平均等待时间:Time taken for tests/(Concurrency Level/Complete requests) |
Time per request | 服务器处理每个请求花费的时间:Time taken for tests/Complete requests |
**注:**Time per request:(mean, across all concurrent requests)用来表示处理每个请求花费的时间
# 秒杀系统的分析
# 秒杀系统的重难点
# 稳定性
- 高并发下,某小依赖可能直接造成雪崩
- 流量预期难精确,过高也会造成雪崩
- 分布式集群,机器多,出故障的概率多
# 高可用
- 减少第三方依赖的使用,自身服务部署需要做到隔离
- 压测、降级、限流方案,确保核心服务可用
- 健康机制检查,整个链路避免单点
# 高性能
- 缩短请求的访问路径、减少IO
- 减少接口数、降低吞吐量、请求次数减少
# 秒杀系统基本需求分析
核心功能:
- 扣库存
- 查库存、排队进度
- 查订单详情、创建订单、支付订单
# 扣库存服务如何实现
- 减少上下文的切换
- 有效压榨CPU
# 扣库存分布式实现方案
传统的解决方案
下单减库存
graph LR
A[并发请求]--> B[创建订单] --> C[扣库存]-->D[支付]
1
2
2
支付减库存
graph LR
A[并发请求]--> B[创建订单] --> C[支付]-->D[扣库存]
1
2
2
上述两种方案主要区别在于是先进行支付,还是先扣库存,各有弊端:
- 下单减库存的方案:库存为100,把单子刷完了库存为0,这样别人就没法下单了,导致东西卖不出去。
- 支付减库存:因为支付和扣库存是绑定在一起的,如果客户下单太多会导致,订单没法支付的问题
从业务流程上解决,将扣库存和订单支付拆分开,保证扣库存环节扣的库存和实际订单的一致性。
graph LR
A[并发请求]--> B[扣库存] --> C[支付]-->D[创建订单]
1
2
2
对于下单完之后,不支付的订单,进行定时清理
先扣除库存,再创建订单、支付
10分钟内不支付订单,则进行取消,避免不支付订单卖不出去的问题
流程分析:
主要思路在于将扣库存分发到各个服务器上执行,lua单进程保证了redis数据的一致性
# 扣库存代码实现
实现了基本的连接redis、json输出
<?php
class Base
{
static $redisObj;
public static function conRedis($config = array())
{
// redis实例唯一
if (self::$redisObj) {
return self::$redisObj;
}
self::$redisObj = new \Redis();
self::$redisObj->connect("127.0.0.1", 6379);
return self::$redisObj;
}
public static function output($data = array(), $errNo = 0, $errMsg = 'ok')
{
$res['errno'] = $errNo;
$res['errmsg'] = $errMsg;
$res['data'] = $data;
echo json_encode($res);
exit();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
实现删除库存及统一增减库存
class Api extends Base
{
// 共享信息,存储在redis中,以hash表的形式存储,%s变量代表的是商品id
static $userId;
static $productId;
// 共享信息key
static $REDIS_REMOTE_HT_KEY = "product_%s";
// 商品总库存
static $REDIS_REMOTE_TOTAL_COUNT = "total_count";
// 已售库存
static $REDIS_REMOTE_USE_COUNT = "used_count";
// 创建订单队列
static $REDIS_REMOTE_QUEUE = "c_order_queue";
// 总共剩余库存
static $APCU_LOCAL_STOCK = "apcu_stock_%s";
// 本地已售多少
static $APCU_LOCAL_USE = "apcu_stock_use_%s";
// 本地分库存分摊总数
static $APCU_LOCAL_COUNT = "apcu_total_count_%s";
public function __construct($productId, $userId)
{
self::$REDIS_REMOTE_HT_KEY = sprintf(self::$REDIS_REMOTE_HT_KEY, $productId);
self::$APCU_LOCAL_STOCK = sprintf(self::$APCU_LOCAL_STOCK, $productId);
self::$APCU_LOCAL_USE = sprintf(self::$APCU_LOCAL_USE, $productId);
self::$APCU_LOCAL_COUNT = sprintf(self::$APCU_LOCAL_COUNT, $productId);
self::$APCU_LOCAL_COUNT = sprintf(self::$APCU_LOCAL_COUNT, $productId);
self::$userId = $userId;
self::$productId = $productId;
}
private static function init()
{
apcu_add(self::$APCU_LOCAL_COUNT, 150);
apcu_add(self::$APCU_LOCAL_USE, 0);
}
public static function clear()
{
apcu_delete(self::$APCU_LOCAL_STOCK);
apcu_delete(self::$APCU_LOCAL_USE);
apcu_delete(self::$APCU_LOCAL_COUNT);
}
/**
* 查剩余库存
*/
public static function getStock()
{
$stockNum = apcu_fetch(self::$APCU_LOCAL_STOCK);
if ($stockNum === false) {
$stockNum = self::initStock();
}
self::output(['stock_num' => $stockNum]);
}
/**
* 抢购-减库存
*/
public static function buy()
{
// 读取分库的库存数,没有则进行初始化
$localStockNum = apcu_fetch(self::$APCU_LOCAL_COUNT);
if ($localStockNum === false) {
$localStockNum = self::init();
}
// 对库存数进行+1
$localUse = apcu_inc(self::$APCU_LOCAL_USE);//本已卖 + 1
if ($localUse > $localStockNum) {
//抢购失败 大部分流量在此被拦截
self::output([], -1, '该商品已售完');
}
// 同步远端已售库存 + 1;
if (!self::incUseCount()) {//改失败,返回商品已售完
self::output([], -1, '该商品已售完');
}
//写入创建订单队列
self::conRedis()->lPush(self::$REDIS_REMOTE_QUEUE, json_encode(['user_id' => self::$userId, 'product_id' => self::$productId]));
//返回抢购成功
self::output([], 0, '抢购成功,请从订单中心查看订单');
}
/**
* 总剩余库存同步本地,定时执行就可以
*/
public static function sync()
{
$data = self::conRedis()->hMGet(self::$REDIS_REMOTE_HT_KEY, [self::$REDIS_REMOTE_TOTAL_COUNT, self::$REDIS_REMOTE_USE_COUNT]);
$num = $data['total_count'] - $data["used_count"];
apcu_add(self::$APCU_LOCAL_STOCK, $num);
self::output([], 0, '同步库存成功');
}
//库存同步
private static function incUseCount()
{
$script = <<<eof
local key = KEYS[1]
local field1 = KEYS[2]
local field2 = KEYS[3]
local field1_val = redis.call('hget', key, field1)
local field2_val = redis.call('hget', key, field2)
if(field1_val>field2_val) then
return redis.call('HINCRBY', key, field2,1)
end
return 0
eof;
// 同步远端库存时,需要经过lua脚本,保证不会出现超卖现象
// lua为单进程阻塞式的,可以有效的避免并发读写带来的数据脏写的问题
// 判断远程的商品的库存是否超卖,没有超卖则进行+1
return self::conRedis()->eval($script, [self::$REDIS_REMOTE_HT_KEY, self::$REDIS_REMOTE_TOTAL_COUNT, self::$REDIS_REMOTE_USE_COUNT], 3);
}
public static function initStock()
{
$data = self::conRedis()->hMGet(self::$REDIS_REMOTE_HT_KEY, [self::$REDIS_REMOTE_TOTAL_COUNT, self::$REDIS_REMOTE_USE_COUNT]);
$num = $data['total_count'] - $data["used_count"];
apcu_add(self::$APCU_LOCAL_STOCK, $num);
return $num;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
代码测试
try {
$act = $_GET['act'];
$product_id = $_GET['product_id'];
$user_id = $_GET['user_id'];
$obj = new Api($product_id, $user_id);
if (method_exists($obj, $act)) {
$obj::$act();
die;
}
echo 'method_error!';
} catch (\Exception $e) {
echo 'exception_error!';
var_dump($e);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 参考链接
上次更新: 2020/12/01, 05:46:11