【実務・中級編】 DISCARDコマンド – Redis

こんにちは。開発チームのテックリードだ。
今日のコードレビューで、あるジュニアエンジニアが書いたトランザクション周りのコードを見つけてね、少し気になったのでこの話をさせてもらう。

「とりあえずエラーが起きたら例外キャッチして、Redisのコネクション破棄すればいいですよね?」

……待て待て。それはRedisのシングルスレッドモデルとクライアント・サーバー間の通信プロトコル(RESP)を全く理解していない者の発想だ。RedisのトランザクションはRDBのそれとは根本的に違う。特に `DISCARD` コマンドの役割とその裏側の挙動を誤解していると、高負荷時にスレッドをブロックさせたり、コネクションプールの汚染(コネクションリーク)という地獄を見る。

今日は、Redisのトランザクション破棄を司る `DISCARD` について、実務で絶対に知っておくべき極限の知見を授けよう。

—

1. そもそも `DISCARD` とは何か?(RDBとの決定的な違い)

RDBのトランザクションであれば、`BEGIN` の後に何かしらのクエリを発行し、エラーが起これば `ROLLBACK` を叩く。この時、実行途中の変更はアトミックに巻き戻される。

しかし、Redisの `MULTI` / `EXEC` / `DISCARD` の世界線はこれとは全く異なる。

1. `MULTI`: クライアントを「トランザクションモード」に移行させる。この瞬間から、送られたコマンドは即座に実行されず、クライアントごとの内部キューに積まれていく(返り値はすべて `”QUEUED”`)。
2. `EXEC`: キューに積まれたコマンドをアトミックに順次実行する。
3. `DISCARD`: 積まれたキューをすべてクリアし、トランザクション状態を終了する。変更は何も行われていないため、「巻き戻し(ロールバック)」ではなく単なる「破棄」だ。

> ⚠️ 超重要知見:Redisは実行中のロールバックをしない
> Redisの設計思想は「シンプルさと速度」である。`EXEC` の途中でコマンドがエラー(構文エラーではなくランタイムエラー、例えば文字列に対して数値操作をした等)を起こした場合でも、Redisは残りのコマンドを実行し続ける。RDBのような自動ロールバックは存在しない。
> だからこそ、「キューイング中に異常を検知した段階で `DISCARD` を叩いて安全に捨てる」というクライアント側の防衛コードが極めて重要になる。

—

2. コマンドの挙動とプロトコルの裏側

実際に `DISCARD` がどのように動くのか、Redis CLIを覗いてみよう。

トランザクションを開始
127.0.0.1:6379> MULTI
OK

コマンドをキューに積む
127.0.0.1:6379> SET account:101:balance 5000
QUEUED
127.0.0.1:6379> INCRBY account:101:balance -1000
QUEUED

処理途中で不整合(ビジネスロジック上のエラーなど)を検知した想定
127.0.0.1:6379> DISCARD
OK

トランザクションが破棄されているため、値は変更されていない
127.0.0.1:6379> GET account:101:balance
(nil)

この一連の流れ、一見地味だが、ネットワーク層では何が起きているか?
`MULTI` から `DISCARD`(あるいは `EXEC`)までの間、クライアントはRedisサーバーに対してパイプライン的にコマンドを送り続けている。サーバー側は、そのクライアント専用のメモリ領域にコマンドのキューを保持し続ける。

もし、アプリケーション側で例外が発生したにもかかわらず `DISCARD` を送らずに処理を中断するとどうなるか?
そのコネクションは「トランザクションモード(MULTI中)」に取り残されたまま、コネクションプールに返却されることになる。

これが最悪のバグを引き起こす。

—

3. 実務で踏む「地雷」:コネクションプールの汚染とデッドロック

次にプールの話だ。設計レビューで私が一番厳しくチェックするのがこの部分である。

アプリケーション(Node.js, Python, Go, Javaなど)からRedisを使う場合、通常はコネクションプール(例:ioredis, go-redis, Jedisなど)を利用する。

❌ 悪い設計(アンチパターン)

Python (redis-py) のイメージ
pipe = client.pipeline(transaction=True)
try:
pipe.set(“key1”, “value1”)
# ここで何らかの予期せぬPython側の例外が発生!
raise ValueError(“Business logic error”)
pipe.set(“key2”, “value2″)
pipe.execute()
except Exception as e:
# DISCARDを呼ばずにスルー、あるいはコネクションをそのままにする
logger.error(f”Error: {e}”)
raise

このコードの何がヤバいか?
例外が起きたとき、`pipe`(コネクション)は `MULTI` を発行した状態で放置され、プールに戻される。
次に別のリクエストがそのコネクションをプールから取得した時、そのコネクションはすでに `MULTI` 状態になっている。その結果、次に流れてきた無関係なコマンドが意図せずキューに積まれ、予期せぬタイミングでバグや暴走を引き起こす。

⭕️ 堅牢な設計パターン(必ず `finally` で保証する)

プログラミング言語のクライアントライブラリの多くはラッパーを提供しているが、トランザクションを自前で制御する場合、あるいは低レベルなコマンド発行を行う場合は、以下のように必ず `finally` 句で `DISCARD`(またはクライアントの強制リセット)を担保しなければならない。

Python (redis-py) での堅牢なトランザクション管理
pipe = client.pipeline(transaction=True)
try:
pipe.multi() # 明示的なMULTI
pipe.set(“inventory:item_1”, 10)

# 在庫チェックなどのビジネスロジック
if not check_stock(“item_1”):
raise ValueError(“Out of stock”)

pipe.decr(“inventory:item_1″)
result = pipe.execute() # 成功すればここでコミット

except Exception as e:
# 異常系:確実にトランザクションを破棄する
try:
pipe.discard()
except Exception as discard_err:
# コネクション自体が切断されている場合のハンドリング
logger.error(f”Failed to discard transaction: {discard_err}”)

# アプリケーション層のエラーハンドリング
raise CustomApplicationException(f”Transaction aborted: {e}”)

ライブラリによっては、`pipeline(transaction=True)` のコンテキストマネージャ(Pythonの `with` 文など)が自動的に `EXEC`(成功時)または `DISCARD`(例外時)をハンドリングしてくれるものもある。
だが、「裏側で何が起きているか」を理解せずにライブラリに依存するな。 ライブラリがタイムアウトやコネクション切断時にどう振る舞うか、ドキュメントとソースコードを確認するのがシニアの仕事だ。

—

4. パフォーマンスとスケーラビリティの注意点

Redisは超高速だ。1秒間に数十万〜数百万のリクエストをさばく。しかし、`MULTI` 〜 `DISCARD` の間、サーバー側はそのクライアントのためのメモリを消費し、状態を維持している。

1. 長期化するトランザクションの禁止
トランザクションのキューイング中に、外部APIを呼び出したり、重いDBクエリを挟んではならない。ネットワーク遅延やアプリの処理遅延によってRedisのコネクションが占有され、サーバー全体のメモリ圧迫やクライアントプールの枯渇を招く。Redisは「インメモリの超高速データストア」であり、トランザクション内は純粋なRedisコマンドの詰め合わせだけを瞬時に処理するべきだ。

2. Luaスクリプトとの使い分け
複雑な条件分岐や、途中の計算結果によって次のコマンドが変わるような処理をトランザクションで行おうとすると、どうしてもクライアント側でロジックを挟む必要があり、結果として `DISCARD` で破棄するケースが増える。
もしアトミック性と複雑なロジックを両立させたいのであれば、`MULTI`/`EXEC`/`DISCARD` ではなく、`EVAL` コマンド(Luaスクリプト)の採用を強く推奨する。Luaスクリプトであれば、Redisサーバー内でアトミックに完結し、クライアントとの無駄な往復通信も発生しない。

—

チームへのメッセージ

Redisの `DISCARD` は、単なる「取り消しボタン」ではない。
分散システムにおける「異常系のフェイルセーフ(Fail-Safe)」の要だ。

「動けばいい」ではなく、「異常時にリソースがどう解放され、次のリクエストにどんな影響を与えるか」まで見通したコードを書くこと。それが、我がチームのエンジニアリング基準だ。

次回のコードレビューでは、トランザクション周りの `try-catch-finally` とコネクションの状態管理、そして本当にその処理に `MULTI` が必要か(Luaスクリプトの選択肢も含めて)を厳しく見させてもらう。

以上、今回の知見を明日からの設計に活かしてくれ。期待している。

コメント

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