【テクニカル・上級編】 トランザクション(MULTI/EXEC) – Redis

Redisのトランザクションは「ACID」ではない:極限のレイテンシと整合性の相克

Redisのトランザクション(`MULTI`/`EXEC`)を、RDBMSのそれと混同しているエンジニアが多い。もし君がRedisのトランザクションを「万能なACID準拠の担保」だと信じているなら、今すぐその認識を捨てろ。

Redisにおけるトランザクションとは、「コマンドのシリアライズされたキューイング」に過ぎない。本稿では、Redisがなぜあえて「ACIDの限界」を突き抜けた設計を選んだのか、その内部メカニズムと、現実のアーキテクチャで我々がどう立ち回るべきかを紐解く。

—

1. 内部構造:`MULTI`から`EXEC`までの正体

Redisはシングルスレッドのイベントループで動作する。この「シングルスレッド性」こそが、Redisのトランザクションがアトミックに見える最大の理由だ。

  • キューイング: `MULTI`コマンドが発行されると、クライアントの接続状態は`CLIENT_MULTI`フラグに切り替わる。それ以降送られてくるコマンドは即時実行されず、クライアントごとのプライベートなメモリバッファ(`multiState`構造体)に積み上げられる。
  • アトミック性: `EXEC`が発行された瞬間、Redisは他のクライアントからのリクエストを一切遮断し、バッファ内のコマンドを順次実行する。この「実行中の非介入性」が、命令の順序性を担保する。

ここが落とし穴

Redisのトランザクションには、RDBMSにあるような「ロールバック」が存在しない。 `EXEC`の実行中にコマンドが失敗(構文エラーや型不一致)しても、先行する成功したコマンドの結果は永続化される。これは、Redisが「複雑なリトライコストよりも、高速なメモリ操作を優先する」という哲学に基づいているからだ。

—

2. WATCHコマンド:楽観的ロックの極致

`MULTI/EXEC`だけでは、Read-Modify-Writeの競合を防げない。そこで登場するのが`WATCH`だ。

`WATCH`は、特定のキーに対する「変更監視」を行う。内部的には、データベース構造体内の`watched_keys`ハッシュマップに、そのキーとクライアントのIDが紐付けられる。

メカニズムの核心

1. 監視: `WATCH key`を実行すると、キーのバージョン(実際には`dirty`カウンターなど)を追跡する。
2. 中断: 他のクライアントが該当キーに書き込みを行うと、Redisはフラグを立てる。
3. 無効化: もし監視中に`EXEC`が呼ばれた場合、Redisはフラグを確認し、変更が検知されていれば`EXEC`は失敗(`nil`を返す)する。

この仕組みは悲観的ロックのようなデッドロックを生まないが、「頻繁な衝突」が起きる環境では、アプリケーション側の再試行(リトライ)コストが爆発する。 大規模システムで`WATCH`を乱用するのは、エンジニアとしての怠慢だ。

—

3. アーキテクトへの提言:トランザクションを使わない設計

限界を突破したいなら、そもそも「トランザクションに頼らない」アーキテクチャを追求すべきだ。

Luaスクリプトという最適解

`MULTI/EXEC`よりも推奨されるのは、Luaスクリプトの活用である。Luaスクリプトは、実行される間、Redisサーバー全体をブロックする(※注:Redis 7.0以降の`Function`機能を除く)。

  • 理由: `MULTI/EXEC`はネットワークラウンドトリップが複数回発生するが、Luaスクリプトなら1回のコマンドで完結する。
  • アトミック性: スクリプト実行中は他のコマンドが割り込まないため、事実上の排他制御がより厳密かつ高速に行える。

— Luaスクリプトによるアトミックなカウンターインクリメント
— 競合を気にせず、サーバー側で安全に処理が完結する
local current = redis.call(‘GET’, KEYS[1])
if not current or tonumber(current) < tonumber(ARGV[1]) then redis.call('SET', KEYS[1], ARGV[1]) return 1 end return 0 ---

4. 結論:何を選択すべきか

1. 単純なコマンドの順序保証が必要な場合: `MULTI/EXEC`で十分だ。だが、エラー処理はアプリケーション側で完結させろ。
2. Read-Modify-Writeの整合性が重要な場合: `WATCH`で楽観的ロックを実装し、必ず「リトライループ」を記述しろ。
3. 極限のパフォーマンスと複雑なロジックが必要な場合: Luaスクリプト(またはRedis Functions)一択だ。これはRedisというエンジンの「計算リソース」を直接叩く方法であり、ネットワーク遅延を極限まで排除できる。

Redisは、整合性を「トレードオフ」として提供している。その仕様を理解し、システムの要求する整合性レベルに合わせて技術を選定する。それこそが、アーキテクトが担うべき責務である。

—

「ツールが何をしないか」を知ることは、「ツールが何をするか」を知ることよりも価値がある。Redisのトランザクションは、その最たる例だ。

コメント

タイトルとURLをコピーしました