PHP での高い同時実行性 (製品のフラッシュセール) の問題を解決するにはどうすればよいですか?次の記事では 2 つのソリューション (mysql ベースまたは Redis ベース) を紹介します。お役に立てば幸いです。
Instakill を使用すると、瞬時に高い同時実行性が得られます。データベースを使用すると、データベースへのアクセス圧力が増大し、アクセス速度も低下するため、キャッシュを使用してデータベース アクセスを減らす必要があります。 .プレッシャー;
ここでの操作は元の注文とは異なることがわかります: 生成されたフラッシュ セールの事前注文はデータベースにすぐには書き込まれませんが、最初にキャッシュに書き込まれます。ユーザーが支払いに成功すると、ステータスが変更され、データベースに書き込まれます。
num はデータベースに保存されているフィールドであり、フラッシュ殺菌された製品の残りの数量が保存されているとします。
if($num > 0){ //用户抢购成功,记录用户信息 $num--; }
同時実行性の高いシナリオで、データベース内の num の値が 1 であるときに、複数のプロセスが num が 1 であることを同時に読み取る可能性があると仮定します。プログラムは条件が次であると判断します。条件を満たし、購入が成功しました。数値から 1 を引いた値です。
これは商品の過剰納品につながります。スナップできる商品は 10 個までですが、10 人を超える人が商品を手に入れる可能性があります。このとき、ラッシュ購入が完了すると num はマイナスになります。 。
この問題には多くの解決策がありますが、単純に mysql ベースの解決策と redis ベースの解決策に分けることができます。redis のパフォーマンスは mysql によるものであるため、より高い同時実行性を実現できますが、以下で紹介する解決策はすべて単一の mysql と redis の場合、同時実行性を高めるには分散ソリューションが必要ですが、この記事では扱いません。
商品テーブル Goods
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
購入結果テーブル ログ
CREATE TABLE `log` ( `id` int(11) NOT NULL AUTO_INCREMENT, `good_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8
①悲観的ロック
悲観的ロック スキームは排他的読み取りを使用します。つまり、同時に 1 つのプロセスのみが num の値を読み取ることができます。トランザクションがコミットまたはロールバックされると、ロックが解放され、他のプロセスがそのロックを読み取ることができるようになります。
このソリューションは最もシンプルで理解しやすいため、パフォーマンス要件が高くない場合は、このソリューションを直接採用できます。 SELECT ... FOR UPDATE
は、ロックする行をできるだけ少なくするために、可能な限りインデックスを使用する必要があることに注意してください。
排他的ロックは、トランザクションの実行が完了した後にのみ解放されます。完了ではなく完了 読み取りが完了すると解放されるため、使用されるトランザクションはできるだけ早くコミットまたはロールバックして、排他ロックをより早く解放する必要があります。
$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; }
②オプティミスティック ロック
オプティミスティック ロック スキームは、データの読み取り時に排他ロックを追加しませんが、バージョン フィールドを通じて排他ロックを更新します。複数のプロセスが同じ数値を読み取り、正常に更新するという問題を解決するために、自動的に増分されます。各プロセスはnumを読み込む際にversionの値も読み込み、numを更新する際にはversionも更新し、更新時にversionの等価判定を追加します。
10 個のプロセスが num の値が 1、version の値が 9 であることを読み取ったとします。すると、これら 10 個のプロセスによって実行される更新ステートメントは UPDATE Goods SET num=num-1,version となります。 =version 1 WHERE version=9
,
ただし、いずれかのプロセスが正常に実行されると、データベース内のバージョン値は 10 になり、残りの 9 つのプロセスは正常に実行されません。製品が過剰に配信されず、num の値が 0 未満にならないようにします。ただし、これは問題にもつながります。つまり、以前に急ぎ購入リクエストを発行したユーザーが商品を入手できない可能性がありますが、後のリクエストによって取得されます。
$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; }
③where 条件 (アトミック操作)
ペシミスティック ロック スキームにより、データベース内の num の値を 1 つのユーザーのみが使用できるようになります。同時に処理 読み取りと処理、つまり、同時読み取りプロセスをキューに入れて、ここで順番に実行する必要があります。
オプティミスティック ロック スキーム num の値は複数のプロセスで同時に読み取ることができますが、更新操作でのバージョンの等しい値の判定により、同時に実行される更新操作のうち 1 つの更新のみが成功することが保証されます。同時。
もっと簡単な解決策があります。それは、更新操作中に num>0 という条件付き制限を追加するだけです。 where 条件によって制限されたソリューションは、オプティミスティック ロック ソリューションに似ているように見え、過剰発行の問題の発生を防ぐことができますが、num が大きい場合、パフォーマンスは依然として大きく異なります。
このとき num が 10 で、5 つのプロセスが同時に num=10 を読み込んだとします。オプティミスティック ロック方式の場合、バージョン フィールドの値が等しいと判断されるため、5 つのプロセスのうち 1 つだけが読み込みを実行します。 5 つの処理の実行が完了すると、num は 9 になります。
where 条件の判定スキームでは、num>0 が正常に更新できれば、実行後の num は 5 になります。 5つの工程のうち、完了です。
$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; }
① 監視ベースのオプティミスティック ロック ソリューション
watch は 1 つ (または複数) のキーを監視するために使用されます。トランザクションの実行前にこの (またはこれらの) キーが他のコマンドによって変更された場合、トランザクションは中断されます。
このスキームは、mysql のオプティミスティック ロック スキームに似ており、具体的なパフォーマンスは同じです。
$num = $this->redis->get('num'); if($num > 0) { $this->redis->watch('num'); usleep(100); $res = $this->redis->multi()->decr('num')->lPush('result',$num)->exec(); if($res == false){ echo "fail1"; }else{ echo "success:".$num; } }else{ echo "fail2"; }
②リストベースのキュースキーム
基于队列的方案利用了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视频教程》
以上がPHP での高い同時実行性 (製品のフラッシュセール) の問題を解決するにはどうすればよいですか? 2 つのソリューションを共有の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。