Redisにおけるトランザクション処理は、主に「MULTI/EXECコマンドによるトランザクションモード」と「Luaスクリプト」の2つのアプローチが存在します。
結論から述べると、それぞれの特性は以下の通りです。
**トランザクションモード (MULTI/EXEC)**
* 隔離性を保証します。
* 永続性の保証は困難です。
* 限定的な原子性を持ちますが、ロールバック機能はサポートしていません。
* 制約条件に基づく一貫性の定義においては、一貫性を維持可能です。
**Luaスクリプト**
* 実用的な場面で多用されるアプローチです。
* 高い原子性を持ちますが、スクリプト内でエラーが発生しても自動的なロールバックは行われません。
* 隔離性を確保しており、処理の結果を次のステップで利用するような依存関係のあるロジックを自然に記述できます。
トランザクションモードの仕組み
Redisのトランザクション関連コマンドは以下の通りです。
| コマンド | 説明 |
|---|
| MULTI | トランザクションブロックの開始をマークします。 |
| EXEC | トランザクションブロック内の全コマンドを実行します。 |
| DISCARD | トランザクションを破棄し、キュー内のコマンドを実行しません。 |
| WATCH | 指定したキーを監視します。EXEC実行前にキーが変更された場合、トランザクションは中断されます。 |
| UNWATCH | WATCHによる監視を解除します。 |
実行フローは以下の3段階で構成されます。
1. **開始**: `MULTI`コマンドにより、クライアントがトランザクション状態へ遷移します。
2. **キューイング**: `MULTI`以降のコマンドは即時実行されず、キューに格納されます。
3. **実行/破棄**: `EXEC`でキュー内のコマンドを一括実行するか、`DISCARD`で破棄します。
以下はトランザクションの実行例です。
redis> MULTI
OK
redis> SET user:1001 "Alice"
QUEUED
redis> INCR visit_count
QUEUED
redis> EXEC
1) OK
2) (integer) 1
`EXEC`実行前であっても、Redisのキーは他のクライアントから変更可能です。これを防ぐために`WATCH`コマンドを使用します。`WATCH`により、楽観的ロックと同様の効果が得られ、競合が発生した場合はトランザクションが失敗し`nil`が返ります。
ACID特性の検証
原子性
原子性とは、全操作が完了するか、全く実行されないかのいずれかの状態になることを指します。
**ケース1: 構文エラー(入札時エラー)**
存在しないコマンドや構文ミスがある場合、コマンドのキューイングに失敗し、`EXEC`時にはトランザクション全体が破棄されます。この場合、原子性は保たれます。
redis> MULTI
OK
redis> SET account "active"
QUEUED
redis> INVALID_COMMAND # 存在しないコマンド
(error) ERR unknown command
redis> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
**ケース2: 実行時エラー**
型不一致など、実行時に初めて判明するエラーが発生した場合、Redisはトランザクション全体を中止せず、エラーとなったコマンドのみをスキップして残りを実行します。この挙動により、原子性は保証されません(ロールバックが行われません)。
redis> MULTI
OK
redis> SET counter "ten" # 文字列をセット
QUEUED
redis> INCR counter # 数値型ではないためエラーになるはず
QUEUED
redis> SET status "done"
QUEUED
redis> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
redis> GET status
"done" # エラー後の有効なコマンドは実行されている
以上より、Redisのトランザクションは特定の条件下でのみ原子性を持ち、ロールバックはサポートしていないと言えます。
隔離性
Redisには伝統的なデータベースのような隔離レベル(Read Uncommitted等)の概念はありませんが、シングルスレッドモデルによる隔離性を持ちます。
* **EXEC実行前**: 他のクライアントによる書き込みが可能です。データの整合性を保つために`WATCH`を併用します。
* **EXEC実行後**: コマンドキューの実行中は他のコマンドが割り込めないため、隔離性が保証されます。
永続性
永続性はRedisの設定に依存します。
* RDBやAOFが無効な場合、メモリ上のデータ消失リスクがあるため永続性はありません。
* AOFの`always`オプション以外(`everysec`や`no`)では、システムクラッシュ時のデータ消失可能性が残ります。
したがって、厳密な永続性を保証することは困難です。
一貫性
一貫性の定義が「データベースの制約を満たすこと」であれば、Redisは単純なデータ構造の制約のみを持ち、複雑な整合性チェックはアプリケーションに委ねられます。クラッシュ復旧後もRDB/AOFから一貫した状態に復帰するため、制約の範囲内では一貫性があると見なせます。
Luaスクリプトによるトランザクション
Redis 2.6以降、Luaスクリプトエンジンが組み込まれました。Luaスクリプトはトランザクションの代替手段として非常に強力です。
**メリット:**
* ネットワークオーバーヘッドの削減。
* 実行の原子性(スクリプト実行中は他のコマンドがブロックされる)。
* スクリプトの再利用。
EVALコマンドの基本
`EVAL`コマンドを使用してスクリプトを実行します。
# キーに値をセットし、その結果を返す
redis> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 config:theme dark
OK
* `KEYS[n]`: 操作対象のキー名。
* `ARGV[n]`: スクリプトに渡す引数。
EVALSHAによる最適化
スクリプトが長大な場合、毎回送信するのではなく、SHA1ハッシュを用いてキャッシュされたスクリプトを実行できます。
# スクリプトをロード
redis> SCRIPT LOAD "return redis.call('GET', KEYS[1])"
"2aaf1b5ef6c18b3e7e6c7e5d10e8a5d8e5e3e1e0"
# SHA1ハッシュで実行
redis> EVALSHA 2aaf1b5ef6c18b3e7e6c7e5d10e8a5d8e5e3e1e0 1 config:theme
"dark"
トランザクションモードとLuaスクリプトの比較
公式ドキュメントでも言及されている通り、RedisにおけるLuaスクリプトはトランザクションの一種として定義されています。
Luaスクリプトを使用することで、`MULTI`では不可能だった「前のステップの結果を利用して次の処理を分岐する」といった複雑なロジックを、ネットワークラウンドトリップなしで完結できます。分散ロックやレートリミット、一括更新などの実装において、Luaスクリプトは不可欠なツールとなっています。
ただし、Luaスクリプト実行中はRedisサーバーがブロックされるため、処理内容は軽量かつ高速である必要があります。また、スクリプト内でエラーが発生しても自動ロールバックはされないため、エラーハンドリングは慎重に行う必要があります。