【実務・中級編】 Redisトランザクション (MULTI/EXEC) – Redis

Redisトランザクションの正体:MULTI/EXECの「限界」と「正攻法」を語る

Redisのトランザクション、つまり `MULTI` / `EXEC` を「RDBMSのトランザクションと同じ感覚」で使おうとしていないか?もしそうなら、今すぐ立ち止まってほしい。

Redisのトランザクションは、RDBMSのそれとは根本的に性質が異なる。これを正しく理解せずに実装すると、本番環境で深刻な整合性不全やボトルネックを引き起こすことになる。今日は、Redisのトランザクションという武器の「正しい構え方」について、現場の知見を詰め込んで伝授しよう。

—

1. MULTI/EXECの「本当の姿」を理解する

Redisのトランザクションは、「コマンドのキューイング」と「一括実行」に過ぎない。

  • アトミック性(Atomicity): 確かに一括実行はされるが、RDBMSにあるような「途中でのロールバック」は存在しない。
  • 隔離性(Isolation): コマンド実行中に他のクライアントの処理が割り込むことはない。これはRedisがシングルスレッドでコマンドを処理するからだ。

ここで注意すべきは、「文法エラー」と「実行時エラー」の扱いの違いだ。

正常なキューイング
MULTI
SET key1 “val1”
INCR key2 # もしkey2が文字列型なら、これは実行時にエラーになる
EXEC

ここで重要なのは、`INCR` がエラーになっても、その前の `SET` はそのままコミットされるということだ。Redisには「全か無か」のロールバックという概念はない。この事実を設計の前提条件に組み込む必要がある。

—

2. WATCHによる楽観的ロック:使い所を見極めよ

`WATCH` コマンドは、キーを監視し、`EXEC` するまでの間にそのキーが変更されたらトランザクションを破棄する仕組みだ。これは「楽観的ロック」そのものだが、乱用は禁物である。

堅牢な設計パターン:楽観的ロックの再試行

`WATCH` を使うなら、必ず「失敗した時の再試行ループ」を書かなければならない。

擬似コード:口座残高を安全に減らす処理
def withdraw(user_id, amount):
key = f”balance:{user_id}”
while True:
redis.watch(key)
balance = int(redis.get(key) or 0)

if balance < amount: redis.unwatch() return False # 残高不足 pipe = redis.pipeline() pipe.multi() pipe.decrby(key, amount) try: pipe.execute() # ここで他のプロセスがキーを更新していたら例外発生 return True except WatchError: continue # 競合したらやり直す チーフアーキテクトからの忠告:
このループ処理は、競合が激しい環境ではパフォーマンスを劇的に悪化させる。競合率が高いなら、Redisのトランザクションに頼らず、Luaスクリプトを採用すべきだ。

—

3. Luaスクリプトこそが「真のトランザクション」である

実務レベルで「真のアトミック性」が必要な場合、私は `MULTI/EXEC` ではなく、Luaスクリプトを強く推奨する。

なぜLuaなのか?

1. サーバーサイド実行: ネットワーク往復(RTT)を最小化できる。
2. 完全なアトミック性: Luaスクリプトの実行中、他のコマンドは一切割り込めない。
3. 条件分岐と計算: スクリプト内で `if` や `for` が書けるため、複雑なロジックを1回のコマンドで完結できる。

— Luaスクリプト例:条件付き更新
local balance = tonumber(redis.call(‘get’, KEYS[1]))
local amount = tonumber(ARGV[1])

if balance >= amount then
redis.call(‘decrby’, KEYS[1], amount)
return 1
else
return 0
end

`MULTI/EXEC` でクライアント側と何度もやり取りするよりも、Luaでロジックを閉じ込める方が、レイテンシ的にも整合性的にも圧倒的に優位だ。

—

4. 運用上の極意:避けるべきアンチパターン

最後に、レビューで私がよく指摘する「やってはいけないこと」をまとめる。

  • トランザクション内での重い処理: `MULTI` から `EXEC` までの間、Redisはシングルスレッドで待機状態になる。この間に時間のかかる処理(キーの全検索など)を挟むと、システム全体のレスポンスが壊滅する。
  • 過剰なWATCH: 監視するキーを増やしすぎると、Redis内部の監視リストの管理コストが跳ね上がる。必要な最小限のキーのみをWATCHせよ。
  • パイプラインとの混同: `MULTI/EXEC` はトランザクション用だが、単に通信効率を上げたいだけなら `PIPELINE` を使うべきだ。目的を履き違えないこと。

結論としてどう設計すべきか

1. 基本はLuaスクリプト: 複雑なロジックのアトミックな実行はこれが正攻法。
2. 軽い更新ならMULTI/EXEC: 競合リスクが極めて低く、単に複数のコマンドを並べるだけで良い場合。
3. 競合が多いならRDBMS: Redisに無理な整合性を持たせようとせず、RDBMSに逃がす勇気を持つこと。

Redisは「速さ」を売りにしたツールだ。その速さを殺すようなトランザクション設計をしていないか、今一度自身のコードを見直してみてほしい。アーキテクチャは、常に「何が起きるか」を予測し、そのリスクを最小化する方向へ設計するべきだ。

健闘を祈る。

コメント

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