Heim >Backend-Entwicklung >PHP-Tutorial >Wie kann das Problem der hohen Parallelität (Produkt-Flash-Verkäufe) in PHP gelöst werden? Zwei gemeinsame Lösungen
Wie kann das Problem der hohen Parallelität (Produkt-Flash-Verkäufe) in PHP gelöst werden? Im folgenden Artikel werden zwei Lösungen vorgestellt (basierend auf MySQL oder basierend auf Redis). Ich hoffe, dass er Ihnen hilfreich sein wird.
Das zweite Töten führt zu einer sofortigen hohen Parallelität. Die Verwendung der Datenbank erhöht den Zugriffsdruck auf die Datenbank und verringert die Zugriffsgeschwindigkeit. Daher sollten wir Caching verwenden, um den Zugriffsdruck auf die Datenbank zu verringern
Sie können sehen Der Vorgang hier unterscheidet sich von der ursprünglichen Bestellung: Die generierte Flash-Sale-Vorbestellung wird nicht sofort in die Datenbank geschrieben, sondern zuerst in den Cache. Wenn der Benutzer erfolgreich bezahlt, wird der Status geändert und geschrieben die Datenbank.
Angenommen, num ist ein in der Datenbank gespeichertes Feld, in dem die verbleibende Menge des Flash-getöteten Produkts gespeichert ist.
if($num > 0){ //用户抢购成功,记录用户信息 $num--; }
Angenommen, in einem Szenario mit hoher Parallelität ist der Wert von num in der Datenbank 1, und mehrere Prozesse lesen möglicherweise gleichzeitig, dass num 1 ist. Das Programm stellt fest, dass die Bedingungen erfüllt sind erfolgreich, und num wird um eins reduziert.
Dies führt zu einer Überlieferung an Produkten. Ursprünglich konnten nur 10 Produkte geschnappt werden, aber zu diesem Zeitpunkt wird die Anzahl nach Abschluss des Ansturms negativ sein.
Es gibt viele Lösungen für dieses Problem, die einfach in Lösungen basierend auf MySQL und Redis unterteilt werden können. Die Leistung von Redis ist auf MySQL zurückzuführen, sodass eine höhere Parallelität möglich ist Single MySQL und Redis erfordern eine höhere Parallelität verteilter Lösungen, die in diesem Artikel nicht behandelt werden.
Diese Lösung ist die einfachste und am leichtesten zu verstehende Lösung. Sie können diese Lösung direkt verwenden, wenn die Leistungsanforderungen nicht hoch sind. Es ist zu beachten, dass SELECT … FOR UPDATE
so viele Indizes wie möglich verwenden sollte, um so wenige Zeilen wie möglich zu sperren.
Exklusive Sperren werden erst freigegeben, nachdem die Transaktionsausführung abgeschlossen ist, nicht nachdem der Lesevorgang abgeschlossen ist Es wird später freigegeben, daher sollte die verwendete Transaktion so früh wie möglich festgeschrieben oder zurückgesetzt werden, um die exklusive Sperre früher aufzuheben.
CREATE TABLE `goods` ( `id` int(11) NOT NULL, `num` int(11) DEFAULT NULL, `version` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
②Optimistische Sperre
Die optimistische Sperrlösung fügt beim Lesen von Daten keine exklusive Sperre hinzu, sondern löst sie durch ein Versionsfeld, das bei jeder Aktualisierung automatisch erhöht wird Dann kann das Problem erfolgreich aktualisiert werden. Wenn jeder Prozess num liest, liest er auch den Wert von version. Beim Aktualisieren von num aktualisiert er auch die Version und fügt beim Aktualisieren eine Äquivalenzbeurteilung zur Version hinzu.
Angenommen, 10 Prozesse haben gelesen, dass der Wert von num 1 und der Wert von version 9 ist. Dann lauten die von diesen 10 Prozessen ausgeführten Aktualisierungsanweisungen alle UPDATE waren SET num=num-1, version=version+ 1 WHERE version=9
, SELECT … FOR UPDATE
要尽可能的使用索引,以便锁定尽可能少的行数;
排他锁是在事务执行结束之后才释放的,不是读取完成之后就释放,因此使用的事务应该尽可能的早些提交或回滚,以便早些释放排它锁。
CREATE TABLE `log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `good_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
②乐观锁
乐观锁的方案在读取数据是并没有加排他锁,而是通过一个每次更新都会自增的version字段来解决,多个进程读取到相同num,然后都能更新成功的问题。在每个进程读取num的同时,也读取version的值,并且在更新num的同时也更新version,并在更新时加上对version的等值判断。
假设有10个进程都读取到了num的值为1,version值为9,则这10个进程执行的更新语句都是UPDATE goods SET num=num-1,version=version+1 WHERE version=9
$this->mysqli->begin_transaction(); $result = $this->mysqli->query("SELECT num FROM goods WHERE id=1 LIMIT 1 FOR UPDATE"); $row = $result->fetch_assoc(); $num = intval($row['num']); if($num > 0){ usleep(100); $this->mysqli->query("UPDATE goods SET num=num-1"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->commit(); echo "success:".$num; }else{ $this->mysqli->rollback(); echo "fail1:".$num; } }else{ $this->mysqli->rollback(); echo "fail2:".$num; } }else{ $this->mysqli->commit(); echo "fail3:".$num; }
③wo Bedingung (atomare Operation)
Das pessimistische Sperrschema stellt sicher, dass der Wert von num in der Datenbank nur von einem Prozess gleichzeitig gelesen und verarbeitet werden kann, d. h. gleichzeitige Leseprozesse müssen dies tun Warteschlange hier oben. Nacheinander ausführen. Optimistisches Sperrschema Obwohl der Wert von num von mehreren Prozessen gleichzeitig gelesen werden kann, kann durch die Äquivalenzbeurteilung der Version im Aktualisierungsvorgang sichergestellt werden, dass nur ein Update gleichzeitiger Aktualisierungsvorgänge gleichzeitig erfolgreich sein kann. Es gibt auch eine einfachere Lösung, die darin besteht, während des Aktualisierungsvorgangs nur die bedingte Einschränkung von num>0 hinzuzufügen. Obwohl die durch die Where-Bedingung eingeschränkte Lösung der optimistischen Sperrlösung ähnlich zu sein scheint und das Auftreten von Überausgabeproblemen verhindern kann, ist die Leistung bei großer Anzahl immer noch sehr unterschiedlich. Angenommen, num ist zu diesem Zeitpunkt 10 und 5 Prozesse lesen gleichzeitig num=10. Aufgrund der Gleichheitsbeurteilung des Versionsfelds wird nur einer dieser 5 Prozesse erfolgreich aktualisiert Diese 5 Prozesse werden nach Abschluss ausgeführt. Für die Where-Bedingungslösung beträgt die Anzahl nach Abschluss der Ausführung dieser 5 Prozesse 5, solange Anzahl> 0 erfolgreich aktualisiert werden kann.$result = $this->mysqli->query("SELECT num,version FROM goods WHERE id=1 LIMIT 1"); $row = $result->fetch_assoc(); $num = intval($row['num']); $version = intval($row['version']); if($num > 0){ usleep(100); $this->mysqli->begin_transaction(); $this->mysqli->query("UPDATE goods SET num=num-1,version=version+1 WHERE version={$version}"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->commit(); echo "success:".$num; }else{ $this->mysqli->rollback(); echo "fail1:".$num; } }else{ $this->mysqli->rollback(); echo "fail2:".$num; } }else{ echo "fail3:".$num; }
① Watch-basiertes optimistisches Sperrschema
watch wird verwendet, um einen (oder mehrere) Schlüssel zu überwachen. Wenn dieser (oder diese) Schlüssel vor der Transaktion vorhanden sind ausgeführt. Bei Änderung durch andere Befehle wird die Transaktion unterbrochen. Diese Lösung ähnelt der optimistischen Sperrlösung in MySQL und die spezifische Leistung ist dieselbe.$result = $this->mysqli->query("SELECT num FROM goods WHERE id=1 LIMIT 1"); $row = $result->fetch_assoc(); $num = intval($row['num']); if($num > 0){ usleep(100); $this->mysqli->begin_transaction(); $this->mysqli->query("UPDATE goods SET num=num-1 WHERE num>0"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->query("INSERT INTO log(good_id) VALUES({$num})"); $affected_rows = $this->mysqli->affected_rows; if($affected_rows == 1){ $this->mysqli->commit(); echo "success:".$num; }else{ $this->mysqli->rollback(); echo "fail1:".$num; } }else{ $this->mysqli->rollback(); echo "fail2:".$num; } }else{ echo "fail3:".$num; }
②Listenbasierte Warteschlangenlösung
基于队列的方案利用了redis出队操作的原子性,抢购开始之前首先将商品编号放入响应的队列中,在抢购时依次从队列中弹出操作,这样可以保证每个商品只能被一个进程获取并操作,不存在超发的情况。
该方案的优点是理解和实现起来都比较简单,缺点是当商品数量较多是,需要将大量的数据存入到队列中,并且不同的商品需要存入到不同的消息队列中。
public function init(){ $this->redis->del('goods'); for($i=1;$i<=10;$i++){ $this->redis->lPush('goods',$i); } $this->redis->del('result'); echo 'init done'; } public function run(){ $goods_id = $this->redis->rPop('goods'); usleep(100); if($goods_id == false) { echo "fail1"; }else{ $res = $this->redis->lPush('result',$goods_id); if($res == false){ echo "writelog:".$goods_id; }else{ echo "success".$goods_id; } } }
③基于decr返回值的方案
如果我们将剩余量num设置为一个键值类型,每次先get之后判断,然后再decr是不能解决超发问题的。
但是redis中的decr操作会返回执行后的结果,可以解决超发问题。我们首先get到num的值进行第一步判断,避免每次都去更新num的值,然后再对num执行decr操作,并判断decr的返回值,如果返回值不小于0,这说明decr之前是大于0的,用户抢购成功。
public function run(){ $num = $this->redis->get('num'); if($num > 0) { usleep(100); $retNum = $this->redis->decr('num'); if($retNum >= 0){ $res = $this->redis->lPush('result',$retNum); if($res == false){ echo "writeLog:".$retNum; }else{ echo "success:".$retNum; } }else{ echo "fail1"; } }else{ echo "fail2"; } }
④基于setnx的排它锁方案
redis没有像mysql中的排它锁,但是可以通过一些方式实现排它锁的功能,就类似php使用文件锁实现排它锁一样。
setnx实现了exists和set两个指令的功能,若给定的key已存在,则setnx不做任何动作,返回0;若key不存在,则执行类似set的操作,返回1。
我们设置一个超时时间timeout,每隔一定时间尝试setnx操作,如果设置成功就是获得了相应的锁,执行num的decr操作,操作完成删除相应的key,模拟释放锁的操作。
public function run(){ do { $res = $this->redis->setnx("numKey",1); $this->timeout -= 100; usleep(100); }while($res == 0 && $this->timeout>0); if($res == 0){ echo 'fail1'; }else{ $num = $this->redis->get('num'); if($num > 0) { $this->redis->decr('num'); usleep(100); $res = $this->redis->lPush('result',$num); if($res == false){ echo "fail2"; }else{ echo "success:".$num; } }else{ echo "fail3"; } $this->redis->del("numKey"); } }
推荐学习:《PHP视频教程》
Das obige ist der detaillierte Inhalt vonWie kann das Problem der hohen Parallelität (Produkt-Flash-Verkäufe) in PHP gelöst werden? Zwei gemeinsame Lösungen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!