我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Per Second,每秒处理请求数),解决每秒数万次的高并发场景,这个指标非常关键。举个例子,我们假设处理一个业务请求平均响应时间为100ms,同时,系统内有20台Apache的Web服务器,配置MaxClients为500个(表示Apache的最大连接数目)。

那么,我们的Web系统的理论峰值QPS为(理想化的计算方式):

20*500/0.1 = 100000 (10万QPS)

咦?我们的系统似乎很强大,1秒钟可以处理完10万的请求,5w/s的秒杀似乎是“纸老虎”哈。实际情况,当然没有这么理想。在高并发的实际场景下,机器都处于高负载的状态,在这个时候平均响应时间会被大大增加。

普通的一个p4的服务器每天最多能支持大约10万左右的IP,如果访问量超过10W那么需要专用的服务器才能解决,如果硬件不给力 软件怎么优化都是于事无补的。主要影响服务器的速度

有:网络-硬盘读写速度-内存大小-cpu处理速度。

就Web服务器而言,Apache打开了越多的连接进程,CPU需要处理的上下文切换也越多,额外增加了CPU的消耗,然后就直接导致平均响应时间增加。因此上述的MaxClient数目,要根据CPU、内存等硬件因素综合考虑,绝对不是越多越好。可以通过Apache自带的abench来测试一下,取一个合适的值。然后,我们选择内存操作级别的存储的Redis,在高并发的状态下,存储的响应时间至关重要。网络带宽虽然也是一个因素,不过,这种请求数据包一般比较小,一般很少成为请求的瓶颈。负载均衡成为系统瓶颈的情况比较少,在这里不做讨论哈。

那么问题来了,假设我们的系统,在5w/s的高并发状态下,平均响应时间从100ms变为250ms(实际情况,甚至更多):

20*500/0.25 = 40000 (4万QPS)

于是,我们的系统剩下了4w的QPS,面对5w每秒的请求,中间相差了1w。

举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定出现大塞车。(5条车道忽然变成4条车道的感觉)

同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。

其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数占满,其他正常的业务请求,无连接进程可用。

更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。

3. 重启与过载保护

如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。

秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回

高并发下的数据安全

我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

1. 超发的原因

假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。(同文章前面说的场景)

在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

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

 
 1 <?php
2 //优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false
3 include('./mysql.php');
4 $username = 'wang'.rand(0,1000);
5 //生成唯一订单
6 function build_order_no(){
7 return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
8 }
9 //记录日志
10 function insertLog($event,$type=0,$username){
11 global $conn;
12 $sql="insert into ih_log(event,type,usernma)
13 values('$event','$type','$username')";
14 return mysqli_query($conn,$sql);
15 }
16 function insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number)
17 {
18 global $conn;
19 $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price,username,number)
20 values('$order_sn','$user_id','$goods_id','$sku_id','$price','$username','$number')";
21 return mysqli_query($conn,$sql);
22 }
23 //模拟下单操作
24 //库存是否大于0
25 $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' ";
26 $rs=mysqli_query($conn,$sql);
27 $row = $rs->fetch_assoc();
28 if($row['number']>0){//高并发下会导致超卖
29 if($row['number']<$number){
30 return insertLog('库存不够',3,$username);
31 }
32 $order_sn=build_order_no();
33 //库存减少
34 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0";
35 $store_rs=mysqli_query($conn,$sql);
36 if($store_rs){
37 //生成订单
38 insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number);
39 insertLog('库存减少成功',1,$username);
40 }else{
41 insertLog('库存减少失败',2,$username);
42 }
43 }else{
44 insertLog('库存不够',3,$username);
45 }
46 ?>

2. 悲观锁思路

解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。

悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

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

 1 <?php
2 //优化方案2:使用MySQL的事务,锁住操作的行
3 include('./mysql.php');
4 //生成唯一订单号
5 function build_order_no(){
6 return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
7 }
8 //记录日志
9 function insertLog($event,$type=0){
10 global $conn;
11 $sql="insert into ih_log(event,type)
12 values('$event','$type')";
13 mysqli_query($conn,$sql);
14 }
15 //模拟下单操作
16 //库存是否大于0
17 mysqli_query($conn,"BEGIN"); //开始事务
18 $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行
19 $rs=mysqli_query($conn,$sql);
20 $row=$rs->fetch_assoc();
21 if($row['number']>0){
22 //生成订单
23 $order_sn=build_order_no();
24 $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
25 values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
26 $order_rs=mysqli_query($conn,$sql);
27 //库存减少
28 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
29 $store_rs=mysqli_query($conn,$sql);
30 if($store_rs){
31 echo '库存减少成功';
32 insertLog('库存减少成功');
33 mysqli_query($conn,"COMMIT");//事务提交即解锁
34 }else{
35 echo '库存减少失败';
36 insertLog('库存减少失败');
37 }
38 }else{
39 echo '库存不够';
40 insertLog('库存不够');
41 mysqli_query($conn,"ROLLBACK");
42 }
43 ?>
 

3. FIFO队列思路

那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。

然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。

4. 文件锁的思路

对于日IP不高或者说并发数不是很大的应用,一般不用考虑这些!用一般的文件操作方法完全没有问题。但如果并发高,在我们对文件进行读写操作时,很有可能多个进程对进一文件进行操作,如果这时不对文件的访问进行相应的独占,就容易造成数据丢失

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

 
 1 <?php
2 //优化方案4:使用非阻塞的文件排他锁
3 include ('./mysql.php');
4 //生成唯一订单号
5 function build_order_no(){
6 return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
7 }
8 //记录日志
9 function insertLog($event,$type=0){
10 global $conn;
11 $sql="insert into ih_log(event,type)
12 values('$event','$type')";
13 mysqli_query($conn,$sql);
14 }
15 $fp = fopen("lock.txt", "w+");
16 if(!flock($fp,LOCK_EX | LOCK_NB)){
17 echo "系统繁忙,请稍后再试";
18 return;
19 }
20 //下单
21 $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
22 $rs = mysqli_query($conn,$sql);
23 $row = $rs->fetch_assoc();
24 if($row['number']>0){//库存是否大于0
25 //模拟下单操作
26 $order_sn=build_order_no();
27 $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
28 values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
29 $order_rs = mysqli_query($conn,$sql);
30 //库存减少
31 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
32 $store_rs = mysqli_query($conn,$sql);
33 if($store_rs){
34 echo '库存减少成功';
35 insertLog('库存减少成功');
36 flock($fp,LOCK_UN);//释放锁
37 }else{
38 echo '库存减少失败';
39 insertLog('库存减少失败');
40 }
41 }else{
42 echo '库存不够';
43 insertLog('库存不够');
44 }
45 fclose($fp);
46 ?>
 
 
 1 <?php
2 //优化方案4:使用非阻塞的文件排他锁
3 include ('./mysql.php');
4 //生成唯一订单号
5 function build_order_no(){
6 return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
7 }
8 //记录日志
9 function insertLog($event,$type=0){
10 global $conn;
11 $sql="insert into ih_log(event,type)
12 values('$event','$type')";
13 mysqli_query($conn,$sql);
14 }
15 $fp = fopen("lock.txt", "w+");
16 if(!flock($fp,LOCK_EX | LOCK_NB)){
17 echo "系统繁忙,请稍后再试";
18 return;
19 }
20 //下单
21 $sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
22 $rs = mysqli_query($conn,$sql);
23 $row = $rs->fetch_assoc();
24 if($row['number']>0){//库存是否大于0
25 //模拟下单操作
26 $order_sn=build_order_no();
27 $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
28 values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
29 $order_rs = mysqli_query($conn,$sql);
30 //库存减少
31 $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
32 $store_rs = mysqli_query($conn,$sql);
33 if($store_rs){
34 echo '库存减少成功';
35 insertLog('库存减少成功');
36 flock($fp,LOCK_UN);//释放锁
37 }else{
38 echo '库存减少失败';
39 insertLog('库存减少失败');
40 }
41 }else{
42 echo '库存不够';
43 insertLog('库存不够');
44 }
45 fclose($fp);
46 ?>
 

5. 乐观锁思路

这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。

有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

优化方案5:Redis中的watch

 
 1 <?php
2 $redis = new redis();
3 $result = $redis->connect('127.0.0.1', 6379);
4 echo $mywatchkey = $redis->get("mywatchkey");
5 /*
6 //插入抢购数据
7 if($mywatchkey>0)
8 {
9 $redis->watch("mywatchkey");
10 //启动一个新的事务。
11 $redis->multi();
12 $redis->set("mywatchkey",$mywatchkey-1);
13 $result = $redis->exec();
14 if($result) {
15 $redis->hSet("watchkeylist","user_".mt_rand(1,99999),time());
16 $watchkeylist = $redis->hGetAll("watchkeylist");
17 echo "抢购成功!<br/>";
18 $re = $mywatchkey - 1;
19 echo "剩余数量:".$re."<br/>";
20 echo "用户列表:<pre>";
21 print_r($watchkeylist);
22 }else{
23 echo "手气不好,再抢购!";exit;
24 }
25 }else{
26 // $redis->hSet("watchkeylist","user_".mt_rand(1,99999),"12");
27 // $watchkeylist = $redis->hGetAll("watchkeylist");
28 echo "fail!<br/>";
29 echo ".no result<br/>";
30 echo "用户列表:<pre>";
31 // var_dump($watchkeylist);
32 }*/
33 $rob_total = 100; //抢购数量
34 if($mywatchkey<=$rob_total){
35 $redis->watch("mywatchkey");
36 $redis->multi(); //在当前连接上启动一个新的事务。
37 //插入抢购数据
38 $redis->set("mywatchkey",$mywatchkey+1);
39 $rob_result = $redis->exec();
40 if($rob_result){
41 $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);
42 $mywatchlist = $redis->hGetAll("watchkeylist");
43 echo "抢购成功!<br/>";
44
45 echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>";
46 echo "用户列表:<pre>";
47 var_dump($mywatchlist);
48 }else{
49 $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao');
50 echo "手气不好,再抢购!";exit;
51 }
52 }
53 ?>
 

php解决高并发问题的更多相关文章

  1. asp.net解决高并发的方案.

    asp.net解决高并发的方案. Posted on 2012-11-27 22:31 75077027 阅读(3964) 评论(1) 编辑 收藏 最近几天一直在读代震军的博客,他是 Discuz!N ...

  2. Nginx和Tengine解决高并发和高可用,而非推荐Apache

    什么是Nginx  什么是Tengine 看看国内大公司在用Nginx和Tengine吗? 步骤一:进入 https://www.taobao.com/,按F12.可看到 有很多APP对淘宝进行请求. ...

  3. 每一个程序员都应该知道的高并发处理技巧、创业公司如何解决高并发问题、互联网高并发问题解决思路、caoz大神多年经验总结分享

    本文来源于caoz梦呓公众号高并发专辑,以图形化.松耦合的方式,对互联网高并发问题做了详细解读与分析,"技术在短期内被高估,而在长期中又被低估",而不同的场景和人员成本又导致了巨头 ...

  4. 用CAS方案解决高并发一致性问题

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt395 缘起:在高并发的分布式环境下,对于数据的查询与修改容易引发一致性问题, ...

  5. 使用数据库乐观锁解决高并发秒杀问题,以及如何模拟高并发的场景,CyclicBarrier和CountDownLatch类的用法

    数据库:mysql 数据库的乐观锁:一般通过数据表加version来实现,相对于悲观锁的话,更能省数据库性能,废话不多说,直接看代码 第一步: 建立数据库表: CREATE TABLE `skill_ ...

  6. 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存

    原文:http://blog.csdn.net/heyewu4107/article/details/71009712 高并发场景系列(一) 利用redis实现分布式事务锁,解决高并发环境下减库存 问 ...

  7. 转发:php解决高并发

    php解决高并发(转发:https://www.cnblogs.com/walblog/articles/8476579.html) 我们通常衡量一个Web系统的吞吐率的指标是QPS(Query Pe ...

  8. asp.net怎样解决高并发问题

    队列+多线程+couchbase缓存 ,解决高并发问题. using System; using System.Collections.Generic; using System.Linq; usin ...

  9. PHP利用Mysql锁解决高并发

    前面写过利用文件锁来处理高并发的问题的,现在我们说另外一个处理方式,利用Mysql的锁来解决高并发的问题 先看没有利用事务的时候并发的后果 创建库存管理表 CREATE TABLE `storage` ...

  10. php面试题二--解决网站大流量高并发方案(从url到硬盘来解决高并发方案总结)

    php面试题二--解决网站大流量高并发方案(从url到硬盘来解决高并发方案总结) 一.总结 从外到内解决网站大流量高并发问题---从提交一个url开始(从用户按下搜索栏回车键开始) url最开始会到d ...

随机推荐

  1. re正则匹配城市名

    匹配城市名称,只要第一次出现之后的数据 import re a='巴州区白云县台公交乡公司对面区海蜃楼6楼' b=re.search(r'(^\w+?区)|(^\w+?县)|(^\w+?镇)',a). ...

  2. mysql 生成时间序列数据 - 存储过程

    由于时间自动转换为int值, 做一步转化,也可在调用时处理 use `test`; CREATE table test.test1 as SELECT state, id, `规格条码`, `色号条码 ...

  3. SoapUI工具做get请求和post请求接口测试

    转载自:https://www.cnblogs.com/hong-fithing/p/7617366.html 此篇主要介绍SoapUI工具做常用的两种请求接口测试,分别是get请求和post请求. ...

  4. 网站设置http到https

    首先ssl证书配置好,以保证可以正常访问https,若不会请看上一个文章 然后就是http模式访问怎么自动到https呢,很简单 首先在ssl网站根目录创建文件.htaccess,很多用文本文档那样创 ...

  5. hiero.ui获取实例名的方法

    在hiero.ui中经常会通过hiero.ui.windowManager().windows()来获取当前QMainWindow中的QWidget子窗口,而这些子窗口是以实例对象的方式返回的,如果想 ...

  6. 关于Asset Library核心功能的一些计划

    Asset Library是公司计划中的一个网站,用于存放图像,视频,三维文件等资产,在之前的一个多月中我写完了该网站的后台及部分前端,现在就剩最后一部分了,这也是最棘手最核心的部分,就是在网页上快速 ...

  7. 语音识别传统方法(GMM+HMM+NGRAM)概述

    春节后到现在近两个月了,没有更新博客,主要是因为工作的关注点正从传统语音(语音通信)转向智能语音(语音识别).部门起了个新项目,要用到语音识别(准备基于Kaldi来做).我们之前做的传统音频已基本成熟 ...

  8. GitLab配置后收取不到邮件问题

    一.root权限下,修改gitlab的配置文件:/etc/gitlab/gitlab.rb gitlab_rails['smtp_enable'] = true gitlab_rails['smtp_ ...

  9. submit提交判断

    body部分 <form action='https://www.baidu.com' method='post' >                   用 户 名:<input ...

  10. LeetCode 797. All Paths From Source to Target

    题目链接:https://leetcode.com/problems/all-paths-from-source-to-target/description/ Given a directed, ac ...