この記事では、PHP が同時実行性の問題をどのように解決するかを紹介します。一定の参考値があるので、困っている友達が参考になれば幸いです。
たとえば、高速道路の交差点では、1 秒あたり 5 台の車が行き来し、高速道路の交差点は通常どおり動作します。突然、この交差点は1秒間に4台の車しか通過できなくなり、交通の流れは変わらないため、間違いなく渋滞が発生します。 (5レーンが4レーンになる感覚)
おすすめ動画コース→: 「数千万データの同時実行ソリューション(理論と実践編)」 "
同様に、ある秒間に、20*500 の利用可能な接続プロセスが全負荷で動作していますが、まだ 10,000 の新しいリクエストがあり、利用可能な接続プロセスはなく、システムはトラップされた状態異常も想定されます。
実際、同時実行性が高くない通常のビジネス シナリオでも、特定のビジネス リクエスト インターフェイスに問題があり、応答時間が非常に遅いという同様の状況が発生します。 . Web リクエスト全体 応答時間が非常に長く、Web サーバー上で使用可能な接続の数が徐々にいっぱいになり、他の通常のビジネス リクエストには接続プロセスが使用できなくなります。
さらに恐ろしい問題は、それがユーザーの行動特性にあるということです。システムが利用できなくなると、ユーザーがクリックする頻度も高くなります。この悪循環は最終的に「雪崩」(Web マシンの 1 つがハングアップする) につながります。正常に動作している他のマシンにトラフィックが分散し、正常なマシンもハングするという悪循環が発生し、Web システム全体がダウンしてしまいます。
再起動と過負荷保護
システムで「なだれ」が発生した場合、むやみにサービスを再起動しても問題は解決しません。最も多い現象は、起動してもすぐにハングアップしてしまうことです。現時点では、イングレス層でトラフィックを拒否してから再起動することをお勧めします。 redis/memcacheなどのサービスもダウンしている場合は、再起動時の「ウォーミングアップ」に注意が必要で、時間がかかる場合があります。
フラッシュセールやラッシュセールのシナリオでは、トラフィックがシステムの準備や想像を超えることがよくあります。このとき、過負荷保護が必要です。リクエストの拒否は、システムのフル負荷状態が検出された場合の保護手段でもあります。フロントエンドでフィルタリングを設定するのが最も簡単な方法ですが、このアプローチはユーザーから「批判」される動作です。顧客からの直接リクエストを迅速に返すために、CGI エントリ層で過負荷保護を設定する方が適切です。
高同時実行下でのデータ セキュリティ
複数のスレッドが同じファイルを書き込む場合 (複数のスレッド)同じコードを同時に実行します。各実行の結果が単一スレッドの実行の結果と同じであり、結果が期待どおりであれば、スレッドセーフです)。 MySQL データベースの場合は、独自のロック メカニズムを使用して問題を解決できますが、大規模な同時実行シナリオでは MySQL はお勧めできません。フラッシュセールやラッシュセールのシナリオには、「過剰納品」というもう 1 つの問題があり、この点を慎重に制御しないと過剰納品が発生します。また、一部の電子商取引会社においては、購入者が商品を購入したにもかかわらず、販売者が注文を有効なものとして認識せず、商品の配送を拒否するという駆け込み購入行為を行っているという話も聞いております。ここでの問題は、必ずしも販売業者が不正であるということではなく、システムの技術レベルでの過剰発行のリスクによって引き起こされる可能性があります。
過剰発行の理由
特定の急ぎ購入シナリオで、合計で 100 製品しかないとします。最後の瞬間には 99 製品が消費され、最後の製品だけが残ります。 1つ。このとき、システムは複数のリクエストを同時に送信し、そのリクエストで読み取った商品残高が99個すべてで残高判定を通過し、最終的に発行過剰に陥ってしまいました。 (記事の前半で説明したシーンと同じです)
上の図では、同時ユーザー B も「購入に成功」し、さらに 1 人が購入できるようになりました。製品。このシナリオは、同時実行性が高い状況で非常に発生しやすくなります。
最適化計画 1: 在庫フィールド番号フィールドを符号なしに設定します。在庫が 0 の場合、フィールドに負の数値を指定できないため、false が返されます。
<?php //优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false include('./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); } ?>
悲観的ロックのアイデア
スレッドセーフを解決するには多くのアイデアがあり、「悲観的ロック」の方向から議論を始めることができます。
悲観的ロック。つまり、データを変更するときに、外部リクエストからの変更を除外するためにロック状態が採用されます。ロック状態になった場合は、待機する必要があります。
虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。
优化方案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); } //模拟下单操作 //库存是否大于0 mysqli_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"); } ?>
FIFO队列思路
那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。
然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。
文件锁的思路
对于日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); ?>
乐观锁思路
这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(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 class="brush:php;toolbar:false">"; 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 class="brush:php;toolbar:false">"; // 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 class="brush:php;toolbar:false">"; var_dump($mywatchlist); }else{ $redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao'); echo "手气不好,再抢购!";exit; } } ?>
推荐学习:php视频教程
以上がPHP が同時実行性の問題をどのように解決するかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。