咱们通常掂量一个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零碎拖垮。

  1. 重启与过载爱护
    如果零碎产生“雪崩”,贸然重启服务,是无奈解决问题的。最常见的景象是,启动起来后,立即挂掉。这个时候,最好在入口层将流量回绝,而后再将重启。如果是redis/memcache这种服务也挂了,重启的时候须要留神“预热”,并且很可能须要比拟长的工夫。
    秒杀和抢购的场景,流量往往是超乎咱们零碎的筹备和设想的。这个时候,过载爱护是必要的。如果检测到零碎满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简略的形式,然而,这种做法是被用户“千夫所指”的行为。更适合一点的是,将过载爱护设置在CGI入口层,疾速将客户的间接申请返回
    高并发下的数据安全
    咱们晓得在多线程写入同一个文件的时候,会存现“线程平安”的问题(多个线程同时运行同一段代码,如果每次运行后果和单线程运行的后果是一样的,后果和预期雷同,就是线程平安的)。如果是MySQL数据库,能够应用它自带的锁机制很好的解决问题,然而,在大规模并发的场景中,是不举荐应用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面管制不慎,会产生发送过多的状况。咱们也已经据说过,某些电商搞抢购流动,买家胜利拍下后,商家却不抵赖订单无效,回绝发货。这里的问题,兴许并不一定是商家忠厚,而是零碎技术层面存在超发危险导致的。
  2. 超发的起因
    假如某个抢购场景中,咱们一共只有100个商品,在最初一刻,咱们曾经耗费了99个商品,仅剩最初一个。这个时候,零碎发来多个并发申请,这批申请读取到的商品余量都是99个,而后都通过了这一个余量判断,最终导致超发。(同文章后面说的场景)

在下面的这个图中,就导致了并发用户B也“抢购胜利”,多让一个人取得了商品。这种场景,在高并发的状况下非常容易呈现。
优化计划1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为正数,将会返回false

<?php//优化计划1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为正数,将会返回falseinclude('./mysql.php');$username = 'wang'.rand(0,1000);//生成惟一订单function build_order_no(){  return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);}//记录日志function insertLog($event,$type=0,$username){    global $conn;    $sql="insert into ih_log(event,type,usernma)    values('$event','$type','$username')";    return mysqli_query($conn,$sql);}function insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number){      global $conn;      $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price,username,number)      values('$order_sn','$user_id','$goods_id','$sku_id','$price','$username','$number')";     return  mysqli_query($conn,$sql);}//模仿下单操作//库存是否大于0$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' ";$rs=mysqli_query($conn,$sql);$row = $rs->fetch_assoc();  if($row['number']>0){//高并发下会导致超卖      if($row['number']<$number){        return insertLog('库存不够',3,$username);      }      $order_sn=build_order_no();      //库存缩小      $sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0";      $store_rs=mysqli_query($conn,$sql);      if($store_rs){          //生成订单          insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number);          insertLog('库存缩小胜利',1,$username);      }else{          insertLog('库存缩小失败',2,$username);      }  }else{      insertLog('库存不够',3,$username);  }?>
  1. 乐观锁思路
    解决线程平安的思路很多,能够从“乐观锁”的方向开始探讨。
    乐观锁,也就是在批改数据的时候,采纳锁定状态,排挤内部申请的批改。遇到加锁的状态,就必须期待。

尽管上述的计划确实解决了线程平安的问题,然而,别忘记,咱们的场景是“高并发”。也就是说,会很多这样的批改申请,每个申请都须要期待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种申请就会死在那里。同时,这种申请会很多,霎时增大零碎的均匀响应工夫,后果是可用连接数被耗尽,零碎陷入异样。
优化计划2:应用MySQL的事务,锁住操作的行

<?php//优化计划2:应用MySQL的事务,锁住操作的行include('./mysql.php');//生成惟一订单号function build_order_no(){  return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);}//记录日志function insertLog($event,$type=0){    global $conn;    $sql="insert into ih_log(event,type)    values('$event','$type')";    mysqli_query($conn,$sql);}//模仿下单操作//库存是否大于0mysqli_query($conn,"BEGIN");  //开始事务$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此时这条记录被锁住,其它事务必须期待此次事务提交后能力执行$rs=mysqli_query($conn,$sql);$row=$rs->fetch_assoc();if($row['number']>0){    //生成订单    $order_sn=build_order_no();    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";    $order_rs=mysqli_query($conn,$sql);    //库存缩小    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";    $store_rs=mysqli_query($conn,$sql);    if($store_rs){      echo '库存缩小胜利';        insertLog('库存缩小胜利');        mysqli_query($conn,"COMMIT");//事务提交即解锁    }else{      echo '库存缩小失败';        insertLog('库存缩小失败');    }}else{  echo '库存不够';    insertLog('库存不够');    mysqli_query($conn,"ROLLBACK");}?>
  1. FIFO队列思路
    那好,那么咱们略微批改一下下面的场景,咱们间接将申请放入队列中的,采纳FIFO(First Input First Output,先进先出),这样的话,咱们就不会导致某些申请永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。

而后,咱们当初解决了锁的问题,全副申请采纳“先进先出”的队列形式来解决。那么新的问题来了,高并发的场景下,因为申请很多,很可能一瞬间将队列内存“撑爆”,而后零碎又陷入到了异样状态。或者设计一个极大的内存队列,也是一种计划,然而,零碎解决完一个队列内申请的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的申请会越积攒越多,最终Web零碎均匀响应时候还是会大幅降落,零碎还是陷入异样。

  1. 文件锁的思路
    对于日IP不高或者说并发数不是很大的利用,个别不必思考这些!用个别的文件操作方法齐全没有问题。但如果并发高,在咱们对文件进行读写操作时,很有可能多个过程对进一文件进行操作,如果这时不对文件的拜访进行相应的独占,就容易造成数据失落
    优化计划4:应用非阻塞的文件排他锁
<?php//优化计划4:应用非阻塞的文件排他锁include ('./mysql.php');//生成惟一订单号function build_order_no(){  return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);}//记录日志function insertLog($event,$type=0){    global $conn;    $sql="insert into ih_log(event,type)    values('$event','$type')";    mysqli_query($conn,$sql);}$fp = fopen("lock.txt", "w+");if(!flock($fp,LOCK_EX | LOCK_NB)){    echo "零碎忙碌,请稍后再试";    return;}//下单$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";$rs =  mysqli_query($conn,$sql);$row = $rs->fetch_assoc();if($row['number']>0){//库存是否大于0    //模仿下单操作    $order_sn=build_order_no();    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";    $order_rs =  mysqli_query($conn,$sql);    //库存缩小    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";    $store_rs =  mysqli_query($conn,$sql);    if($store_rs){      echo '库存缩小胜利';        insertLog('库存缩小胜利');        flock($fp,LOCK_UN);//开释锁    }else{      echo '库存缩小失败';        insertLog('库存缩小失败');    }}else{  echo '库存不够';    insertLog('库存不够');}fclose($fp); ?>
<?php//优化计划4:应用非阻塞的文件排他锁include ('./mysql.php');//生成惟一订单号function build_order_no(){  return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);}//记录日志function insertLog($event,$type=0){    global $conn;    $sql="insert into ih_log(event,type)    values('$event','$type')";    mysqli_query($conn,$sql);}$fp = fopen("lock.txt", "w+");if(!flock($fp,LOCK_EX | LOCK_NB)){    echo "零碎忙碌,请稍后再试";    return;}//下单$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";$rs =  mysqli_query($conn,$sql);$row = $rs->fetch_assoc();if($row['number']>0){//库存是否大于0    //模仿下单操作    $order_sn=build_order_no();    $sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)    values('$order_sn','$user_id','$goods_id','$sku_id','$price')";    $order_rs =  mysqli_query($conn,$sql);    //库存缩小    $sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";    $store_rs =  mysqli_query($conn,$sql);    if($store_rs){      echo '库存缩小胜利';        insertLog('库存缩小胜利');        flock($fp,LOCK_UN);//开释锁    }else{      echo '库存缩小失败';        insertLog('库存缩小失败');    }}else{  echo '库存不够';    insertLog('库存不够');}fclose($fp); ?>
  1. 乐观锁思路
    这个时候,咱们就能够讨论一下“乐观锁”的思路了。乐观锁,是绝对于“乐观锁”采纳更为宽松的加锁机制,大都是采纳带版本号(Version)更新。实现就是,这个数据所有申请都有资格去批改,但会取得一个该数据的版本号,只有版本号合乎的能力更新胜利,其余的返回抢购失败。这样的话,咱们就不须要思考队列的问题,不过,它会增大CPU的计算开销。然而,综合来说,这是一个比拟好的解决方案。

有很多软件和服务都“乐观锁”性能的反对,例如Redis中的watch就是其中之一。通过这个实现,咱们保障了数据的平安。
优化计划5:Redis中的watch

<?php$redis = new redis(); $result = $redis->connect('127.0.0.1', 6379); echo $mywatchkey = $redis->get("mywatchkey");/*  //插入抢购数据 if($mywatchkey>0) {     $redis->watch("mywatchkey");  //启动一个新的事务。    $redis->multi();   $redis->set("mywatchkey",$mywatchkey-1);   $result = $redis->exec();   if($result) {      $redis->hSet("watchkeylist","user_".mt_rand(1,99999),time());      $watchkeylist = $redis->hGetAll("watchkeylist");        echo "抢购胜利!<br/>";        $re = $mywatchkey - 1;        echo "残余数量:".$re."<br/>";        echo "用户列表:<pre>";        print_r($watchkeylist);   }else{      echo "手气不好,再抢购!";exit;   } }else{     // $redis->hSet("watchkeylist","user_".mt_rand(1,99999),"12");     //  $watchkeylist = $redis->hGetAll("watchkeylist");        echo "fail!<br/>";        echo ".no result<br/>";        echo "用户列表:<pre>";      //  var_dump($watchkeylist); }*/$rob_total = 100;   //抢购数量if($mywatchkey<=$rob_total){    $redis->watch("mywatchkey");    $redis->multi(); //在以后连贯上启动一个新的事务。    //插入抢购数据    $redis->set("mywatchkey",$mywatchkey+1);    $rob_result = $redis->exec();    if($rob_result){         $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);        $mywatchlist = $redis->hGetAll("watchkeylist");        echo "抢购胜利!<br/>";        echo "残余数量:".($rob_total-$mywatchkey-1)."<br/>";        echo "用户列表:<pre>";        var_dump($mywatchlist);    }else{          $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao');        echo "手气不好,再抢购!";exit;    }}?>