前言
我们实际开发中经常遇到定点秒杀业务,比如抢购商品、抢红包等等;这种情况下,一瞬间的并发访问量非常大,若设计不完善可能会出现超卖的现象。通过Redis的列表可以很好起到消峰的作用,同时实现业务之间的解耦。
环境
redis: 5.0.5
wrk: 4.1.0-4 # 压测工具(https://github.com/wg/wrk)
eg: wrk -t12 -c400 -d30s http://127.0.0.1:8080/index.html
-c, --connections: 连接:保持打开的HTTP连接的总数,每个线程处理N = 连接/线程
-d, --duration: 持续时间:试验持续时间,如2s, 2m, 2h
-t, --threads: 要使用的线程总数
-s, --script: LuaJIT脚本
-H, --header: 添加到请求的HTTP头文件,例如。“用户代理:wrk
--latency: 打印详细的延迟统计信息
--timeout: 如果在这段时间内没有收到响应,则记录超时。
需求
- 活动:抢购10件商品
- 使用Redis列表(LIST)实现
方案
- 判断列表长度是否小于10,如果小于10就向列表加入用户信息,表示用户抢到免单资格,否则就没有抢到;
代码:seckill_1.php <?php // 加载redis组件 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); // 秒杀任务Redis队列名称 $redis_name = 'seckill'; // 模拟用户ID $uid = mt_rand(1, 99999999); // 如果数量小于10,加入队列 if ($redis->lLen($redis_name) < 10) { // 将用户ID和时间采用%拼接加入队列 $redis->rPush($redis_name, $uid . '%' . microtime()); echo $uid . "秒杀成功\n"; } else { echo "秒杀已结束\n"; } $redis->close(); --------------------------------------------------------------------------- 接下来我们采用wrk并发测试 [root@Lewis ~]# /usr/local/wrk/wrk -t12 -c300 -d5s http://127.0.0.1:1010/queue_redis/seckill_1.php Running 5s test @ http://127.0.0.1:1010/queue_redis/seckill_1.php 12 threads and 300 connections Thread Stats Avg Stdev Max +/- Stdev Latency 500.58ms 472.27ms 1.99s 71.63% Req/Sec 27.74 28.30 242.00 89.31% 1133 requests in 5.04s, 235.73KB read Socket errors: connect 0, read 0, write 0, timeout 294 Requests/sec: 224.60 Transfer/sec: 46.73KB 正常情况下seckill列表中应该只有十条记录,我们验证一下 [root@Lewis ~]# redis-cli 127.0.0.1:6379> LRANGE seckill 0 -1 1) "57622090%0.74547100 1578362569" 2) "96859777%0.74460200 1578362569" 3) "9940359%0.74467400 1578362569" 4) "62113889%0.74470900 1578362569" 5) "10321533%0.74474300 1578362569" 6) "4424149%0.74484300 1578362569" 7) "85393877%0.74463800 1578362569" 8) "11917405%0.74480800 1578362569" 9) "96283400%0.74453800 1578362569" 10) "93670956%0.74490200 1578362569" 11) "55303655%0.74495300 1578362569" 12) "75197864%0.74501900 1578362569" 我们发现列表有12条记录,这就是并发所带来的超卖问题。
-
利用Redis的pop操作的原子性来实现秒杀。先定义一个长度为10的列表,请求时如果能从列表中pop出数据,表示抢购到,否则抢购失败。
代码:seckill_2.php <?php // 加载redis组件 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $redis_name = 'seckill'; $new_name = 'lucky_dog'; // 从队列最左侧取出一个值 $flag = $redis->lPop($redis_name); // 值不存在,直接范围失败 if (!$flag) { return json_encode(['status' => 0, 'msg' => "秒杀已结束"]); } // 模拟用户ID $uid = mt_rand(1, 99999999); // 将有资格用户加入新列表中,以便其他逻辑操作(比如再起一个进程保存到数据库) $redis->rPush($new_name, $uid . '%' . microtime()); $redis->close(); 首先,删除seckill列表,向列表中添加10条记录 127.0.0.1:6379> DEL seckill (integer) 1 127.0.0.1:6379> LPUSH seckill 1 2 3 4 5 6 7 8 9 10 (integer) 10 然后用wrk压测 [root@Lewis ~]# /usr/local/wrk/wrk -t12 -c300 -d5s http://127.0.0.1:1010/queue_redis/seckill_2.php Running 5s test @ http://127.0.0.1:1010/queue_redis/seckill_2.php 12 threads and 300 connections Thread Stats Avg Stdev Max +/- Stdev Latency 481.78ms 461.79ms 1.97s 68.52% Req/Sec 27.36 22.61 141.00 85.84% 1583 requests in 5.06s, 295.27KB read Socket errors: connect 0, read 0, write 0, timeout 58 Requests/sec: 312.68 Transfer/sec: 58.32KB 最后Redis验证结果 127.0.0.1:6379> LRANGE seckill 0 -1 (empty list or set) 127.0.0.1:6379> LRANGE lucky_dog 0 -1 1) "85047694%0.79134800 1578367304" 2) "24375119%0.79158500 1578367304" 3) "38742237%0.79166800 1578367304" 4) "48787460%0.79199000 1578367304" 5) "71600652%0.78556500 1578367304" 6) "64137637%0.78562400 1578367304" 7) "75032333%0.78537600 1578367304" 8) "61620233%0.78551300 1578367304" 9) "68069808%0.78525800 1578367304" 10) "74514593%0.79245800 1578367304" 结果符合预期结果