blog

redisシリーズ - 物事と楽観的ロック

mysqlの学習でよく言われることですが、mysqlにはACIDの4つの特徴、原子性、一貫性、分離、永続性があります。 redisは何か?それはどのようなものですか?以下、実際のテストを使って結果をお...

May 8, 2020 · 6 min. read
シェア

Redisシリーズカタログ

redisシリーズ - Redisはなぜ速いのか?

redisシリーズ - データ永続化

redisシリーズ - 一貫したハッシュアルゴリズム

redisシリーズ - 高可用性

redisシリーズ - 物事と楽観的ロック

redisシリーズ - データ型geospatial: 隣に老王はいるか?

redisシリーズ - データ型ビットマップ:今日はサインインしましたか?

ブルーム・フィルターは存在します!

mysql を学ぶとき、mysql にはモノがあり、モノには 4 つの ACID 特性、原子性、一貫性、分離、および永続性があるとよく言われます。

redisには何かありますか?どのように見えるのでしょうか?実際にテストした結果です。

もの

redisにはモノがあります。しかし、redisにおけるthingは弱いものです。thingsは分離レベルを持ちませんし、thing内の複数のコマンドはアトミックではありません。このような理由から、redisのthingも実際の生産現場ではほとんど使われません。

redisのthingの本質は、コマンドのコレクションです。thing内のコマンドはすべてシリアライズされてキューに格納され、thingの実行中に、コマンドは落ちてきた順に実行されます。

redisは単一コマンドのアトミック性を保証しますが、複数コマンドのアトミック性は保証しません。

通常の業務遂行

redisを使うには3つのステップがあります:

  • オンにする

  • チームに参加させる

  • 実行

普通に展示されているもの

.1:6379>
.1:6379> flushall
OK
.1:6379> multi #オンにする
OK
.1:6379> set name wuxl #キューに入るコマンド
QUEUED
.1:6379> set age 30 #キューに入るコマンド
QUEUED
.1:6379> get name #キューに入るコマンド
QUEUED
.1:6379> set addr Tokyo #キューに入るコマンド
QUEUED
.1:6379> exec #実行する 
1) OK
2) OK
3) "wuxl"
4) OK
.1:6379>

上記のことがコミットされると、4つのコマンドが順次実行され、実行が完了すると終了します。

物品のキャンセル

また、オンになっているものをキャンセルすることもできます:

.1:6379>
.1:6379> flushall
OK
.1:6379> multi
OK
.1:6379> set name wuxl
QUEUED
.1:6379> set age 30
QUEUED
.1:6379> discard #キャンセルする
OK
.1:6379> get age #コマンドの中にあるものは実行されない。
(nil)
.1:6379>

エラーの報告

コンパイルエラー

コンパイルエラーは、キュー内のコマンドのキュー自体に問題があるため、エラーが発生したときにキューにコマンドの結果、;コンパイルエラーがある、execの実行は、失敗をプロンプトが表示されます、すべてのコマンドを実行することはできません。

.1:6379> flushall
OK
.1:6379> multi
OK
.1:6379> set name wuxl
QUEUED
.1:6379> get # 間違ったコマンド、キューの入力エラー
(error) ERR wrong number of arguments for 'get' command
.1:6379> set age 30
QUEUED
.1:6379> exec # コミットエラーが発生し、全てのコマンドが実行できない
(error) EXECABORT Transaction discarded because of previous errors.
.1:6379> get name # 結果ではないクエリー
(nil)
.1:6379> get age
(nil)
.1:6379>

実行時エラー

ランタイム・エラーとは、スタック上にあるコマンド自体にエラーはないが、実行キューから外れたときにエラーを報告することです。例えば、Stringに対する次のような自己インクリメント操作です。

.1:6379> flushall
OK
.1:6379> set name wuxl # 名前を初期化する。string
OK
.1:6379> multi
OK
.1:6379> set age 30
QUEUED
.1:6379> incr name # キューに入るコマンドは、名前に対してセルフインクリメントを行う。コマンド自体に問題はないが、実行時にエラーが発生する。
QUEUED
.1:6379> set addr Tokyo
QUEUED
.1:6379> exec # タスクを投入し、各コマンドを順番に実行する
1) OK
2) (error) ERR value is not an integer or out of range # つ目のコマンドはエラーを報告する。
3) OK
.1:6379> get name 
"wuxl"
.1:6379> get age # 他のコマンドは全て正常に実行されている
".0.0.1:6379> get addr
"Tokyo"
.1:6379>

ここでわかるように、ランタイムはエラーを報告しますが、事態はロールバックされず、エラーは後続のコマンドの実行には影響しません。つまり、キュー内のコマンドにはアトミック性がありません

オプティミスト・ロック

楽観的ロックと悲観的ロック

ペシミスティック・ロック

同時実行の問題が発生する可能性が高いと考える方が悲観的です。このような場合こそ、実際のロッキングを処理する必要があります。ロックはパフォーマンスを低下させます。

オプティミスト・ロック

これは、同時実行の問題が発生する可能性が低く、より楽観的であると考えられています。この時点では、ロックを追加する必要はなく、変更操作を実行するだけで、元のデータが変更されたかどうかを比較し、変更がなければ変更し、変更があれば変更しません。これは通常、バージョンフィールドを使用してmyslで処理されます。

redisはデータを変更する際に、他のスレッドによってデータが変更されたかどうかを監視するwatchコマンドを提供しており、データが変更された場合は変更に失敗し、変更されていない場合は変更に成功します。実際、watchコマンドはredisにおける楽観的ロックの実装とみなすことができます。

転送シミュレーション

以下では、2つの口座間で送金を行うビジネスをシミュレートしています。

シングルスレッドシミュレーション

通常の移籍プロセス:

.1:6379> flushall #データベースを空にする
OK
.1:6379> set acc1 1000 #支払い口座に$1000がある
OK
.1:6379> set acc2 0 #受取口座には$0がある 
OK
.1:6379> multi #オンにする
OK
.1:6379> decrby acc1 100 #支払口座の引き落とし100
QUEUED
.1:6379> incrby acc2 100 #マネー口座のレシートを受け取る100
QUEUED
.1:6379> exec #実行する
1) (integer) 900
2) (integer) .0.1:6379> get acc1 #支払い口座には"
.1:6379> get acc2 #受け取ったアカウントは"
.1:6379>

上記のシングルスレッドでの送金シミュレーションの後、支払い口座には支払い後900があり、受け取り口座には100があります。

コンカレントシミュレーション

この処理において、誰かがexecの前にacc1に$1000をチャージしようとした場合、今回ロックが使用されなかった場合、同時実行の問題が発生しますが、exec終了後の結果はどうなるでしょうか?結果はacc1が1900、acc1が100となり、この結果も正しいです。なぜでしょうか?なぜなら、redisは物事を分離しないので、2つの物事が互いに影響し合うからです。

execを実行する必要がある場合は、acc1が変更されていないことを比較し、それが変更されている場合は、転送が失敗しました。このような場合、redisウォッチを使って楽観的にロックすることができます。以下は、2つのクライアントが同時にredisデータを変更し、楽観的ロックとしてwatchを使用するシミュレーションです。

acc1 は支払口座、acc2 は受取口座です。

.1:6379> flushall
OK
.1:6379> set acc1 1000 #口座への支払い
OK
.1:6379> set acc2 0 #お金を受け取る
OK
.1:6379>

ステップ2:クライアント1を使用して、acc1の変更をリッスンするためにwatchをオンにし、また、物事をオンにし、物事を最初に実行せずにキューにコマンドを実行します。

.1:6379>
.1:6379> watch acc1 # watchを使って、acc1の口座が実行中に変更されないか監視する。
OK
.1:6379> multi # オンにする
OK
.1:6379> decrby acc1 100 #支払いのシミュレーション
QUEUED
.1:6379> incrby acc2 100 #集金をシミュレートする
QUEUED
.1:6379> 

ステップ3:クライアント2を使用して、acc1口座の金額を変更します。

.1:6379>
.1:6379> incrby acc1 1000 # acc1の口座への別の入金をシミュレートする。1000
(integer) .0.1:6379> get acc1 # これはacc1の口座の変更である。"
.1:6379>

ここで、クライアント2が正常に実行されたことがわかります!もしmysqlだったら、この時点でクライアント2はブロックされ、ここで成功する前にクライアント1の実行が終わるのを待たなければなりません。redisは分離されておらず、互いに影響を及ぼし合っている、というのはこういう意味です。

ステップ4:クライアント・ワンを使って実行します。

.1:6379> exec # ことを実行すると、実行時に、変更するかどうかacc1と比較される、変更すると、失敗の実装;acc1が変更されなかった場合は、成功の実装の
(nil) #実行失敗
.1:6379> get acc1
".0.0.1:6379> get acc2
"0"
.1:6379>

ここでは、クライアント2がacc2の口座の金額を変更したため、クライアント1がexecを実行する前に、watchがacc1の金額が変更を送信したことを監視し、クライアント1の転送処理が失敗していることがわかります。ここでは実際にwatchを使って楽観的ロックを実装しています。

完了!

知識の普及、価値の共有、小さなパートナーの注目とサポートのおかげで、私は諸葛小猿、インターネット民衆の闘争の不確実性です!

Read next

[最初にしなければならないことは、データを回復するためにビンログを使用することである。

まとめると、binlogは記録されたSQL文に従ってデータを復元したり、ちょっとした修理をしたり、主にバックアップに頼ったりするのにしか使えません。

May 8, 2020 · 2 min read