redis の同時実行の問題
私は長い間 redis をキャッシュとして使用してきましたが、Redis は単一のプロセスとして実行され、コマンドが次々に実行されます。同時実行性の問題は発生しないだろうと思っていましたが、今日関連情報を見て初めて気づきました。
具体的な問題の例
名前が myNum であると仮定したキーがあり、その中にアラビア数字が格納されているとします。現在の値は 1 です。myNum 上で複数の接続が動作している場合、同時実行性の問題が発生します。 linkA と linkB の 2 つの接続があるとします。両方の接続が次の操作を実行し、myNum の値 1 を取り出して保存し直し、次の相互作用を確認します:
linkA get myNum => 1linkB get myNum => 1linkA set muNum => 2linkB set myNum => 2
操作を実行した後の結果おそらく 2 ですが、これは私たちが予想していた 3 とは一致しません。
具体的な例を見てみましょう:
<?phprequire "vendor/autoload.php";$client = new Predis\Client([ 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379,]);for ($i = 0; $i < 1000; $i++) { $num = intval($client->get("name")); $num = $num + 1; $client->setex("name", $num, 10080); usleep(10000);}
name の初期値を 0 に設定し、2 つの端末を使用して上記のプログラムを同時に実行します。name の最終値は異なる場合があります。 2000 ですが、< ;2000 という値は、上記の同時実行性の問題が存在することを証明しています。これを解決するにはどうすればよいでしょうか?
redis のトランザクション
redis にもトランザクションがありますが、このトランザクションは mysql ほど完璧ではなく、一貫性と分離性を保証するだけであり、アトミック性は満たしていません。 . 性別と耐久性。
redis トランザクションは multi、execcommands
アトミック性を使用します。redis は、途中で実行がある場合でも、トランザクション内のすべてのコマンドを実行します。失敗してもロールバックはありません。 Kill シグナル、ホストのダウンタイムなどによりトランザクションの実行が失敗し、redis は再試行またはロールバックしません。
永続性、redis トランザクションの永続性は、redis で使用される永続性モードに依存しますが、残念ながら、さまざまな永続性モードは永続的ではありません。
分離、redis は単一プロセスです。トランザクションの開始後、exec コマンドが検出されるまで現在の接続のすべてのコマンドが実行され、その後、他の接続のコマンドが処理されます。
一貫性、この文書を読んだ後、これは非常にばかげていると思いますが、それは正しいようです。
redis のトランザクションはアトミック性をサポートしていないため、上記の問題は解決できません。
もちろん、redis には、この問題を解決できる watch コマンドもあります。次の例を参照して、キーに対して watch を実行し、トランザクションを実行します。watch の存在により、 , 彼はキー a を監視します。 a が修復されると、後続のトランザクションは実行に失敗します。これにより、複数の接続が同時に行われ、すべての a が監視されます。正常に実行できるのは 1 つだけで、他のものは失敗を返します。
127.0.0.1:6379> set a 1OK127.0.0.1:6379> watch aOK127.0.0.1:6379> multi OK127.0.0.1:6379> incr aQUEUED127.0.0.1:6379> exec1) (integer) 2 127.0.0.1:6379> get a"2"
失敗の例。最後から、test の値が他の接続によって変更されたことがわかります:
127.0.0.1:6379> set test 1OK127.0.0.1:6379> watch testOK127.0.0.1:6379> multiOK127.0.0.1:6379> incrby test 11QUEUED127.0.0.1:6379> exec(nil) 127.0.0.1:6379> get test"100"
問題の解決方法
redis コマンドはアトミックなので、値がアラビア数字の場合は、get および set コマンドを incr または incrby に変更してこの問題を解決できます。次のコードにより、2 つの端末を同時に実行できるようになります。結果は、2000 年を予想しました。
<?phprequire "vendor/autoload.php";$client = new Predis\Client([ 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379,]);for ($i = 0; $i < 1000; $i++) { $client->incr("name"); $client->expire("name", 10800); usleep(10000);}
これは確かに実現可能であり、その効果は悪くありません。これは例です
<?phprequire "vendor/autoload.php";$client = new Predis\Client([ 'scheme' => 'tcp', 'host' => '127.0.0.1', 'port' => 6379,]);class RedisLock{ public $objRedis = null; public $timeout = 3; /** * @desc 设置redis实例 * * @param obj object | redis实例 */ public function __construct($obj) { $this->objRedis = $obj; } /** * @desc 获取锁键名 */ public function getLockCacheKey($key) { return "lock_{$key}"; } /** * @desc 获取锁 * * @param key string | 要上锁的键名 * @param timeout int | 上锁时间 */ public function getLock($key, $timeout = NULL) { $timeout = $timeout ? $timeout : $this->timeout; $lockCacheKey = $this->getLockCacheKey($key); $expireAt = time() + $timeout; $isGet = (bool)$this->objRedis->setnx($lockCacheKey, $expireAt); if ($isGet) { return $expireAt; } while (1) { usleep(10); $time = time(); $oldExpire = $this->objRedis->get($lockCacheKey); if ($oldExpire >= $time) { continue; } $newExpire = $time + $timeout; $expireAt = $this->objRedis->getset($lockCacheKey, $newExpire); if ($oldExpire != $expireAt) { continue; } $isGet = $newExpire; break; } return $isGet; } /** * @desc 释放锁 * * @param key string | 加锁的字段 * @param newExpire int | 加锁的截止时间 * * @return bool | 是否释放成功 */ public function releaseLock($key, $newExpire) { $lockCacheKey = $this->getLockCacheKey($key); if ($newExpire >= time()) { return $this->objRedis->del($lockCacheKey); } return true; } }$start_time = microtime(true);$lock = new RedisLock($client);$key = "name";for ($i = 0; $i < 10000; $i++) { $newExpire = $lock->getLock($key); $num = $client->get($key); $num++; $client->set($key, $num); $lock->releaseLock($key, $newExpire);}$end_time = microtime(true);echo "花费时间 : ". ($end_time - $start_time) . "\n";
shell php setnx.php & php setnx.php& を実行すると、最終的に結果:
$ 花费时间 : 4.3004920482635 [2] + 72356 done php setnx.php# root @ ritoyan-virtual-pc in ~/PHP/redis-high-concurrency [20:23:41] $ 花费时间 : 4.4319710731506 [1] + 72355 done php setnx.php
同様に 1w 回ループし、usleep を削除し、incr を使用して直接増加します。これには約 2 秒かかります。
インカム取得時に usleep をキャンセルすると、時間が減らないどころか増加してしまうため、処理が無駄なループをしないように適切な usleep 設定を行う必要があります。
まとめ
簡単にまとめると、実際、redis は単一プロセスであり、コマンドがどれだけ実行されても 1 つずつ実行されるため、同時実行性の問題は発生しません。これを使用すると、get と set のペアなど、同時実行の問題が発生する可能性があります。
以上がRedis の最大同時実行数はどれくらいですか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。