redis の同時実行の問題
私は長い間 redis をキャッシュとして使用してきましたが、redis は単一のプロセスです。実行中、コマンドは次々に実行されます。同時実行性の問題は発生しないだろうと常々思っていました。今日関連情報を見て初めてそのことに気づきました (推奨: redis ビデオ チュートリアル) )
具体的な問題例
名前が myNum であると仮定したキーがあり、その中にアラビア数字が格納されているとします。 1 であり、myNum 上で複数の接続が動作しているため、同時実行の問題が発生します。 linkA と linkB の 2 つの接続があるとします。両方の接続が次の操作を実行し、myNum の値 1 を取り出して保存し直し、次の相互作用を確認します:
linkA get myNum => 1 linkB get myNum => 1 linkA set muNum => 2 linkB set myNum => 2
操作を実行した後の結果おそらく 2 ですが、これは私たちが予想していた 3 とは一致しません。
別の具体的な例を見てください:
<?php require "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 コマンドと exec コマンドを使用します
Atomicity、redis はトランザクション内のすべてのコマンドを 1 回実行し、途中で実行に失敗した場合でもロールバックしません。 Kill シグナル、ホストのダウンタイムなどによりトランザクションの実行が失敗し、redis は再試行またはロールバックしません。
永続性、redis トランザクションの耐久性は、redis で使用される永続化モードに依存しますが、残念ながら、さまざまな永続化モードは永続的ではありません。
分離、redis は単一のプロセスです。トランザクションの開始後、exec コマンドが検出されるまで現在の接続のすべてのコマンドが実行され、その後、他の接続のコマンドが処理されます。 。
一貫性、この文書を読んだ後、これは非常にばかげていると思いますが、それは正しいようです。
redis のトランザクションはアトミック性をサポートしていないため、上記の問題は解決できません。
もちろん、redis には watch コマンドもあり、この問題を解決できます。次の例を参照して、キーに対して watch を実行し、トランザクションを実行します。watch の存在により、キー a を監視します。 . After が修正されると、後続のトランザクションは実行に失敗します。これにより、複数の接続が同時に受信され、すべての接続が監視されます。 a. 1 つだけが正常に実行され、他の接続はすべて失敗を返します。
127.0.0.1:6379> set a 1 OK 127.0.0.1:6379> watch a OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> incr a QUEUED 127.0.0.1:6379> exec 1) (integer) 2 127.0.0.1:6379> get a "2"
失敗の例。最後から、test の値が他の接続によって変更されたことがわかります:
127.0.0.1:6379> set test 1 OK 127.0.0.1:6379> watch test OK 127.0.0.1:6379> multi OK 127.0.0.1:6379> incrby test 11 QUEUED 127.0.0.1:6379> exec (nil) 127.0.0.1:6379> get test "100"
問題の解決方法
Redis のコマンドはアトミックなので、値がアラビア数字の場合、get および set コマンドを incr または incrby に変更してこの問題を解決できます。次のコードは、次のコードで 2 つのターミナルを開きます。同時に実行後の結果は 2000 で、期待どおりです。
<?php require "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); }
@manzilu さんがコメントで挙げた方法
manzilu さんがコメントで挙げた方法を情報を確認して確認しましたが、確かに実現可能で効果も悪くありませんこれは例です
<?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
同様に 1 回ループし、usleep を削除し、incr を使用します直接増加するには約 2 秒かかります。
インカム取得時に usleep をキャンセルすると、時間が減らないどころか増加してしまうため、処理が無駄なループをしないように適切な usleep 設定を行う必要があります。
まとめ
ここまで読んだので簡単にまとめると、実際、redis は単一プロセスであり、どれだけ多くのコマンドが実行されても 1 つずつ実行されるため、同時実行性の問題はありません。これを使用すると、get と set のペアなど、同時実行の問題が発生する可能性があります。
その他の redis 関連の記事については、redis データベース チュートリアル 列に注目してください。
以上がRedis の同時実行性の問題の解決の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。