高并发下用户抢购问题简答
前言
面试题当中如何处理高并发用户抢购问题可以说是一个十分经典的问题,经常被提及,在这就这个问题写一个简要的解答;
思路
并发的最大瓶颈永远是数据库,MySQL的读写速度是制约并发的最大问题,而抢购之时真正需要写入的用户量实际上是很少的,等于抢购的商品总数.这就要求我们需要把无效的用户排除出.在前期放入有效用户量,一旦产品抢购结束,将活动页改为结束也的静态页面,最大程度的提升服务器相应速度.这里面放入限定数量的用户是最为关键的地方.
redis的高性能读写是现在最为主流的解决方案.下面就简单的介绍一下如何用redis来完成高并发抢购处理.
解决方案
将用户id写入redis列表当中,一旦列表长度达到商品总数,则拒掉后面的用户.
示例代码
Talk is cheap, show you my code.
<?php
/**
* Created by PhpStorm.
* User: mc
* Date: 18/3/21
* Time: 下午1:53
*/
$user_id = rand(100, 10000); // 模拟用户id
$key = 'user_list'; // 列表名
$redis = new Redis();
$redis->pconnect('localhost', 6379);
$len = $redis->lLen($key); // 获取列表长度
$count = 10; // 商品总数
if ($len >= $count) { // 达到商品总数则停止抢购
echo '秒杀已结束';
return '秒杀已结束';
}
$redis->lPush($key, $user_id); // 将用户id推入列表
echo '恭喜你秒杀成功';
return '恭喜你秒杀成功';
ab测
ab -n 3000 -c 100 http://localhost:8001/ // 模拟3000个请求,100个并发
结果
列表当中的结果确实是10条,满足要求
LRANGE user_list 0 -1
1) "466"
2) "5090"
3) "8299"
4) "6436"
5) "4537"
6) "9617"
7) "9291"
8) "2162"
9) "1903"
10) "983"
Concurrency Level: 100
Time taken for tests: 4.558 seconds
Complete requests: 3000
Failed requests: 0
Total transferred: 540000 bytes
HTML transferred: 45000 bytes
Requests per second: 658.22 [#/sec] (mean)
Time per request: 151.924 [ms] (mean)
Time per request: 1.519 [ms] (mean, across all concurrent requests)
Transfer rate: 115.70 [Kbytes/sec] received
QPS能够达到658.
问题
上面的代码看似没问题,可是要是老司机一定不难发现代码当中存在一个极大的漏洞.当列表当中有9个值,两个用户同时取得$len,那么这两个用户就会被同时写入列表当中,这样就会出现超卖的问题.而且写列表的方式就需要取数据要在抢购完成之后,这显然不合理.
这需要我们将抢购判断和用户列表拆分开来,redis当中的string有一一自增的api具有原子性,哪怕并发情况下也一定能够保证自增.这能够很好的服务于我们的需求.
代码示例
$user_id = rand(100, 10000); // 模拟用户id
$key = 'user_list'; // 列表名
$redis = new Redis();
$redis->connect('localhost', 6379);
$len = $redis->incr('count');
$count = 10; // 商品总数
if ($len > $count) { // 达到商品总数则停止抢购
echo '秒杀已结束';
return '秒杀已结束';
}
$redis->lPush($key, $user_id); // 将用户id推入列表
echo '恭喜你秒杀成功';
return '恭喜你秒杀成功';
这样就可以避免商品超售了,而且列表的生产和消费可以同步进行,提升业务体验.当然真正的业务当中复杂度远高于这里.
关于避免用户重复抢的简略方案
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$user_id = rand(1, 15);
if (!$redis->hSetNx('seckill', 'test:' . $user_id, 1)) { // hSetNx函数当key存在时会返回false,不存在时才会被设置成功,返回true
echo '您已经参加过秒杀请勿重复参加';
return '您已经参加过秒杀请勿重复参加';
}
$len = $redis->incr('count');
$count = 10; // 商品总数
if ($len > $count) { // 达到商品总数则停止抢购
echo '秒杀已结束';
return '秒杀已结束';
}
$redis->lPush('user_id', $user_id);
echo '恭喜您,秒杀成功';
return '恭喜您,秒杀成功';
上面的QPS才600+而原生的PHP在我的电脑下应该能到达3000+,这里面new Redis()创建redis连接耗费了过多的资源
连接复用
<?php
$redis = new Redis();
$redis->pconnect('127.0.0.1', 6379); // redis自带连接复用函数
$user_id = rand(1, 15);
if (!$redis->hSetNx('seckill', 'test:' . $user_id, 1)) { // hSetNx函数当key存在时会返回false,不存在时才会被设置成功,返回true
echo '您已经参加过秒杀请勿重复参加';
return '您已经参加过秒杀请勿重复参加';
}
$len = $redis->incr('count');
$count = 10; // 商品总数
if ($len > $count) { // 达到商品总数则停止抢购
echo '秒杀已结束';
return '秒杀已结束';
}
$redis->lPush('user_id', $user_id);
echo '恭喜您,秒杀成功';
return '恭喜您,秒杀成功';
Concurrency Level: 100
Time taken for tests: 0.428 seconds
Complete requests: 1000
Failed requests: 0
Total transferred: 180000 bytes
HTML transferred: 15000 bytes
Requests per second: 2337.77 [#/sec] (mean)
Time per request: 42.776 [ms] (mean)
Time per request: 0.428 [ms] (mean, across all concurrent requests)
Transfer rate: 410.94 [Kbytes/sec] received
结束
QPS能够高达2300+完全能够适应一般业务的并发需求了.