Der Indikator, den wir normalerweise für die Durchsatzrate eines Websystems messen, ist QPS (Query Per Second, Anzahl der pro Sekunde verarbeiteten Anfragen). Dieser Indikator ist sehr wichtig, um Szenarios mit hoher Parallelität zu lösen, die mehrere Zehntausend Mal vorkommen pro Sekunde. Wir gehen beispielsweise davon aus, dass die durchschnittliche Antwortzeit für die Verarbeitung einer Geschäftsanfrage 100 ms beträgt. Gleichzeitig gibt es 20 Apache-Webserver im System und MaxClients ist auf 500 konfiguriert (was die maximale Anzahl von Apache-Verbindungen angibt).
Dann beträgt der theoretische Spitzen-QPS unseres Websystems (idealisierte Berechnungsmethode):
20*500/0,1 = 100.000 (100.000 QPS)
Hä? Unser System scheint sehr leistungsfähig zu sein. Es kann 100.000 Anfragen in einer Sekunde verarbeiten. Der Flash-Sale von 5w/s scheint ein „Papiertiger“ zu sein. Die tatsächliche Situation ist natürlich nicht so ideal. In tatsächlichen Szenarien mit hoher Parallelität sind Maschinen stark ausgelastet und die durchschnittliche Antwortzeit wird zu diesem Zeitpunkt erheblich verlängert.
Was den Webserver betrifft, gilt: Je mehr Verbindungsprozesse Apache öffnet, desto mehr Kontextwechsel muss die CPU bewältigen, was den CPU-Verbrauch erhöht und direkt zu einer Erhöhung der durchschnittlichen Antwortzeit führt. Daher muss die oben genannte Anzahl an MaxClients anhand von Hardwarefaktoren wie CPU und Speicher berücksichtigt werden. Mehr ist definitiv nicht besser. Sie können es über Apaches eigene Benchmark testen und einen geeigneten Wert erhalten. Dann wählen wir Redis für die Speicherung auf Speicherbetriebsebene. In einem Zustand mit hoher Parallelität ist die Speicherreaktionszeit entscheidend. Obwohl auch die Netzwerkbandbreite ein Faktor ist, sind solche Anforderungspakete im Allgemeinen relativ klein und stellen selten einen Engpass für Anforderungen dar. Es kommt selten vor, dass der Lastausgleich zu einem Systemengpass wird, daher werden wir hier nicht darauf eingehen.
Dann lautet die Frage: Unter der Annahme, dass sich in unserem System in einem Zustand hoher Parallelität von 5 W/s die durchschnittliche Antwortzeit von 100 ms auf 250 ms ändert (in der tatsächlichen Situation sogar noch mehr):
20 *500/0,25 = 40.000 (40.000 QPS)
Unser System bleibt also bei 40.000 QPS. Bei 50.000 Anfragen pro Sekunde ergibt sich ein Unterschied von 10.000.
An einer Autobahnkreuzung kommen beispielsweise 5 Autos pro Sekunde vorbei und überholen 5 Autos, und die Autobahnkreuzung funktioniert normal. Plötzlich können nur noch 4 Autos diese Kreuzung in einer Sekunde passieren, und das Verkehrsaufkommen ist immer noch das gleiche. Daher wird es definitiv einen Stau geben. (Es fühlt sich an, als wären aus 5 Lanes plötzlich 4 Lanes geworden)
In ähnlicher Weise sind in einer bestimmten Sekunde 20*500 verfügbare Verbindungsprozesse voll ausgelastet, aber es gibt immer noch 10.000 neue Anfragen, es gibt keine Verbindung Prozess verfügbar und es wird erwartet, dass das System in einen abnormalen Zustand gerät.
Tatsächlich treten in normalen Geschäftsszenarien ohne hohe Parallelität ähnliche Situationen auf. Es liegt ein Problem mit einer bestimmten Geschäftsanforderungsschnittstelle vor und die Antwortzeit ist extrem langsam Die gesamte Web-Anfrage hat eine sehr lange Antwortzeit, wodurch die Anzahl der verfügbaren Verbindungen auf dem Webserver allmählich aufgefüllt wird und für andere normale Geschäftsanfragen kein Verbindungsprozess verfügbar ist.
Je erschreckender das Problem ist, desto unzugänglicher ist das System, desto häufiger klicken die Benutzer. Der Teufelskreis führt schließlich zu einer „Lawine“ (einer der Web-Rechner hängt sich auf). Der Datenverkehr soll auf andere Maschinen verteilt werden, die normal funktionieren. Die normalen Maschinen bleiben ebenfalls hängen, und dann entsteht ein Teufelskreis, der das gesamte Websystem zum Absturz bringt.
3. Neustart- und Überlastschutz
Wenn im System eine „Lawine“ auftritt, wird ein vorschneller Neustart des Dienstes das Problem nicht lösen. Das häufigste Phänomen ist, dass es nach dem Start sofort aufhängt. Zu diesem Zeitpunkt ist es am besten, den Datenverkehr auf der Eingangsebene zu verweigern und dann neu zu starten. Wenn auch Dienste wie Redis/Memcache ausfallen, müssen Sie beim Neustart auf das „Aufwärmen“ achten, was lange dauern kann.
In Flash-Sale- und Rush-Sale-Szenarien übersteigt der Datenverkehr oft die Vorbereitung und Vorstellungskraft unseres Systems. Zu diesem Zeitpunkt ist ein Überlastschutz erforderlich. Das Ablehnen von Anfragen ist auch eine Schutzmaßnahme, wenn eine vollständige Systemauslastung festgestellt wird. Das Einrichten der Filterung im Frontend ist der einfachste Weg, aber dieser Ansatz wird von Benutzern „kritisiert“. Angemessener ist es, einen Überlastungsschutz auf der CGI-Einstiegsebene festzulegen, um direkte Anfragen von Kunden schnell zurückzugeben
Datensicherheit bei hoher Parallelität
Wir wissen, dass, wenn mehrere Threads in dieselbe Datei schreiben ( Mehrere Threads führen denselben Code gleichzeitig aus. Wenn das Ergebnis jedes Laufs mit dem eines Einzel-Thread-Laufs übereinstimmt und das Ergebnis das gleiche ist wie erwartet, ist es Thread-sicher. Wenn es sich um eine MySQL-Datenbank handelt, können Sie das Problem mit einem eigenen Sperrmechanismus lösen. In großen Parallelitätsszenarien wird MySQL jedoch nicht empfohlen. Bei Flash-Verkäufen und Eilverkäufen gibt es ein weiteres Problem: „Überversenden“. Wenn dieser Aspekt nicht sorgfältig kontrolliert wird, kommt es zu übermäßigem Versenden. Wir haben auch gehört, dass einige E-Commerce-Unternehmen Eilkäufe durchführen. Nachdem der Käufer das Produkt erfolgreich gekauft hat, erkennt der Händler die Bestellung nicht als gültig an und weigert sich, die Ware zu liefern. Das Problem liegt hier möglicherweise nicht unbedingt darin, dass der Händler betrügerisch vorgeht, sondern darin, dass die Gefahr einer Überausgabe auf der technischen Ebene des Systems besteht.
1. Gründe für die Überlieferung
Angenommen, wir haben in einem bestimmten Eilkaufszenario insgesamt nur 100 Produkte. Im letzten Moment haben wir 99 Produkte verbraucht und nur noch das letzte. Zu diesem Zeitpunkt sendete das System mehrere gleichzeitige Anfragen. Die von diesen Anfragen gelesenen Produktsalden betrugen alle 99, und dann verabschiedeten sie alle diese Saldenbeurteilung, was schließlich zu einer Überausgabe führte. (Dasselbe wie in der zuvor im Artikel erwähnten Szene)
Im Bild oben hat der gleichzeitige Benutzer B ebenfalls „erfolgreich gekauft“, sodass eine weitere Person das Produkt erhalten kann. Dieses Szenario kann in Situationen mit hoher Parallelität sehr leicht auftreten.
Optimierungsplan 1: Setzen Sie das Feld „Inventarnummer“ auf „ohne Vorzeichen“, da das Feld keine negative Zahl sein darf.
<?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 Sperridee
Es gibt viele Ideen zur Lösung der Thread-Sicherheit, und wir können die Diskussion aus der Richtung des „pessimistischen Sperrens“ beginnen.
Pessimistisches Sperren, d. h. beim Ändern von Daten wird der Sperrstatus übernommen, um Änderungen von externen Anfragen auszuschließen. Wenn Sie auf einen gesperrten Zustand stoßen, müssen Sie warten.
Obwohl die obige Lösung das Problem der Thread-Sicherheit löst, vergessen Sie nicht, dass unser Szenario „hohe Parallelität“ ist. Mit anderen Worten, es wird viele solcher Änderungsanfragen geben, und jede Anfrage muss auf eine „Sperre“ warten. Einige Threads haben möglicherweise nie die Chance, diese „Sperre“ zu ergreifen, und solche Anfragen werden dort sterben. Gleichzeitig wird es viele solcher Anfragen geben, was die durchschnittliche Antwortzeit des Systems sofort erhöht. Dadurch wird die Anzahl der verfügbaren Verbindungen erschöpft und das System fällt in eine Ausnahme.
Optimierungsplan 2: Verwenden Sie MySQL-Transaktionen, um die Operationszeilen zu sperren
<?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. Idee der FIFO-Warteschlange
Okay, dann ändern wir das Obige in diesem Szenario leicht , stellen wir die Anfrage direkt in die Warteschlange und verwenden FIFO (First Input First Output, First In First Out). In diesem Fall werden wir nicht dazu führen, dass einige Anfragen niemals die Sperre erhalten. Fühlt es sich angesichts dessen so an, als würde man Multi-Threading gewaltsam in Single-Threading umwandeln?
Dann haben wir nun das Sperrproblem gelöst und alle Anfragen werden in einer „First In, First Out“-Warteschlange verarbeitet. Dann tritt in einem Szenario mit hoher Parallelität ein neues Problem auf, da der Warteschlangenspeicher augenblicklich „explodiert“ wird und das System erneut in einen abnormalen Zustand gerät. Oder das Entwerfen einer riesigen Speicherwarteschlange ist ebenfalls eine Lösung. Die Geschwindigkeit, mit der das System Anforderungen in einer Warteschlange verarbeitet, kann jedoch nicht mit der Anzahl der verrückten Zuflüsse in die Warteschlange verglichen werden. Mit anderen Worten, die Anzahl der Anforderungen in der Warteschlange wird immer größer, und schließlich wird die durchschnittliche Antwortzeit des Websystems immer noch erheblich sinken und das System wird immer noch in eine Ausnahme geraten.
4. Die Idee der Dateisperre
Für Anwendungen, bei denen die tägliche IP nicht hoch ist oder die Anzahl der Parallelität nicht sehr groß ist, besteht im Allgemeinen keine Notwendigkeit, dies zu berücksichtigen! Mit normalen Dateimanipulationsmethoden ist das überhaupt kein Problem. Wenn jedoch die Parallelität beim Lesen und Schreiben von Dateien hoch ist, ist es sehr wahrscheinlich, dass mehrere Prozesse an der nächsten Datei arbeiten. Wenn wir zu diesem Zeitpunkt den Zugriff auf die Datei nicht monopolisieren, führt dies leicht zu Datenverlust
Optimierungsplan 4: Nicht blockierende dateiexklusive Sperren verwenden
<?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. Idee des optimistischen Sperrens
Zu diesem Zeitpunkt können wir die Idee des „optimistischen Sperrens“ diskutieren ". Beim optimistischen Sperren wird im Vergleich zum „pessimistischen Sperren“ ein entspannterer Sperrmechanismus verwendet, und die meisten von ihnen verwenden Versionsaktualisierungen. Die Implementierung besteht darin, dass alle Anforderungen für diese Daten geändert werden können, es wird jedoch eine Versionsnummer der Daten abgerufen. Nur diejenigen mit konsistenten Versionsnummern können erfolgreich aktualisiert werden, und andere Anforderungen werden als Fehler zurückgegeben. In diesem Fall müssen wir das Warteschlangenproblem nicht berücksichtigen, aber es erhöht den Rechenaufwand der CPU. Insgesamt ist dies jedoch eine bessere Lösung.
Es gibt viele Software und Dienste, die die Funktion „optimistisches Sperren“ unterstützen, wie z. B. Watch in Redis ist eine davon. Mit dieser Implementierung gewährleisten wir die Datensicherheit.
Optimierungslösung 5: In Redis ansehen
<?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; } } ?>
Das obige ist der detaillierte Inhalt vonAnalyse von Flash-Sale-Ideen und Datensicherheit bei hoher Parallelität. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!