xxq's blog

go phper


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

MySQL事务隔离级别

发表于 2017-12-23   |   分类于 MySQL

一、事务的基本要素(ACID)

  1. 原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
  2. 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。
  3. 隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
  4. 持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。
  5. 小结:原子性是事务隔离的基础,隔离性和持久性是手段,最终目的是为了保持数据的一致性。

二、事务的并发问题

  1. 脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
  2. 不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
  3. 幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
  4. 小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

三、MySQL事务隔离级别

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted) 是 是 是
不可重复读(read-committed) 否 是 是
可重复读(repeatable-read) 否 否 是
串行化(serializable) 否 否 否

MySQL默认的事务隔离级别为可重复读

1
2
3
4
5
6
mysql> SELECT @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+

四、用例子说明各个隔离级别的情况

未提交读(read-uncommitted)

打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> SET session TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
1 row in set (0.00 sec)
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 450 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
在客户端A的事务提交之前,打开另一个客户端B,更新表account:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> SET session TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance = balance - 50 where id = 1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 400 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:
1
2
3
4
5
6
7
8
9
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 400 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:
1
2
3
4
5
6
7
8
9
10
11
12
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 450 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
在客户端A执行更新语句update account set balance = balance - 50 where id =1,lilei的balance没有变成350,居然是400,是不是很奇怪,数据的一致性没问啊,如果你这么想就太天真 了,在应用程序中,我们会用400-50=350,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别
1
2
3
4
5
6
7
8
9
10
11
12
13
mysql> update account set balance = balance - 50 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 400 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)

已提交读(read-committed)

打开一个客户端A,并设置当前事务模式为read committed(未提交读),查询表account的初始值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> SET session TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 450 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
在客户端A的事务提交之前,打开另一个客户端B,更新表account:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> SET session TRANSACTION ISOLATION LEVEL READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance = balance - 50 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 400 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:
1
2
3
4
5
6
7
8
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 450 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
客户端B的事务提交
1
2
3
4
5
6
7
8
9
10
11
12
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 400 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题,在应用程序中,假设我们处于客户端A的会话,查询到lilei的balance为450,但是其他事务将lilei的balance值改为400,我们并不知道,如果用450这个值去做其他操作,是有问题的,不过这个概率真的很小哦,要想避免这个问题,可以采用可重复读的隔离级别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 450 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 400 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)

可重复读(repeatable-read)

打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的初始值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql> SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 400 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交,客户端B的事务居然可以修改客户端A事务查询到的行,也就是mysql的可重复读不会锁住事务查询到的行,这一点出乎我的意料,sql标准中事务隔离级别为可重复读时,读写操作要锁行的,mysql居然没有锁,我了个去。在应用程序中要注意给行加锁,不然你会以步骤(1)中lilei的balance为400作为中间值去做其他操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> SET session TRANSACTION ISOLATION LEVEL REPEATABLE READ;
Query OK, 0 rows affected (0.00 sec)
mysql> update account set balance = balance - 50 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 350 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
在客户端A执行步骤(1)的查询:
1
2
3
4
5
6
7
8
9
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 400 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
客户端A,lilei的balance仍然是400与步骤(1)查询结果一致,没有出现不可重复读的问题;接着执行update balance = balance - 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤(2)中的350来算的,所以是300,数据的一致性倒是没有被破坏,这个有点神奇,也许是mysql的特色吧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 400 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
mysql> update account set balance = balance - 50 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 300 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
在客户端B开启事务,新增一条数据,其中balance字段值为600,并提交
1
2
3
4
5
6
7
8
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(null,'lily',600);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
在客户端A计算balance之和,值为300+850+1850=3000,没有把客户端B的值算进去,客户端A提交后再计算balance之和,居然变成了3600,这是因为把客户端B的600算进去了,站在客户的角度,客户是看不到客户端B的,它会觉得是天下掉馅饼了,多了600块,这就是幻读,站在开发者的角度,数据的一致性并没有破坏。但是在应用程序中,我们得代码可能会把18700提交给用户了,如果你一定要避免这情况小概率状况的发生,那么就要采取下面要介绍的事务隔离级别“串行化”
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
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 300 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
+----+-----------+---------+
3 rows in set (0.00 sec)
mysql> select sum(balance) from account;
+--------------+
| sum(balance) |
+--------------+
| 3000 |
+--------------+
1 row in set (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
mysql> select sum(balance) from account;
+--------------+
| sum(balance) |
+--------------+
| 3600 |
+--------------+
1 row in set (0.00 sec)

串行化(serializable)

打开一个客户端A,并设置当前事务模式为serializable,查询表account的初始值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mysql> SET session TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Query OK, 0 rows affected (0.00 sec)
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from account;
+----+-----------+---------+
| id | name | balance |
+----+-----------+---------+
| 1 | lilei | 300 |
| 2 | hanmeimei | 850 |
| 3 | tom | 1850 |
| 4 | lily | 600 |
+----+-----------+---------+
4 rows in set (0.00 sec)
打开一个客户端B,并设置当前事务模式为serializable,插入一条记录报错,表被锁了插入失败,mysql中事务隔离级别为serializable时会锁表,因此不会出现幻读的情况,这种隔离级别并发性极低,往往一个事务霸占了一张表,其他成千上万个事务只有干瞪眼,得等他用完提交才可以使用,开发中很少会用到。
1
2
3
4
5
6
7
8
mysql> SET session TRANSACTION ISOLATION LEVEL SERIALIZABLE;
Query OK, 0 rows affected (0.00 sec)
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into account values(null,'lisa',0);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

五、参考

MySQL的四种事务隔离级别

Mysql防止库存超卖

发表于 2017-12-23   |   分类于 PHP

电商系统经常会有各种活动,如团购、秒杀、特价。活动商品库存有限,面对段时间成千上万的用户抢购,怎么保证商品不会超卖?

例子

商品ID:100
总库存:20个
请求人:a(购买5个)、b(购买10个)、c(购买6个)
分析:由于5+10+6=21大于总库存,所以必定第三人不能购买成功,否则库存超卖

减库存SQL

下单必须有足够的库存, 所以库存必须大于需要购买的库存数量

1
UPDATE goods SET stock = stock - 2 WHERE goods_id = 100 AND stock >= 2;

下单量大怎么处理

下单量大的话,会有大量的写请求打到Mysql服务器,造成数据库服务器压力甚至导致数据库宕机,所以我们利用Redis decrby命令来减少大量到达Mysql的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#商品1的库存
$key = "goods:1:stock";
$script = '
local remainNum = redis.call("decrby", KEYS[1], ARGV[1])
if remainNum < 0
then
redis.call("incrby", KEYS[1], ARGV[1])
return 0
else
return 1
end';
#随机购买1到5个,压测
$buyNumber = mt_rand(1, 5);
$re = $this->redis->eval($script, [$key, $buyNumber], 1);
#记录日志
error_log(sprintf("购买数:%s, 购买结果:%s\n", $buyNumber, $re ? '成功' : '失败'), 3, '/tmp/buy_result.log');

Redis脚本压测

压测预期:100个库存被购买完毕,且最终库存数为0,不会为负数

压测命令
1
siege -c 50 -r 10 http://test.haha.com/
压测结果
1
2
3
4
5
6
7
8
9
10
11
12
Transactions: 500 hits
Availability: 100.00 %
Elapsed time: 3.58 secs
Data transferred: 0.00 MB
Response time: 0.02 secs
Transaction rate: 139.66 trans/sec
Throughput: 0.00 MB/sec
Concurrency: 2.46
Successful transactions: 500
Failed transactions: 0
Longest transaction: 0.10
Shortest transaction: 0.00
压测日志
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
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
购买数:3, 购买结果:成功
购买数:5, 购买结果:成功
购买数:2, 购买结果:成功
购买数:1, 购买结果:成功
购买数:3, 购买结果:成功
购买数:3, 购买结果:成功
购买数:4, 购买结果:成功
购买数:2, 购买结果:成功
购买数:2, 购买结果:成功
购买数:1, 购买结果:成功
购买数:5, 购买结果:成功
购买数:1, 购买结果:成功
购买数:5, 购买结果:成功
购买数:5, 购买结果:成功
购买数:3, 购买结果:成功
购买数:1, 购买结果:成功
购买数:2, 购买结果:成功
购买数:2, 购买结果:成功
购买数:2, 购买结果:成功
购买数:3, 购买结果:成功
购买数:5, 购买结果:成功
购买数:4, 购买结果:成功
购买数:2, 购买结果:成功
购买数:1, 购买结果:成功
购买数:2, 购买结果:成功
购买数:1, 购买结果:成功
购买数:3, 购买结果:成功
购买数:4, 购买结果:成功
购买数:1, 购买结果:成功
购买数:4, 购买结果:成功
购买数:1, 购买结果:成功
购买数:4, 购买结果:成功
购买数:3, 购买结果:成功
购买数:1, 购买结果:成功
购买数:2, 购买结果:成功
购买数:3, 购买结果:成功
购买数:3, 购买结果:成功
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:成功
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:3, 购买结果:失败
购买数:3, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:3, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:3, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:4, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:2, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
购买数:1, 购买结果:失败
购买数:5, 购买结果:失败
购买数:3, 购买结果:失败
购买数:5, 购买结果:失败
购买数:5, 购买结果:失败
购买数:2, 购买结果:失败
购买数:5, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:1, 购买结果:失败
购买数:4, 购买结果:失败
最终库存
1
2
127.0.0.1:6379> get goods:1:stock
"0"

符合预期

下单量还是大怎么处理

限流(Openresty 令牌桶、漏桶、计数器)
队列削峰(Rabbit、Kafka)
异步处理(下单返订单号,处理完成异步通知)
内存缓存
秒杀开始前和库存为0后直接返回生成好的静态html页面
负载均衡(加机器)
Mysql连接池

PHP可变参数

发表于 2017-12-21   |   分类于 PHP

PHP Redis扩展的zRem、sRem等删除多个元素时传可变参数数组

1
2
3
4
5
6
7
#定义: public function zRem( $key, $member1, $member2 = null, $memberN = null ) {}
#示例
$members = [1, 2, 3];
$res = $this->redis->zRem('wq21212', ...$members);
var_dump($res);

Redis+Lua锁

发表于 2017-12-19   |   分类于 PHP

一、Redis单例

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
/**
* Redis单例
*
* User: xiangqian
* Date: 17/8/3
* Time: 下午3:40
*/
namespace Model\Redis;
use \Redis;
class BaseModel
{
private static $_instances = [];
public $redis;
/**
* 构造方法
*
* BaseModel constructor.
* @param array $config
*/
private function __construct(array $config)
{
try {
$this->redis = new Redis();
$this->redis->connect($config['host'], $config['port'], $config['time_out']);
$this->redis->auth($config['auth']);
} catch (\RedisException $e) {
exit($e->getMessage());
}
}
/**
* 获取实例
*
* @param array $config
* @return BaseModel
*/
public static function getInstance(array $config)
{
ksort($config);
$key = md5(serialize($config));
if (isset(self::$_instances[$key]) && self::$_instances[$key] instanceof self) {
return self::$_instances[$key];
}
self::$_instances[$key] = new self($config);
return self::$_instances[$key];
}
/**
* 禁止克隆
*/
public function __clone()
{
trigger_error('can not clone', E_USER_ERROR);
}
}

二、Redis + Lua锁实现

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
/**
* Redis + Lua实现锁
*
* User: xiangqian
* Date: 17/12/19
* Time: 下午5:14
*/
namespace Model\Redis;
class Lock
{
private static $redisConfig = [
'host' => '127.0.0.1',
'port' => 6379,
'time_out' => 1,
'auth' => '123456'
];
/**
* 加锁
*
* @param $key
* @param $token
* @param int $ttl
* @return int
*/
public static function lock($key, $token, $ttl = 100)
{
$script = '
local ok = redis.call("setnx", KEYS[1], ARGV[1])
if ok == 1 then
redis.call("expire", KEYS[1], ARGV[2])
end
return ok';
return BaseModel::getInstance(self::$redisConfig)->redis->eval($script, [$key, $token, $ttl], 1);
}
/**
* 释放锁
*
* @param $key
* @param $token
* @return int
*/
public static function unlock($key, $token)
{
$script = '
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end';
return BaseModel::getInstance(self::$redisConfig)->redis->eval($script, [$key, $token], 1);
}
}

三、使用示例

1
2
3
4
5
$key = 'test_key'; //根据业务定名称
$token = uniqid($key); //每个请求生成唯一的token, 防止被别的请求释放锁
Lock::lock($key, $token, 60); //加锁
#do something with lock
Lock::unlock($key, $token); //释放锁

Linux常用命令,备忘(不完全)

发表于 2017-06-27   |   分类于 Linux

lsof -i:4000 查看指定端口的进程

split 文件拆分
usage: split [-a sufflen] [-b byte_count] [-l line_count] [-p pattern] [file [prefix]]

cat small_files* > large_file 文件进行合并

PHP7新特性

发表于 2017-02-20   |   分类于 PHP7

PHP 标量类型与返回值类型声明

默认情况下,所有的PHP文件都处于弱类型校验模式。

PHP7增加了标量类型声明的特性,标量类型声明有两种模式:

1. 强制模式 (默认)
2. 严格模式 (强类型)

参数类型:int、 string、 float、 bool、 interfaces、 array、 callable
代码示例:

1
2
3
4
5
6
7
8
declare(strict_types=1); //开启严格模式
function sum(int ...$ints)
{
return array_sum($ints);
}
print(sum(2, '3', 4.1)); //PHP Fatal error: Uncaught TypeError: Argument 2 passed to sum() must be of the type integer, string given, called in.....
print(sum(1, 3, 6)); //正确

PHP NULL 合并运算符

PHP7新增加的 NULL 合并运算符(??)是用于执行isset()检测的三元运算的快捷方式。
NULL 合并运算符会判断变量是否存在且值不为NULL,如果是,它就会返回自身的值,否则返回它的第二个操作数。
代码示例:

1
2
$site = isset($_GET['site']) ? $_GET['site'] : '菜鸟教程'; //PHP7前写法
$site = $_GET['site'] ?? '菜鸟教程'; //PHP7 NULL合并运算符 ??

PHP 太空船运算符(组合比较符)

PHP 7 新增加的太空船运算符(组合比较符)用于比较两个表达式 $a 和 $b,如果 $a 小于、等于或大于 $b时,它分别返回-1、0或1。

代码示例:

1
2
3
4
//整型比较
print( 1 <=> 1);print(PHP_EOL); //返回0
print( 1 <=> 2);print(PHP_EOL); //返回-1
print( 2 <=> 1);print(PHP_EOL); //返回1

PHP 常量数组

在PHP5.6中仅能通过 const 定义常量数组,PHP7 可以通过 define() 来定义。

代码示例:

1
2
3
4
5
6
define('sites', [
'baidu',
'qq',
'google'
'taobao'
]);

PHP 匿名类

PHP7支持通过 new class 来实例化一个匿名类,这可以用来替代一些”用后即焚”的完整类定义。

代码示例:

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
interface Logger
{
public function log(string $msg);
}
class Application
{
private $logger;
public function getLogger(): Logger
{
return $this->logger;
}
public function setLogger(Logger $logger)
{
$this->logger = $logger;
}
}
$app = new Application;
//使用 new class 创建匿名类
$app->setLogger(new class implements Logger {
public function log(string $msg) {
error_log($msg . PHP_EOL, 3, 'my_xiangqian.log');
print($msg);
}
});
$app->getLogger()->log("我的第一条日志");

PHP Closure::call()

PHP 7 的 Closure::call() 有着更好的性能,将一个闭包函数动态绑定到一个新的对象实例并调用执行该函数。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class A
{
private $x = 1;
}
//PHP7之前定义闭包函数代码
$getXCB = function()
{
return $this->x;
}
//闭包函数绑定到类A上
$getX = $getXCB->bindTo(new A, 'A');
echo $getX . PHP_EOL;
//PHP7+代码
$getX = function()
{
return $this->x;
}
echo $getX->call(new A) . PHP_EOL;

PHP 过滤 unserialize()

PHP IntlChar()

PHP CSPRNG

PHP7 异常

PHP7 use 语句

PHP7可以使用一个 use 从同一个 namespace 中导入类、函数和常量
示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//PHP7之前版本需要使用多次 use
use some\namespace\ClassA;
use some\namespace\ClassB;
use some\namespace\ClassC as C;
use function some\namespace\fn_a;
use function some\namespace\fn_b;
use function some\namespace\fn_c;
use const some\namespace\ConstA;
use const some\namespace\ConstB;
use const some\namespace\ConstC;
//PHP7+之后版本可以使用一个 use 导入同一个 namespace 的类
use some\namespace\{ClassA, ClassB, ClassC as C};
use function some\namespace\{fn_a, fn_b, fn_c};
use const some\namespace\{ConstA, ConstB, ConstC};

PHP7 错误处理

PHP 7 改变了大多数错误的报告方式。不同于 PHP 5 的传统错误报告机制,现在大多数错误被作为 Error 异常抛出。
这种 Error 异常可以像普通异常一样被 try / catch 块所捕获。如果没有匹配的 try / catch 块, 则调用异常处理函数(由 set_exception_handler() 注册)进行处理。 如果尚未注册异常处理函数,则按照传统方式处理:被报告为一个致命错误(Fatal Error)。
Error 类并不是从 Exception 类 扩展出来的,所以用 catch (Exception $e) { … } 这样的代码是捕获不 到 Error 的。你可以用 catch (Error $e) { … } 这样的代码,或者通过注册异常处理函数( set_exception_handler())来捕获 Error。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MathDemo
{
public function doOperation(float $a, float $b) : float
{
try {
return $a % $b;
} catch (DivisionByZeroError $e) {
return 0;
}
}
}
$obj = new MathDemo();
echo $obj->doOperation(10, 0) . PHP_EOL;

PHP intdiv() 函数

PHP 7 新增加了 intdiv() 函数,接收两个参数,返回值为第一个参数除于第二个参数的值并取整。

1
2
3
echo intdiv(9,3),PHP_EOL; //3
echo intdiv(10,3),PHP_EOL; //2
echo intdiv(5,10),PHP_EOL; //0

PHP7 Session 选项

PHP7 废弃特性

  1. PHP4 风格的构造函数
  2. 以静态的方式调用非静态方法
  3. password_hash() 随机因子选项
  4. capture_session_meta SSL 上下文选项

PHP7 移除的扩展

  1. ereg
  2. mssql
  3. mysql
  4. sybase_ct

PHP7 移除的 SAPI

  1. aolserver
  2. apache
  3. apache_hooks
  4. apache2filter
  5. caudium
  6. continuity
  7. isapi
  8. milter
  9. nsapi
  10. phttpd
  11. pi3web
  12. roxen
  13. thttpd
  14. tux
  15. webjames

用Redis构建分布式锁

发表于 2017-02-19   |   分类于 Redis

在不同进程需要互斥地访问共享资源时,分布式锁是一种非常有用的技术手段。 有很多三方库和文章描述如何用Redis实现一个分布式锁管理器,但是这些库实现的方式差别很大,而且很多简单的实现其实只需采用稍微增加一点复杂的设计就可以获得更好的可靠性。 这篇文章的目的就是尝试提出一种官方权威的用Redis实现分布式锁管理器的算法,我们把这个算法称为RedLock,我们相信这个算法会比一般的普通方法更加安全可靠。我们也希望社区能一起分析这个算法,提供一些反馈,然后我们以此为基础,来设计出更加复杂可靠的算法,或者更好的新算法。

安全和可靠性保证

在描述我们的设计之前,我们想先提出三个属性,这三个属性在我们看来,是实现高效分布式锁的基础。

  1. 安全属性:互斥,不管任何时候,只有一个客户端能持有同一个锁。
  2. 效率属性A:不会死锁,最终一定会得到锁,就算一个持有锁的客户端宕掉或者发生网络分区。
  3. 效率属性B:容错,只要大多数Redis节点正常工作,客户端应该都能获取和释放锁。

为什么基于故障切换的方案不够好

为了理解我们想要提高的到底是什么,我们先看下当前大多数基于Redis的分布式锁三方库的现状。 用Redis来实现分布式锁最简单的方式就是在实例里创建一个键值,创建出来的键值一般都是有一个超时时间的(这个是Redis自带的超时特性),所以每个锁最终都会释放。而当一个客户端想要释放锁时,它只需要删除这个键值即可。 表面来看,这个方法似乎很管用,但是这里存在一个问题:在我们的系统架构里存在一个单点故障,如果Redis的master节点宕机了怎么办呢?有人可能会说:加一个slave节点!在master宕机时用slave就行了!但是其实这个方案明显是不可行的,因为这种方案无法保证第1个安全互斥属性,因为Redis的复制是异步的。 总的来说,这个方案里有一个明显的竞争条件(race condition),举例来说:

  1. 客户端A在master节点拿到了锁。
  2. master节点在把A创建的key写入slave之前宕机了。
  3. slave变成了master节点
  4. B也得到了和A还持有的相同的锁(因为原来的slave里还没有A持有锁的信息)

当然,在某些特殊场景下,前面提到的这个方案则完全没有问题,比如在宕机期间,多个客户端允许同时都持有锁,如果你可以容忍这个问题的话,那用这个基于复制的方案就完全没有问题,否则的话我们还是建议你采用这篇文章里接下来要描述的方案。

采用单实例的正确实现

在讲述如何用其他方案突破单实例方案的限制之前,让我们先看下是否有什么办法可以修复这个简单场景的问题,因为这个方案其实如果可以忍受竞争条件的话是有望可行的,而且单实例来实现分布式锁是我们后面要讲的算法的基础。 要获得锁,要用下面这个命令: SET resource_name my_random_value NX PX 30000 这个命令的作用是在只有这个key不存在的时候才会设置这个key的值(NX选项的作用),超时时间设为30000毫秒(PX选项的作用) 这个key的值设为“my_random_value”。这个值必须在所有获取锁请求的客户端里保持唯一。 基本上这个随机值就是用来保证能安全地释放锁,我们可以用下面这个Lua脚本来告诉Redis:删除这个key当且仅当这个key存在而且值是我期望的那个值。

1
2
3
4
5
6
if redis.call("get",KEYS[1]) == ARGV[1]
then
return redis.call("del",KEYS[1])
else
return 0
end

这个很重要,因为这可以避免误删其他客户端得到的锁,举个例子,一个客户端拿到了锁,被某个操作阻塞了很长时间,过了超时时间后自动释放了这个锁,然后这个客户端之后又尝试删除这个其实已经被其他客户端拿到的锁。所以单纯的用DEL指令有可能造成一个客户端删除了其他客户端的锁,用上面这个脚本可以保证每个客户单都用一个随机字符串’签名’了,这样每个锁就只能被获得锁的客户端删除了。

这个随机字符串应该用什么生成呢?我假设这是从/dev/urandom生成的20字节大小的字符串,但是其实你可以有效率更高的方案来保证这个字符串足够唯一。比如你可以用RC4加密算法来从/dev/urandom生成一个伪随机流。还有更简单的方案,比如用毫秒的unix时间戳加上客户端id,这个也许不够安全,但是也许在大多数环境下已经够用了。

key值的超时时间,也叫做”锁有效时间”。这个是锁的自动释放时间,也是一个客户端在其他客户端能抢占锁之前可以执行任务的时间,这个时间从获取锁的时间点开始计算。 所以现在我们有很好的获取和释放锁的方式,在一个非分布式的、单点的、保证永不宕机的环境下这个方式没有任何问题,接下来我们看看无法保证这些条件的分布式环境下我们该怎么做。

Redlock算法

在分布式版本的算法里我们假设我们有N个Redis master节点,这些节点都是完全独立的,我们不用任何复制或者其他隐含的分布式协调算法。我们已经描述了如何在单节点环境下安全地获取和释放锁。因此我们理所当然地应当用这个方法在每个单节点里来获取和释放锁。在我们的例子里面我们把N设成5,这个数字是一个相对比较合理的数值,因此我们需要在不同的计算机或者虚拟机上运行5个master节点来保证他们大多数情况下都不会同时宕机。一个客户端需要做如下操作来获取锁:

  1. 获取当前时间(单位是毫秒)。
  2. 轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。比如如果锁自动释放时间是10秒钟,那每个节点锁请求的超时时间可能是5-50毫秒的范围,这个可以防止一个客户端在某个宕掉的master节点上阻塞过长时间,如果一个master节点不可用了,我们应该尽快尝试下一个master节点。
  3. 客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),而且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。
  4. 如果锁获取成功了,那现在锁自动释放时间就是最初的锁释放时间减去之前获取锁所消耗的时间。
  5. 如果锁获取失败了,不管是因为获取成功的锁不超过一半(N/2+1)还是因为总消耗时间超过了锁释放时间,客户端都会到每个master节点上释放锁,即便是那些他认为没有获取成功的锁。

失败的重试

当一个客户端获取锁失败时,这个客户端应该在一个随机延时后进行重试,之所以采用随机延时是为了避免不同客户端同时重试导致谁都无法拿到锁的情况出现。同样的道理客户端越快尝试在大多数Redis节点获取锁,出现多个客户端同时竞争锁和重试的时间窗口越小,可能性就越低,所以最完美的情况下,客户端应该用多路传输的方式同时向所有Redis节点发送SET命令。 这里非常有必要强调一下客户端如果没有在多数节点获取到锁,一定要尽快在获取锁成功的节点上释放锁,这样就没必要等到key超时后才能重新获取这个锁(但是如果网络分区的情况发生而且客户端无法连接到Redis节点时,会损失等待key超时这段时间的系统可用性)

释放锁

释放锁比较简单,因为只需要在所有节点都释放锁就行,不管之前有没有在该节点获取锁成功。

代码(PHP)

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
/**
* Redis分布式锁实现
*/
namespace Lib\Lock;
class RedLock
{
private $retryDelay;
private $retryCount;
private $clockDriftFactor = 0.01;
private $quorum;
private $servers = [];
private $instances = [];
/**
* 构造方法
*
* RedLock constructor.
* @param array $servers
* @param int $retryDelay
* @param int $retryCount
*/
function __construct(array $servers, $retryDelay = 200, $retryCount = 3)
{
$this->servers = $servers; //多台redis实例
$this->retryDelay = $retryDelay; //重试延迟 毫秒
$this->retryCount = $retryCount; //重试次数
//获取锁成功数量必须大于等于该数(实例总数的半数以上->分布式锁互斥原则)
$this->quorum = min(count($servers), (count($servers) / 2 + 1));
}
/**
* 锁定
*
* @param $resource
* @param $ttl
* @return array|bool
*/
public function lock($resource, $ttl)
{
$this->initInstances();
$token = uniqid(); //唯一ID
$retry = $this->retryCount; //重试次数
do {
$n = 0; //获取锁成功数量
$startTime = microtime(true) * 1000; //开始时间
foreach ($this->instances as $instance) { //轮流在每个实例上请求锁
if ($this->lockInstance($instance, $resource, $token, $ttl)) {
$n++;
}
}
# Add 2 milliseconds to the drift to account for Redis expires
# precision, which is 1 millisecond, plus 1 millisecond min drift
# for small TTLs.
$drift = ($ttl * $this->clockDriftFactor) + 2; //偏移时间
//锁对象的有效时间=锁自动释放时间-(当前时间-开始时间)-偏移时间
$validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;
if ($n >= $this->quorum && $validityTime > 0) { //加锁成功
return [
'validity' => $validityTime,
'resource' => $resource,
'token' => $token,
];
} else { //加锁失败 轮流释放锁后重试
foreach ($this->instances as $instance) {
$this->unlockInstance($instance, $resource, $token);
}
}
//等待随机延迟重试
$delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);
usleep($delay * 1000);
$retry--;
} while ($retry > 0);
return false;
}
/**
* 解锁
*
* @param array $lock
*/
public function unlock(array $lock)
{
$this->initInstances();
$resource = $lock['resource'];
$token = $lock['token'];
foreach ($this->instances as $instance) {
$this->unlockInstance($instance, $resource, $token);
}
}
/**
* 获取redis连接
*/
private function initInstances()
{
if (empty($this->instances)) {
foreach ($this->servers as $server) {
list($host, $port, $timeout) = $server;
$redis = new \Redis();
$redis->connect($host, $port, $timeout);
$this->instances[] = $redis;
}
}
}
/**
* 加锁
*
* @param \Redis $instance
* @param $resource
* @param $token
* @param $ttl
* @return bool
*/
private function lockInstance(\Redis $instance, $resource, $token, $ttl)
{
return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);
}
/**
* 解锁
*
* @param \Redis $instance
* @param $resource
* @param $token
* @return mixed
*/
private function unlockInstance(\Redis $instance, $resource, $token)
{
$script = '
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
';
return $instance->eval($script, [$resource, $token], 1);
}
}

参考

转载自并发编程网, 《Redis官方文档》用Redis构建分布式锁

redis中的事务、lua脚本和管道的使用场景

发表于 2017-02-19   |   分类于 Redis

一、事务

Redis中的事务并不像MySQL中那么完美,只是简单的保证了原子性。redis中提供了四个命令来实现事务,MULTI类似于mysql中的BEGIN;EXEC类似于COMMIT;DISCARD类似于ROLLBACK;WATCH则是用于来实现mysql中类似锁的功能。具体的使用方法非常简单,例如:

1
2
3
4
5
6
7
8
9
10
11
12
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr count
QUEUED
127.0.0.1:6379> incr count
QUEUED
127.0.0.1:6379> exec
1) (integer) 1
2) (integer) 2
127.0.0.1:6379> get count
"2"
127.0.0.1:6379>

redis事务的实现原理是把事务中的命令先放入队列中,当client提交了exec命令后,redis会把队列中的每一条命令按序执行一遍。如果在执行exec之前事务中断了,那么所有的命令都不会执行;如果执行了exec命令之后,那么所有的命令都会按序执行。但如果在事务执行期间redis被强制关闭,那么则需要使用redis-check-aof 工具对redis进行修复,删除那些部分执行的命令。下面分几种情况讨论下redis事务中需要注意的地方:

  1. 入队命令语法错误,此时还没有执行exec命令

虽然redis在碰到exec命令之前不会执行事务中的命令,但是,它会对每个命令进行适当的检查,当发现有某些明显的语法错误时,如参数个数不正确,则会在入队时,返回错误信息,并当看到exec命令调用discard命令进行回滚。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
127.0.0.1:6379> get name
"xiangqian"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name sanduo
QUEUED
127.0.0.1:6379> set name
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get name
"xiangqian"
127.0.0.1:6379>

  1. 当exec执行完毕后,执行其它命令时发生错误

当redis在执行命令时,如果出现了错误,那么redis不会终止其它命令的执行。即只要是正确的命令,无论在错误命令之前还是之后,都会顺利执行。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
127.0.0.1:6379> lpush visited "name1"
(integer) 1
127.0.0.1:6379> get name
"xiangqian"
127.0.0.1:6379> get count
"2"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name sanduo
QUEUED
127.0.0.1:6379> get visited
QUEUED
127.0.0.1:6379> set count 20
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
127.0.0.1:6379> get name
"sanduo"
127.0.0.1:6379> get count
"20"
127.0.0.1:6379>

  1. 事务间的相互影响

事务中最长出现的影响就是同时修改一条记录,而redis中的事务默认没有对此进行处理,如果两个事务同时修改一条记录,首先执行exec的事务的结果将会被覆盖。这里我们可以使用watch命令,该命令用于监控某些具体的key,如果这些key被其它事务修改了,那么本事务再修改时就不会成功,然后返回失败的提示。

1
2
3
4
5
6
7
8
9
10
T1:
watch name
multi
set name Jeff
exec
T2:
watch name
multi
set name Kate
exec

如果T2先提交exec,那么T1提交时则更新失败,此时name依旧是Kate,然后在应用层决定是否需要重新执行该事务。

二、Lua脚本(2.6.0及以后版本)

Lua脚本其使用方法很简单,例如:

1
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

redis确保正一条script脚本执行期间,其它任何脚本或者命令都无法执行。正是由于这种原子性,script才可以替代MULTI/EXEC作为事务使用。当然,官方文档也说了,正是由于script执行的原子性,所以我们不要在script中执行过长开销的程序,否则会验证影响其它请求的执行。

另外,redis为了减少每次客户端发送来的数据带宽(如果script太长,则发送来的内容可能非常多),会把每次新出现的脚本的sha1摘要保存下来,这样后续如果script不变的话,只需要调用evalsha命令+script摘要即可,而不需要重复传递过长的脚本内容。例如:

1
2
3
4
127.0.0.1:6379> eval "return redis.call('get','foo')" 0
"bar"
127.0.0.1:6379> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0
"bar"

从这里可以看出把key和arg以参数的形式传递而不是直接写在script中的好处,因为这样可以把变量提取出来,使得script的sha1摘要保持不变,提高命中率。在应用程序中,可以先使用evalsha进行调用,如果失败,再使用eval进行操作,这样可以在一定程度上提高效率。

有了上面的知识,我们就可以使用lua脚本来灵活的使用redis的事务,这里举几个简单的例子。

场景1:我们要判断一个IP是不是第一次访问,如果是第一次访问,那么返回状态1,否则插入该ip,并返回状态0.

1
2
3
4
127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) then return 1 else redis.call('set', KEYS[1], 'test') return 0 end" 1 test_127.0.0.1
(integer) 0
127.0.0.1:6379> eval "if redis.call('get',KEYS[1]) then return 1 else redis.call('set', KEYS[1], 'test') return 0 end" 1 test_127.0.0.1
(integer) 1

场景2:使用redis限制30分钟内一个IP只允许访问5次

思路:每次想把当前的时间插入到redis的list中,然后判断list长度是否达到5次,如果大于5次,那么取出队首的元素,和当前时间进行判断,如果在30分钟之内,则返回-1,其它情况返回1.

1
eval "redis.call('rpush', KEYS[1],ARGV[1]);if (redis.call('llen',KEYS[1]) >tonumber(ARGV[2]))

通过上面两个场景可以看到,我们仅仅使用了lua的if语句,就可以实现这么方便的操作,如果使用其它的lua语法,肯定更加方便。

官网文档上有这样一段话:

A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.

由此可以看出,官方还是支持大家尽量使用lua script来代替transaction的。

三、管道

大家都知道redis是基于TCP连接进行通信的,每一个request/response都需要经历一个RTT往返时间,如果需要执行很多短小的命令,这些往返时间的开销是很大的,在此情形下,redis提出了管道来提高执行效率。管道的思想是:如果client执行一些相互之间无关的命令或者不需要获取命令的返回值,那么redis允许你连续发送多条命令,而不需要等待前面命令执行完毕。比如我们执行3条INCR命令,如果使用管道,理论上只需要一个RTT+3条命令的执行时间即可,如果不适用管道,那么可能需要额外的两个RTT时间。因此,管道相当于批处理脚本,相当于是命令集,例如:

1
2
3
4
5
6
7
8
9
//php代码示例
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$pipe = $redis->multi(Redis::PIPELINE);
for ($i = 0; $i < 10000; $i++) {
$pipe->set("key::$i", str_pad($i, 4, '0'));
$pipe->get("key::$i");
}
$replies = $pipe->exec();

Pipeline在某些场景下非常有用,比如有多个command需要被“及时的”提交,而且他们对相应结果没有互相依赖,而且对结果响应也无需立即获得,那么pipeline就可以充当这种“批处理”的工具;而且在一定程度上,可以较大的提升性能,性能提升的原因主要是TCP链接中较少了“交互往返”的时间。例如:因为业务需要,我们需要把用户的操作过程记录在日志中以方便以后的统计,每隔3个小时生成一个新的日志文件,那么后台处理线程,将会扫描日志文件并将每条日志输出为“operation”:1,即表示操作次数为1;如果每个operation都发送一个command,事实上性能是很差的,而且是没有必要的;那么我们就可以使用pipeline批量提交即可。

管道和事务是不同的,pipeline只是表达“交互”中操作的传递的方向性,pipeline也可以在事务中运行,也可以不在。无论如何,pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的相应中得到信息;也就是pipeline并不是表达“所有command都一起成功”的语义,管道中前面命令失败,后面命令不会有影响,继续执行。简单来说就是管道中的命令是没有关系的,它们只是像管道一样流水发给server,而不是串行执行,仅此而已;但是如果pipeline的操作被封装在事务中,那么将有事务来确保操作的成功与失败。

使用管道可能在效率上比使用script要好,但是有的情况下只能使用script。因为在执行后面的命令时,无法得到前面命令的结果,就像事务一样,所以如果需要在后面命令中使用前面命令的value等结果,则只能使用script或者事务+watch。

参考

redis中的事务、lua脚本和管道的使用场景

mongodb命令汇总

发表于 2017-01-22   |   分类于 MongoDB

MongoDB基本命令汇总

  1. find()/findOne()条件过滤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
> use test;
switched to db test
> db.Student.find({name: 'tony'});
{ "_id" : ObjectId("58847521dd517a3b4e43c52d"), "name" : "tony", "sex" : 1, "age" : 13 }
{ "_id" : ObjectId("5884762cdd517a3b4e43c532"), "name" : "tony", "sex" : 1, "age" : 13 }
{ "_id" : ObjectId("5884762cdd517a3b4e43c537"), "name" : "tony", "sex" : 1, "age" : 13 }
> db.Student.findOne();
{
"_id" : ObjectId("588474b8dd517a3b4e43c524"),
"name" : "jack",
"sex" : 1,
"age" : 33
}
  1. find()/findOne()指定返回的fileds
1
2
3
4
5
6
7
> db.Student.find({name: 'tony'}, {'_id': 0, 'name': 1})
{ "name" : "tony" }
{ "name" : "tony" }
{ "name" : "tony" }
> db.Student.findOne({age: 13}, {'_id': 0, 'name': 1, 'sex': 1})
{ "name" : "lily", "sex" : 0 }
  1. 查询条件
符号 对应 作用
$lt \< 小余
$lte \<= 小于或等于
$gt > 大于
$gte \= 大于或等于
$ne \<> 不等于
$in in in范围
$not not not范围
$or or 或者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
> db.Student.findOne({age: {$gte: 98}})
{
"_id" : ObjectId("58847521dd517a3b4e43c529"),
"name" : "nima",
"sex" : 1,
"age" : 98
}
> db.Student.findOne({age: {$ne: 98}})
{
"_id" : ObjectId("588474b8dd517a3b4e43c524"),
"name" : "jack",
"sex" : 1,
"age" : 33
}
> db.Student.find({$or: [{age: {$in: [12, 13]}}, {name: 'tony'}]}, {_id: 0})
{ "name" : "lily", "sex" : 0, "age" : 13 }
{ "name" : "hehh", "sex" : 0, "age" : 13 }
{ "name" : "tony", "sex" : 1, "age" : 13
  1. 特殊查询–null和exists

null可以匹配自身,而且可以匹配”不存在的”

1
2
3
4
5
> db.Student.find({name: null}, {_id: 0})
{ "name" : null, "sex" : 1, "age" : 18 }
{ "sex" : 1, "age" : 24 }
> db.Student.find({name:{$in:[null],$exists:true}}, {_id: 0})
{ "name" : null, "sex" : 1, "age" : 18 }

  1. 数组查询

造数据

1
2
3
db.Student.insert({name:"wjh",sex:1,age:18,color:["red","blue","black"]})
db.Student.insert({name:"lpj",sex:1,age:22,color:["white","blue","black"]})
db.Student.find()

1
2
3
4
5
6
7
8
> db.Student.find({color:"white"}, {_id: 0})
{ "name" : "lpj", "sex" : 1, "age" : 22, "color" : [ "white", "blue", "black" ] }
> db.Student.find({color:{$all:["red","blue"]}}, {_id: 0})
{ "name" : "wjh", "sex" : 1, "age" : 18, "color" : [ "red", "blue", "black" ] }
> db.Student.find({color:["red","blue","black"]}, {_id: 0})
{ "name" : "wjh", "sex" : 1, "age" : 18, "color" : [ "red", "blue", "black" ] }
> db.Student.find({"color.0":"white"}, {_id: 0})
{ "name" : "lpj", "sex" : 1, "age" : 22, "color" : [ "white", "blue", "black" ] }
  1. 排序
1
2
db.Student.find().sort({age:1})
db.Student.find().sort({age:1, sex:1})
  1. 分页
1
db.Student.find().sort({age:1}).limit(3).skip(3)
  1. 获取数量
1
2
db.Student.count()
db.Student.find().count()
  1. 删除数据
1
db.Student.remove({age: 22})
  1. 新增数据
1
db.Student.insert({name: 'sanduo', age: 16, sex: 1})
  1. 更新数据
1
2
3
db.Student.update({name: 'sanduo'}, {age: 66})
db.Student.update({name: 'sanduo'}, {$inc: {height : 175}}) //新增字段height
db.Student.update({name: 'sanduo'}, {$inc: {age : 185}}, true) //upsert存在则修改,不存在则新增

生成唯一订单号(16位字符串)

发表于 2017-01-21   |   分类于 PHP

思想: 时间+随机数, 由于订单一般会分库分表, 所以加上用户ID(userId)后两位, 具体怎么加可看具体情况, 理论上时间+随机数+用户ID后两位不能百分百保证唯一, 所以数据库字段还是得建唯一索引保证订单ID的绝对唯一。

规则:

  1. 年份取后两位足矣
  2. 月份取值范围为01-12,所以这里取月份的16进制数(dechex),减少位数
  3. 时分秒,date(‘His’)所表达的结果其实就是000000到235959,而且其中很多数字不会被用到比如126998。一天86400秒,如果从一天的0:0:0算起直到23:59:59使用00000-86400就可以完全表示,这样下来我们就完全可以把date(‘His’)换成五位数字。既然time()函数就是按秒计数,那咱就取time()结果的后五位,同一天之内后五位不会重复出现,比如今天0:0:0后五位是98765,那么到今天23:59:59后五位就应该是98765+86400去掉最高位,相信这个应该是很好理解的
  4. 随机数,秒小数点后取4位
  5. 用户ID(userId)取后两位或取余

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
/**
* 随机生成唯一订单号码
*
* @param int $userId
* @return string
* @throws Exception
*/
function createOrderId($userId = 0)
{
if (empty($userId)) {
throw new Exception('用户ID错误, userId=' . $userId);
}
list($ms, $time) = explode(' ', microtime());
$year = date('y', $time); //当前年份,2位数
$month = date('m', $time); //当前月份,2为数,转为16进制只占一位
$day = date('d', $time); //当前日期
return $year . strtoupper(dechex($month)) . $day . substr($time, -5) . substr($ms, 2, 4) . substr($userId, -2);
}
$userId = 110003978;
for ($i = 0; $i < 10; $i++) {
echo createOrderId($userId) . PHP_EOL;
}

12
xxq

xxq

记录平时学习

19 日志
10 分类
16 标签
github weibo
© 2017 xxq
由 Hexo 强力驱动
主题 - NexT.Pisces