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

Redisトランザクションの正体:`MULTI`、`EXEC`、そして`WATCH`の境界線を極める

こんにちは。テックリードの私だ。
今日のコードレビューで、こんな実装を見かけた。

> 「マイコンの排他制御のために、Redisの `MULTI` と `EXEC` を使って残高更新を安全にしました!」

……思わずコーヒーを吹き出しそうになった。
彼はRDBMS(MySQLやPostgreSQL)の感覚のまま、Redisにトランザクションを持ち込もうとしていたのだ。

Redisのトランザクションは強力だが、RDBMSのそれとは全く別物である。この違いを理解せずにプロダクション環境に投入すれば、データ破損やサイレントエラーの温床となる。

今回は、Redisにおけるトランザクション(`MULTI` / `EXEC`)の限界、そして唯一の救世主である `WATCH` による楽観的ロックのメカニズムを、アーキテクチャの深部から叩き込む。

—

1. Redisのトランザクションは「ACID」ではない

まず、残酷な現実から伝えなければならない。
Redisのトランザクションは、一般的なRDBMSが持つACID特性の多くを満たさない。

| 特性 | Redisの挙動 | 備考 |
| :— | :— | :— |
| A (Atomicity: 原子性) | 部分的にNO | コマンドキューイングはアトミックだが、途中でエラーがあってもロールバックしない。 |
| C (Consistency: 一貫性) | YES | 構文エラーや型エラー(Stringに対してHGETなど)を防ぐ仕組みはある。 |
| I (Isolation: 隔離性) | YES | シングルスレッドモデルのため、`EXEC` 実行中のコマンドが割り込むことはない。 |
| D (Durability: 持続性) | 設定次第 | AOFのfsyncポリシー(everysec等)に依存するため、永続性は保証されない場合がある。 |

特に重要なのは 「ロールバックしない」 という仕様だ。
RDBMSに毒された脳みそだと、「トランザクション中に例外が出たら自動で巻き戻る」と思いがちだが、Redisは違う。

以下のコードを見てほしい。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET balance 1000
QUEUED
127.0.0.1:6379> INCRBY balance “invalid_string” # 意図的な型エラー
QUEUED
127.0.0.1:6379> SET balance 2000
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK

結果はどうなったか?
2番目のコマンドはエラーになったが、1番目の `SET balance 1000` も、3番目の `SET balance 2000` も実行され、完了している。

これがRedisの `MULTI` / `EXEC` の正体だ。
単なる「コマンドをまとめて順番通りに一気に実行する(途中で他のクライアントのコマンドが割り込まないようにする)キューイング機構」に過ぎない。

これを「トランザクションだから安全」と勘違いして設計しているエンジニアがいたら、今すぐコードを差し戻すべきだ。

—

2. なぜRedisはこの設計なのか?(アーキテクチャ的背景)

なぜRedisはロールバックをサポートしないのか?
答えはシンプルで、「圧倒的なパフォーマンスを維持するため」だ。

RDBMSがロールバックを実装できるのは、変更前のデータをUNDOログに記録し、複雑なMVCC(多版同時実行制御)やロック管理を行っているからである。これには相応のCPUとメモリのオーバーヘッドが伴う。

一方、Redisのコアはシングルスレッド(正確にはI/O多重化とイベントループ)で動いている。
「複雑なロールバック機構を排除し、メモリ上のデータ構造を極限までシンプルに高速操作する」という哲学が、RedisをRedisたらしめているのだ。

したがって、Redisにおけるトランザクションエラーのほとんどは、「アプリケーション側のバグ(開発ミス)」として扱う。実行時エラー(型違いなど)が発生した場合、それはトランザクションのせいではなく、コードの不備としてテスト段階で排除されるべき性質のものなのだ。

—

3. 競合を制する:`WATCH` による楽観的ロック

「じゃあ、複数のクライアントが同時に同じキーを書き換えるような競合状態(Race Condition)はどう防ぐんだ?」

ここで登場するのが `WATCH` コマンド だ。
Redisは悲観的ロック(行ロックなど)を持たない代わりに、楽観的ロック(Optimistic Locking)を提供する。

楽観的ロックのアルゴリズム

1. `WATCH key` でキーを監視対象にする。
2. キーの現在の値を取得する(ビジネスロジックの計算用)。
3. `MULTI` を発行する。
4. 計算結果を基に更新コマンドをキューイングする。
5. `EXEC` を実行する。
6. もし `WATCH` してから `EXEC` するまでの間に、他のクライアントがそのキーを変更していれば、トランザクションは破棄され、`nil`(Null)が返る。

具体的な実装例(Python + Redis-py)

実務でよくある「残高引き落とし」の安全な実装パターンを擬似コードで示そう。

import redis

client = redis.Redis(host=’localhost’, port=6379, decode_responses=True)

def withdraw(account_id: str, amount: int):
key = f”account:{account_id}:balance”

with client.pipeline() as pipe:
while True:
try:
# 1. キーを監視(楽観的ロックの開始)
pipe.watch(key)

# 2. 現在の値を取得
current_balance = pipe.get(key)
current_balance = int(current_balance) if current_balance else 0

if current_balance < amount: pipe.unwatch() raise ValueError("残高不足です") # 3. トランザクション開始 pipe.multi() pipe.set(key, current_balance - amount) # 4. 実行(他のクライアントに書き換えられていればここで WatchError が発生) pipe.execute() break except redis.WatchError: # 競合が発生したため、ループをやり直す(リトライ) continue 使用例 withdraw("user_101", 500) このパターンにおいて、`WatchError` をキャッチしたらループして再試行(リトライ)するのが鉄則だ。コンテンション(競合)が激しい環境ではリトライ回数が跳ね上がるため、システムの負荷設計には注意が必要だが、データの整合性を担保する唯一無二の手段となる。 ---

4. チーフアーキテクトからの設計上の警鐘・ベストプラクティス

最後に、実務でRedisのトランザクションを扱う際の重要なプラクティスを授けよう。

1. `WATCH` のスコープに気をつけろ

`WATCH` は接続(Connection)単位で状態を持つ。コネクションプールを使っている場合、パイプラインのライフサイクルと接続の占有に注意しないと、予期せぬキー監視が残る原因になる。必ず `pipeline()` コンテキストマネージャ等の安全なスコープ管理を利用すること。

2. Luaスクリプトという強力な選択肢を忘れるな

実は、Redis 2.6以降、複雑なアトミック処理には Luaスクリプト (`EVAL` / `EVALSHA`) が使えるようになった。
Luaスクリプトの内部で実行されるコマンドは、完全にアトミックであり、Redisサーバー内で直列に実行される。`WATCH` を使った複雑なリトライループを書くよりも、Luaスクリプトでアトミックに処理を閉じ込めた方が、コードがシンプルかつ高速になるケースが非常に多い。

  • 簡単なコマンドの連続 ➔ `MULTI` / `EXEC`
  • 競合制御が必要な条件付き更新 ➔ `WATCH` + `MULTI` または Luaスクリプト

現代のRedisアーキテクチャにおいて、複雑なロジックはLuaスクリプトに寄せることがトレンドだ。

—

まとめ

Redisのトランザクションは、RDBMSの感覚で使うと痛い目を見る。

  • `MULTI`/`EXEC` は単なるコマンドのバッチ実行(非割り込み)であり、ロールバックはしない。
  • エラーはトランザクションの失敗ではなく、アプリケーションのバグとして扱う。
  • 並行制御が必要な場合は、`WATCH` による楽観的ロック、あるいは Luaスクリプト を適切に選択する。

ツールの特性を正確に理解し、表層的な機能に惑わされない堅牢なシステムをデザインしてほしい。あなたのコードレビューを、楽しみにしている。

コメント

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