Maison > Article > base de données > Analyser les idées de vente flash et la sécurité des données dans un contexte de concurrence élevée
L'indicateur que nous mesurons habituellement le débit d'un système Web est le QPS (Query Per Second, nombre de requêtes traitées par seconde). Cet indicateur est très critique pour résoudre des scénarios de forte concurrence de plusieurs dizaines de milliers de fois. par seconde. Par exemple, nous supposons que le temps de réponse moyen pour traiter une demande commerciale est de 100 ms. Dans le même temps, il y a 20 serveurs Web Apache dans le système et MaxClients est configuré sur 500 (indiquant le nombre maximum de connexions Apache).
Ensuite, le QPS de pointe théorique de notre système Web est (méthode de calcul idéalisée) :
20*500/0,1 = 100000 (100 000 QPS)
Hein ? Notre système semble très puissant. Il peut traiter 100 000 demandes en une seconde. La vente flash de 5 W/S semble être un « tigre de papier ». La situation actuelle n’est bien sûr pas si idéale. Dans les scénarios réels à forte concurrence, les machines sont soumises à une charge élevée et le temps de réponse moyen sera alors considérablement augmenté.
En ce qui concerne le serveur Web, plus Apache ouvre de processus de connexion, plus le CPU doit gérer de changements de contexte, ce qui augmente la consommation du CPU et entraîne directement une augmentation du temps de réponse moyen. Par conséquent, le nombre de MaxClients mentionné ci-dessus doit être pris en compte en fonction de facteurs matériels tels que le processeur et la mémoire. Plus n'est certainement pas meilleur. Vous pouvez le tester via le propre abench d'Apache et obtenir une valeur appropriée. Ensuite, nous choisissons Redis pour le stockage au niveau du fonctionnement de la mémoire. Dans un état de forte concurrence, le temps de réponse du stockage est crucial. Bien que la bande passante du réseau soit également un facteur, ces paquets de requêtes sont généralement relativement petits et deviennent rarement un goulot d'étranglement pour les requêtes. Il est rare que l’équilibrage de charge devienne un goulot d’étranglement du système, nous n’en discuterons donc pas ici.
Alors la question est, en supposant que notre système, dans un état de concurrence élevée de 5w/s, le temps de réponse moyen passe de 100ms à 250ms (situation réelle, encore plus) :
20 *500/0,25 = 40 000 (40 000 QPS)
Notre système se retrouve donc avec 40 000 QPS Face à 50 000 requêtes par seconde, il y a une différence de 10 000.
Par exemple, à une intersection d'autoroute, 5 voitures viennent et dépassent 5 voitures par seconde, et l'intersection d'autoroute fonctionne normalement. Du coup, seules 4 voitures peuvent traverser cette intersection en une seconde, et le volume de trafic est toujours le même, il y aura donc certainement un embouteillage. (On a l'impression que 5 voies se sont soudainement transformées en 4 voies)
De même, en une certaine seconde, 20*500 processus de connexion disponibles fonctionnent à pleine capacité, mais il y a encore 10 000 nouvelles demandes, il n'y a pas de connexion. processus disponible, et il est prévu que le système tombe dans un état anormal.
En fait, dans des scénarios commerciaux normaux sans forte concurrence, des situations similaires se produisent. Il y a un problème avec une certaine interface de demande commerciale et le temps de réponse est extrêmement lent. .L'ensemble de la requête Web Le temps de réponse est très long, remplissant progressivement le nombre de connexions disponibles sur le serveur Web, et aucun processus de connexion n'est disponible pour les autres requêtes commerciales normales.
Le problème le plus terrifiant concerne les caractéristiques comportementales des utilisateurs. Plus le système est indisponible, plus les utilisateurs cliquent fréquemment, le cercle vicieux conduit finalement à une « avalanche » (l'une des machines Web raccroche, provoquant un blocage). le trafic à disperser Sur d'autres machines qui fonctionnent normalement, les machines normales se bloqueront également, et alors un cercle vicieux se produira), faisant tomber l'ensemble du système Web.
3. Redémarrage et protection contre les surcharges
Si une "avalanche" se produit dans le système, un redémarrage imprudent du service ne résoudra pas le problème. Le phénomène le plus courant est qu'après le démarrage, il raccroche immédiatement. À ce stade, il est préférable de refuser le trafic au niveau de la couche d’entrée, puis de redémarrer. Si des services comme redis/memcache sont également en panne, vous devez faire attention au « préchauffage » lors du redémarrage, et cela peut prendre beaucoup de temps.
Dans les scénarios de vente flash et de vente urgente, le trafic dépasse souvent la préparation et l'imagination de notre système. À ce stade, une protection contre les surcharges est nécessaire. Le refus des demandes constitue également une mesure de protection si une condition de charge complète du système est détectée. Mettre en place un filtrage sur le front-end est le moyen le plus simple, mais cette approche est un comportement « critiqué » par les utilisateurs. Ce qui est plus approprié est de définir une protection contre les surcharges au niveau de la couche d'entrée CGI pour renvoyer rapidement les demandes directes des clients
Sécurité des données sous haute concurrence
Nous savons que lorsque plusieurs threads écrivent dans le même fichier ( Plusieurs threads exécutent le même morceau de code en même temps. Si le résultat de chaque exécution est le même que celui d'une exécution à un seul thread et que le résultat est le même que celui attendu, il est thread-safe). S'il s'agit d'une base de données MySQL, vous pouvez utiliser son propre mécanisme de verrouillage pour résoudre le problème. Cependant, dans les scénarios de concurrence à grande échelle, MySQL n'est pas recommandé. Dans les scénarios de vente flash et de vente urgente, il existe un autre problème, celui du « envoi excessif ». Si cet aspect n'est pas soigneusement contrôlé, un envoi excessif se produira. Nous avons également entendu dire que certaines sociétés de commerce électronique mènent des activités d'achat précipitées. Une fois que l'acheteur a réussi à acheter le produit, le commerçant ne reconnaît pas la commande comme valide et refuse de livrer la marchandise. Le problème ici n’est peut-être pas nécessairement que le commerçant est traître, mais qu’il est causé par le risque de surémission au niveau technique du système.
1. Raisons de la livraison excessive
Supposons que dans un certain scénario d'achat précipité, nous n'ayons que 100 produits au total, au dernier moment, nous avons consommé 99 produits et il ne reste que le dernier. À ce moment-là, le système a envoyé plusieurs demandes simultanées. Les soldes de produits lus par ces demandes étaient tous de 99, puis ils ont tous rendu ce jugement de solde, ce qui a finalement conduit à une émission excessive. (Identique à la scène mentionnée plus tôt dans l'article)
Dans l'image ci-dessus, l'utilisateur simultané B a également "acheté avec succès", permettant à une personne supplémentaire d'obtenir le produit. Ce scénario est très facile à réaliser dans des situations de forte concurrence.
Plan d'optimisation 1 : définissez le champ du numéro de champ d'inventaire sur non signé Lorsque l'inventaire est 0, car le champ ne peut pas être un nombre négatif, false sera renvoyé
<?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. idée de verrouillage
Il existe de nombreuses idées pour résoudre la sécurité des threads, et nous pouvons commencer la discussion dans la direction du "verrouillage pessimiste".
Verrouillage pessimiste, c'est-à-dire que lors de la modification des données, l'état de verrouillage est adopté pour exclure les modifications des requêtes externes. Lorsque vous rencontrez un état verrouillé, vous devez attendre.
Bien que la solution ci-dessus résolve le problème de la sécurité des threads, n'oubliez pas que notre scénario est "à haute concurrence". En d'autres termes, il y aura de nombreuses requêtes de modification de ce type, et chaque requête devra attendre un "verrou". Certains threads n'auront peut-être jamais la chance de récupérer ce "verrou", et de telles requêtes mourront là. Dans le même temps, de nombreuses demandes de ce type apparaîtront, ce qui augmentera instantanément le temps de réponse moyen du système. En conséquence, le nombre de connexions disponibles sera épuisé et le système tombera dans une exception.
Plan d'optimisation 2 : Utiliser les transactions MySQL pour verrouiller les lignes d'opérations
<?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"); } ?>
3 Idée de file d'attente FIFO
D'accord, modifions légèrement ce qui précède Dans ce scénario. , nous mettons directement la requête dans la file d'attente et utilisons FIFO (First Input First Output, first in first out). Dans ce cas, nous ne ferons pas en sorte que certaines requêtes n'obtiennent jamais le verrou. En voyant cela, avez-vous l'impression de transformer de force le multi-threading en mono-threading ?
Ensuite, nous avons maintenant résolu le problème de verrouillage et toutes les demandes sont traitées dans une file d'attente "premier entré, premier sorti". Ensuite, un nouveau problème survient. Dans un scénario à forte concurrence, parce qu'il y a de nombreuses requêtes, la mémoire de la file d'attente peut être « explosée » en un instant, et le système retombera alors dans un état anormal. Ou concevoir une énorme file d'attente de mémoire est également une solution. Cependant, la vitesse à laquelle le système traite les requêtes dans une file d'attente ne peut être comparée au nombre de requêtes qui affluent follement dans la file d'attente. En d'autres termes, le nombre de requêtes dans la file d'attente s'accumulera de plus en plus, et finalement le temps de réponse moyen du système Web diminuera encore considérablement et le système tombera toujours dans une exception.
4. L'idée du verrouillage de fichiers
Pour les applications où l'IP quotidienne n'est pas élevée ou le nombre de simultanéités n'est pas très grand, il n'est généralement pas nécessaire d'en tenir compte ! Il n’y a aucun problème avec les méthodes normales de manipulation de fichiers. Mais si la concurrence est élevée, lorsque nous lisons et écrivons des fichiers, il est très probable que plusieurs processus fonctionneront sur le fichier suivant. Si l'accès au fichier n'est pas exclusif à ce moment-là, cela entraînera facilement une perte de données
.Plan d'optimisation 4 : Utiliser des verrous exclusifs de fichiers non bloquants
<?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); ?>
5. Idée de verrouillage optimiste
À ce stade, nous pouvons discuter de l'idée du « verrouillage optimiste » . Le verrouillage optimiste adopte un mécanisme de verrouillage plus souple que le « verrouillage pessimiste », et la plupart d'entre eux utilisent des mises à jour de version. L'implémentation est que toutes les demandes concernant ces données peuvent être modifiées, mais un numéro de version des données sera obtenu. Seules celles avec un numéro de version cohérent pourront être mises à jour avec succès, et les autres demandes seront renvoyées en cas d'échec de capture. Dans ce cas, nous n’avons pas besoin de prendre en compte le problème de la file d’attente, mais cela augmentera la charge de calcul du processeur. Cependant, dans l’ensemble, c’est une meilleure solution.
Il existe de nombreux logiciels et services qui prennent en charge la fonction de « verrouillage optimiste », comme watch dans Redis en fait partie. Avec cette implémentation, nous garantissons la sécurité des données.
Solution d'optimisation 5 : regarder dans Redis
<?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; } } ?>
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!