php结合redis实现高并发下的抢购、秒杀功能的实例

释放双眼,带上耳机,听听看~!

抢购、秒杀是如今很常见的一个应用场景,主要需要解决的问题有两个:

1 高并发对数据库产生的压力

2 竞争状态下如何解决库存的正确减少("超卖"问题)

对于第一个问题,已经很容易想到用缓存来处理抢购,避免直接操作数据库,例如使用Redis。

重点在于第二个问题

常规写法:

查询出对应商品的库存,看是否大于0,然后执行生成订单等操作,但是在判断库存是否大于0处,如果在高并发下就会有问题,导致库存量出现负数


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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
1<?php
2
3
4
5$conn=mysql_connect("localhost","big","123456");
6
7
8
9if(!$conn){
10
11
12
13    echo "connect failed";
14
15
16
17    exit;
18
19
20
21}
22
23
24
25mysql_select_db("big",$conn);
26
27
28
29mysql_query("set names utf8");
30
31
32
33 
34
35
36
37$price=10;
38
39
40
41$user_id=1;
42
43
44
45$goods_id=1;
46
47
48
49$sku_id=11;
50
51
52
53$number=1;
54
55
56
57 
58
59
60
61//生成唯一订单
62
63
64
65function build_order_no(){
66
67
68
69  return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
70
71
72
73}
74
75
76
77//记录日志
78
79
80
81function insertLog($event,$type=0){
82
83
84
85    global $conn;
86
87
88
89    $sql="insert into ih_log(event,type)
90
91
92
93    values('$event','$type')";
94
95
96
97    mysql_query($sql,$conn);
98
99
100
101}
102
103
104
105 
106
107
108
109//模拟下单操作
110
111
112
113//库存是否大于0
114
115
116
117$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";//解锁 此时ih_store数据中goods_id='$goods_id' and sku_id='$sku_id' 的数据被锁住(注3),其它事务必须等待此次事务 提交后才能执行
118
119
120
121$rs=mysql_query($sql,$conn);
122
123
124
125$row=mysql_fetch_assoc($rs);
126
127
128
129if($row['number']>0){//高并发下会导致超卖
130
131
132
133    $order_sn=build_order_no();
134
135
136
137    //生成订单
138
139
140
141    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
142
143
144
145    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
146
147
148
149    $order_rs=mysql_query($sql,$conn);
150
151
152
153     
154
155
156
157    //库存减少
158
159
160
161    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
162
163
164
165    $store_rs=mysql_query($sql,$conn);
166
167
168
169    if(mysql_affected_rows()){
170
171
172
173        insertLog('库存减少成功');
174
175
176
177    }else{
178
179
180
181        insertLog('库存减少失败');
182
183
184
185    }
186
187
188
189}else{
190
191
192
193    insertLog('库存不够');
194
195
196
197}
198
199
200
201?>
202

**优化方案1:**将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false

 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1//库存减少
2
3
4
5$sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0";
6
7
8
9$store_rs=mysql_query($sql,$conn);
10
11
12
13if(mysql_affected_rows()){
14
15
16
17    insertLog('库存减少成功');
18
19
20
21}
22

优化方案2:使用MySQL的事务,锁住操作的行


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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
1<?php
2
3
4
5$conn=mysql_connect("localhost","big","123456");
6
7
8
9if(!$conn){
10
11
12
13    echo "connect failed";
14
15
16
17    exit;
18
19
20
21}
22
23
24
25mysql_select_db("big",$conn);
26
27
28
29mysql_query("set names utf8");
30
31
32
33 
34
35
36
37$price=10;
38
39
40
41$user_id=1;
42
43
44
45$goods_id=1;
46
47
48
49$sku_id=11;
50
51
52
53$number=1;
54
55
56
57 
58
59
60
61//生成唯一订单号
62
63
64
65function build_order_no(){
66
67
68
69  return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
70
71
72
73}
74
75
76
77//记录日志
78
79
80
81function insertLog($event,$type=0){
82
83
84
85    global $conn;
86
87
88
89    $sql="insert into ih_log(event,type)
90
91
92
93    values('$event','$type')";
94
95
96
97    mysql_query($sql,$conn);
98
99
100
101}
102
103
104
105 
106
107
108
109//模拟下单操作
110
111
112
113//库存是否大于0
114
115
116
117mysql_query("BEGIN");   //开始事务
118
119
120
121$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行
122
123
124
125$rs=mysql_query($sql,$conn);
126
127
128
129$row=mysql_fetch_assoc($rs);
130
131
132
133if($row['number']>0){
134
135
136
137    //生成订单
138
139
140
141    $order_sn=build_order_no();
142
143
144
145    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
146
147
148
149    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
150
151
152
153    $order_rs=mysql_query($sql,$conn);
154
155
156
157     
158
159
160
161    //库存减少
162
163
164
165    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
166
167
168
169    $store_rs=mysql_query($sql,$conn);
170
171
172
173    if(mysql_affected_rows()){
174
175
176
177        insertLog('库存减少成功');
178
179
180
181        mysql_query("COMMIT");//事务提交即解锁
182
183
184
185    }else{
186
187
188
189        insertLog('库存减少失败');
190
191
192
193    }
194
195
196
197}else{
198
199
200
201    insertLog('库存不够');
202
203
204
205    mysql_query("ROLLBACK");
206
207
208
209}
210
211
212
213?>
214

**优化方案3:**使用非阻塞的文件排他锁


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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
1<?php
2
3
4
5$conn=mysql_connect("localhost","root","123456");
6
7
8
9if(!$conn){
10
11
12
13    echo "connect failed";
14
15
16
17    exit;
18
19
20
21}
22
23
24
25mysql_select_db("big-bak",$conn);
26
27
28
29mysql_query("set names utf8");
30
31
32
33 
34
35
36
37$price=10;
38
39
40
41$user_id=1;
42
43
44
45$goods_id=1;
46
47
48
49$sku_id=11;
50
51
52
53$number=1;
54
55
56
57 
58
59
60
61//生成唯一订单号
62
63
64
65function build_order_no(){
66
67
68
69  return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
70
71
72
73}
74
75
76
77//记录日志
78
79
80
81function insertLog($event,$type=0){
82
83
84
85    global $conn;
86
87
88
89    $sql="insert into ih_log(event,type)
90
91
92
93    values('$event','$type')";
94
95
96
97    mysql_query($sql,$conn);
98
99
100
101}
102
103
104
105 
106
107
108
109$fp = fopen("lock.txt", "w+");
110
111
112
113if(!flock($fp,LOCK_EX | LOCK_NB)){
114
115
116
117    echo "系统繁忙,请稍后再试";
118
119
120
121    return;
122
123
124
125}
126
127
128
129//下单
130
131
132
133$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
134
135
136
137$rs=mysql_query($sql,$conn);
138
139
140
141$row=mysql_fetch_assoc($rs);
142
143
144
145if($row['number']>0){//库存是否大于0
146
147
148
149    //模拟下单操作
150
151
152
153    $order_sn=build_order_no();
154
155
156
157    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
158
159
160
161    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
162
163
164
165    $order_rs=mysql_query($sql,$conn);
166
167
168
169     
170
171
172
173    //库存减少
174
175
176
177    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
178
179
180
181    $store_rs=mysql_query($sql,$conn);
182
183
184
185    if(mysql_affected_rows()){
186
187
188
189        insertLog('库存减少成功');
190
191
192
193        flock($fp,LOCK_UN);//释放锁
194
195
196
197    }else{
198
199
200
201        insertLog('库存减少失败');
202
203
204
205    }
206
207
208
209}else{
210
211
212
213    insertLog('库存不够');
214
215
216
217}
218
219
220
221fclose($fp);
222

**优化方案4:**使用redis队列,因为pop操作是原子的,即使有很多用户同时到达,也是依次执行,推荐使用(mysql事务在高并发下性能下降很厉害,文件锁的方式也是)

先将商品库存如队列

 


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
1<?php
2
3
4
5$store=1000;
6
7
8
9$redis=new Redis();
10
11
12
13$result=$redis->connect('127.0.0.1',6379);
14
15
16
17$res=$redis->llen('goods_store');
18
19
20
21echo $res;
22
23
24
25$count=$store-$res;
26
27
28
29for($i=0;$i<$count;$i++){
30
31
32
33    $redis->lpush('goods_store',1);
34
35
36
37}
38
39
40
41echo $redis->llen('goods_store');
42
43
44
45?>
46

抢购、描述逻辑


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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
1<?php
2
3
4
5$conn=mysql_connect("localhost","big","123456");
6
7
8
9if(!$conn){
10
11
12
13    echo "connect failed";
14
15
16
17    exit;
18
19
20
21}
22
23
24
25mysql_select_db("big",$conn);
26
27
28
29mysql_query("set names utf8");
30
31
32
33 
34
35
36
37$price=10;
38
39
40
41$user_id=1;
42
43
44
45$goods_id=1;
46
47
48
49$sku_id=11;
50
51
52
53$number=1;
54
55
56
57 
58
59
60
61//生成唯一订单号
62
63
64
65function build_order_no(){
66
67
68
69  return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
70
71
72
73}
74
75
76
77//记录日志
78
79
80
81function insertLog($event,$type=0){
82
83
84
85    global $conn;
86
87
88
89    $sql="insert into ih_log(event,type)
90
91
92
93    values('$event','$type')";
94
95
96
97    mysql_query($sql,$conn);
98
99
100
101}
102
103
104
105 
106
107
108
109//模拟下单操作
110
111
112
113//下单前判断redis队列库存量
114
115
116
117$redis=new Redis();
118
119
120
121$result=$redis->connect('127.0.0.1',6379);
122
123
124
125$count=$redis->lpop('goods_store');
126
127
128
129if(!$count){
130
131
132
133    insertLog('error:no store redis');
134
135
136
137    return;
138
139
140
141}
142
143
144
145 
146
147
148
149//生成订单
150
151
152
153$order_sn=build_order_no();
154
155
156
157$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
158
159
160
161values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
162
163
164
165$order_rs=mysql_query($sql,$conn);
166
167
168
169 
170
171
172
173//库存减少
174
175
176
177$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
178
179
180
181$store_rs=mysql_query($sql,$conn);
182
183
184
185if(mysql_affected_rows()){
186
187
188
189    insertLog('库存减少成功');
190
191
192
193}else{
194
195
196
197    insertLog('库存减少失败');
198
199
200
201}
202

模拟5000高并发测试

webbench -c 5000 -t 60 http://192.168.1.198/big/index.php
ab -r -n 6000 -c 5000  http://192.168.1.198/big/index.php

上述只是简单模拟高并发下的抢购,真实场景要比这复杂很多,很多注意的地方

如抢购页面做成静态的,通过ajax调用接口

再如上面的会导致一个用户抢多个,思路:

需要一个排队队列和抢购结果队列及库存队列。高并发情况,先将用户进入排队队列,用一个线程循环处理从排队队列取出一个用户,判断用户是否已在抢购结果队列,如果在,则已抢购,否则未抢购,库存减1,写数据库,将用户入结果队列。

测试数据表


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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
1--
2
3
4
5-- 数据库: `big`
6
7
8
9--
10
11
12
13 
14
15
16
17-- --------------------------------------------------------
18
19
20
21 
22
23
24
25--
26
27
28
29-- 表的结构 `ih_goods`
30
31
32
33--
34
35
36
37 
38
39
40
41 
42
43
44
45CREATE TABLE IF NOT EXISTS `ih_goods` (
46
47
48
49  `goods_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
50
51
52
53  `cat_id` int(11) NOT NULL,
54
55
56
57  `goods_name` varchar(255) NOT NULL,
58
59
60
61  PRIMARY KEY (`goods_id`)
62
63
64
65) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=2 ;
66
67
68
69 
70
71
72
73 
74
75
76
77--
78
79
80
81-- 转存表中的数据 `ih_goods`
82
83
84
85--
86
87
88
89 
90
91
92
93 
94
95
96
97INSERT INTO `ih_goods` (`goods_id`, `cat_id`, `goods_name`) VALUES
98
99
100
101(1, 0, '小米手机');
102
103
104
105 
106
107
108
109-- --------------------------------------------------------
110
111
112
113 
114
115
116
117--
118
119
120
121-- 表的结构 `ih_log`
122
123
124
125--
126
127
128
129 
130
131
132
133CREATE TABLE IF NOT EXISTS `ih_log` (
134
135
136
137 `id` int(11) NOT NULL AUTO_INCREMENT,
138
139
140
141 `event` varchar(255) NOT NULL,
142
143
144
145 `type` tinyint(4) NOT NULL DEFAULT '0',
146
147
148
149 `addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
150
151
152
153 PRIMARY KEY (`id`)
154
155
156
157) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;
158
159
160
161 
162
163
164
165--
166
167
168
169-- 转存表中的数据 `ih_log`
170
171
172
173--
174
175
176
177 
178
179
180
181 
182
183
184
185-- --------------------------------------------------------
186
187
188
189 
190
191
192
193--
194
195
196
197-- 表的结构 `ih_order`
198
199
200
201--
202
203
204
205 
206
207
208
209CREATE TABLE IF NOT EXISTS `ih_order` (
210
211
212
213 `id` int(11) NOT NULL AUTO_INCREMENT,
214
215
216
217 `order_sn` char(32) NOT NULL,
218
219
220
221 `user_id` int(11) NOT NULL,
222
223
224
225 `status` int(11) NOT NULL DEFAULT '0',
226
227
228
229 `goods_id` int(11) NOT NULL DEFAULT '0',
230
231
232
233 `sku_id` int(11) NOT NULL DEFAULT '0',
234
235
236
237 `price` float NOT NULL,
238
239
240
241 `addtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
242
243
244
245 PRIMARY KEY (`id`)
246
247
248
249) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='订单表' AUTO_INCREMENT=1 ;
250
251
252
253 
254
255
256
257--
258
259
260
261-- 转存表中的数据 `ih_order`
262
263
264
265--
266
267
268
269 
270
271
272
273 
274
275
276
277-- --------------------------------------------------------
278
279
280
281 
282
283
284
285--
286
287
288
289-- 表的结构 `ih_store`
290
291
292
293--
294
295
296
297 
298
299
300
301CREATE TABLE IF NOT EXISTS `ih_store` (
302
303
304
305 `id` int(11) NOT NULL AUTO_INCREMENT,
306
307
308
309 `goods_id` int(11) NOT NULL,
310
311
312
313 `sku_id` int(10) unsigned NOT NULL DEFAULT '0',
314
315
316
317 `number` int(10) NOT NULL DEFAULT '0',
318
319
320
321 `freez` int(11) NOT NULL DEFAULT '0' COMMENT '虚拟库存',
322
323
324
325 PRIMARY KEY (`id`)
326
327
328
329) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='库存' AUTO_INCREMENT=2 ;
330
331
332
333 
334
335
336
337--
338
339
340
341-- 转存表中的数据 `ih_store`
342
343
344
345--
346
347
348
349 
350
351
352
353INSERT INTO `ih_store` (`id`, `goods_id`, `sku_id`, `number`, `freez`) VALUES
354
355
356
357(1, 1, 11, 500, 0);
358

给TA打赏
共{{data.count}}人
人已打赏
安全经验

Google AdSense 全面解析(申请+操作+作弊+忠告)

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索