トランザクション

MULTI, EXEC, DISCARD および WATCH は Redis におけるトランザクションの基本です。これらは、複数のコマンドの実行をひとつのステップで行えるようにします。その際、2 つの重要な点が保証されます。

バージョン 2.2 以降、Redis は上記 2 つの保証に加えて、check-and-set (CAS) 操作によく似た方法で楽観ロックの手段を提供しています。この話題については、このページで後ほど (“check-and-set を使った楽観ロック”) 触れます。

使用方法

Redis トランザクションは MULTI により開始されます。このコマンドは常に OK を返します。この時点から、ユーザーは複数のコマンドを発行可能です。これらのコマンドを実行する代わりに、Redis はキューイングを行います。すべてのコマンドは、 EXEC コマンドがコールされた時点で実行されます。

代わりに DISCARD をコールすると、トランザクションキューの内容がフラッシュされて、トランザクションは終了します。

以下の例では、’foo’ と ‘bar’ の 2 つのキーをアトミックにインクリメントしています。

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

セッションの様子を見るとわかるとおり、 MULTI [訳注: EXEC の間違い?] は応答の配列を返します。配列の要素はそれぞれ、トランザクション中の個々のコマンドへの応答になっており、また順番はコマンドの発行順序と同じです。

Redis コネクションが MULTI リクエストの途中にあるとき、すべてのコマンドは ‘QUEUED’ という応答を返します (Redis プロトコルの観点から、Status Reply として送られる)。キューイングされたコマンドは、 EXEC が呼び出され、実行されるまで、シンプルにスケジューリングされます。

トランザクション中のエラーについて

トランザクション中、2 種類のコマンドエラーが発生する可能性があります:

  • コマンドのキューイングに失敗すると、 EXEC コマンドが呼び出される前にエラーが発生します。たとえば、コマンドが文法的に誤っている場合 (引数の数が誤っている、不正なコマンド名、...)、または、out of memory (‘maxmemory’ ディレクティブによりサーバーにメモリ制限が設定されているとき) 等のなんらかのクリティカルな状態になった場合などです。

  • EXEC が呼び出された 後に コマンドが失敗することもあります。たとえば、誤った値をもったキーに対する操作を行った場合(文字列値に対して、リスト操作を実行する、というように)などです。

以前には、クライアントは、キューイングされたコマンドの戻り値をチェックし、ひとつめの種類のエラー (EXEC コールの前に発生する) を検知する必要がありました: もしコマンドが QUEUED と応答したら、正しくキューイングされており、そうでなければ Redis はエラーを返します。もしコマンドのキューイング中にエラーが発生したら、多くのクライアントはトランザクションを中止し、破棄するでしょう。

しかし Redis 2.6.5 以降、サーバーはコマンドの累積中にエラーが発生したことを覚えておき、 EXEC コール時にトランザクションの実行を拒否するとともにエラーを返し、トランザクションを自動的に破棄するようになりました。

Redis 2.6.5 以前は、事前にエラーが発生したことに構わずクライアントが EXEC コマンドをコールした場合、キューイングに成功した一部のコマンドだけでトランザクションを実行する、という振る舞いをしていました。新しい振る舞いは、トランザクションとパイプライニングのミックスをよりシンプルにし、これにより、トランザクション全体は一度に送信され、またすべての応答は一度に読み込まれるようになっています。

対して、 EXEC コマンドの後に起こるエラーには、特別なハンドリングは行われません: トランザクション中にいくつかのコマンドが失敗したとしても、その他のすべてのコマンドはそのまま実行されます。

これはプロトコルレベルでよりクリアになります。以下の例では、ひとつのコマンドが文法上は正しいにも関わらず、実行時に失敗しています:

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MULTI
+OK
SET a 3
abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
-ERR Operation against a key holding the wrong kind of value

EXEC は 2 つの要素からなる bulk-string-reply を返します。ひとつは ‘OK’ コード、そしてもうひとつは ‘-ERR’ 応答です。ユーザーに対して、ふさわしいエラーを提示する方法は、クライアントライブラリに任されています。

あるコマンドが失敗しても、その他のキューに入っているコマンドは処理される 点に留意することは重要です。Redis は一連のコマンドの処理を 止めません

別の例として、’telnet’ によるワイヤプロトコルを使って、文法エラーがすぐに報告されるケースを示します。

MULTI
+OK
INCR a b c
-ERR wrong number of arguments for 'incr' command

この場合、誤った INCR は、文法エラーのためキューイングされません。

なぜ Redis はロールバックをサポートしないのか?

もしあなたにリレーショナル・データベースのバックグランドがあったら、トランザクション中に Redis コマンドが失敗しても、ロールバックの代わりに残りのコマンドが実行される、という事実は奇妙に感じられるかもしれません。

しかし、この振る舞いについては、理に適う見解があります:

  • Redis のコマンドが失敗するのは、誤った文法で呼び出された(かつコマンドのキューイング時に問題が検知されなかった)場合か、間違ったデータ型をもつキーに対して実行された場合のいずれかしかありません: つまり、実際的な観点から言って、コマンドの失敗はプログラミングエラーによるものであり、プロダクション環境ではなく開発段階で検知可能な類のエラーです。

  • Redis は、ロールバック機能を必要としないことで、内部的にシンプルかつ高速化されています。

バグは起こり得る、という観点からの Redis への反論はありますが、しかし一般的に言って、ロールバック機能は、プログラミングエラーからあなたを救うことはありません。たとえば、あるキーを 1 ではなく 2 だけインクリメントしたり、または間違ったキーに対してインクリメントしたり、という誤りについて、ロールバック機構は助けになりません。プログラマを、自身のエラーから救い出すことは誰にもできないことで、また実行に失敗する Redis コマンドがプロダクション環境には入り込むことは起こりにくいでしょう。この点から、私たちはエラー時のロールバックをサポートするよりも、シンプルで高速なアプローチを選択しました。

コマンドキューの破棄

DISCARD はトランザクションを途中で打ち切るために使われます。このとき、いずれのコマンドも実行されることはなく、コネクションは通常の状態の戻されます。

> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"

check-and-set を使った楽観ロック

WATCH は check-and-set (CAS) の振る舞いを Redis トランザクションに提供します。

‘WATCH’ されたキーは、それらに対する変更を検知するために監視されます。もし、watch されているキーが 1 つでも EXEC コマンドの実行前に変更されたら、トランザクション全体が中止され、またトランザクションが失敗したことを通知するために EXECNull-reply を返します。

たとえば、あるキーの値を 1 ずつ自動インクリメントすることを考えてください (Redis が INCR をサポートしていないとして)。

最初の試行は次のようになります:

val = GET mykey
val = val + 1
SET mykey $val

このコードは、一度にひとつのクライアントのみから実行される場合に限り、安全に動作するでしょう。もし複数のクライアントが該当のキーを同時にインクリメントしようとすると、競合が発生します。たとえば、クライアント A とクライアント B が古い値、10 を読み取ったとしましょう。両方のクライアントにより、値は 11 にインクリメントされ、 SET されます。結果的に値は、12 ではなく 11 となります。

WATCH のおかげで、私たちはプログラムがうまく働くように作ることができます:

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

上記のコードを使うと、競合が発生し WATCHEXEC の呼び出しの間で他のクライアントが ‘val’ の結果を変更した場合、トランザクションは失敗します。

私たちは、次は競合が発生しないことを願いながら単純にオペレーションを繰り返します。この形式のロックは、 楽観ロック と呼ばれ、非常に強力なロックの方法です。多くのユースケースでは、複数のクライアントは異なるキーにアクセスするため、衝突は起こりにくいでしょう。通常は、オペレーションをやり直す必要はありません。

WATCH の説明

実際のところ、 WATCH とは何でしょう?これは、 EXEC を条件つきにします: ‘watch’ しているキーが、他のクライアントによって変更されていない場合に限り、トランザクションを実行するように Redis に要請します。そうでなければ、トランザクションはまったく実行されません。(もし、有効期限つきのキーを WATCH していて、watch 中に Redis がそのキーを expire した場合は、 EXEC は機能することに注意してください。 詳細についてはこちら )

WATCH は複数回呼ぶことができます。単純に、すべての WATCH 呼び出しは、開始から EXEC 呼び出しまでの間の変更を watch するのみです。1 回の WATCH 呼び出しで、キーはいくつでも指定できます。

EXEC が呼ばれると、トランザクションが中止されたかどうかに関わらず、すべてのキーは ‘unwatch’ されます。また、クライアントの接続が閉じられた場合も、すべて ‘unwatch’ されます。

すべての watch 中のキーをフラッシュするため、(引数なしの) UNWATCH コマンドが使えます。いくつかのキーを更新するために楽観ロックをかけ、その現在の値を読み取った後で処理を中断したくなったときに、しばしば有用です。この場合、単に UNWATCH を呼ぶだけで良く、そうすると新しいトランザクションのために接続が使える状態になります。

ZPOP を実装するために WATCH を使う

新しいアトミックな操作を定義するのに、 WATCH がどのように使えるか、を示す良い例が ZPOP の実装です。これは sorted set から、最も低いスコアをもつ要素をアトミックなやり方で pop する、というコマンドです。以下は、もっともシンプルな実装です:

WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC

もし EXEC が失敗したら(つまり、Null reply が返却されたら)、私たちは単純に操作を繰り返します。

Redis スクリプティングとトランザクション

Redis script はその定義からいってトランザクショナルです。そのため、Redis トランザクションで可能なことはすべてスクリプトで実現できます。また、大抵の場合、スクリプトはよりシンプルで高速です。

この重複は、スクリプティングは Redis 2.6 から導入されたけれども、一方でトランザクションはそれよりもずっと以前から存在していた、という事実からきています。しかし、私たちは直近のうちにトランザクションのサポートを排除するつもりはありません。Redis スクリプティングへの大がかりな移行をしなくても、競合を避けることが可能で、また Redis トランザクションの実装の複雑さは最小限なものであるためです。

しかし、今すぐではない将来、ユーザーベース全体がスクリプトを使うようになったと判断できれば、移行は不可能ではないでしょう。その際には、トランザクションを非推奨とし、最終的には機能を削除するかもしれません。