0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

キャッシュを超えて理解するRedisの仕組みと設計判断

0
Last updated at Posted at 2026-06-20

こんにちは!株式会社GROWTH VERSEに所属しております川田剣士です。実務でRedisはキャッシュサーバーとして使用してはいるもののの、Redisはシステムデザインにおいては一般的に非常に多用途で使用される技術であるため、改めてしっかり整理したいなと思いまして、知見を整理し技術記事で体系的にまとめようと思いました。記事に対する質問・コメント大歓迎です!

Redisとは何か

Redisはオープンソースのインメモリデータストアです。多くのエンジニアはRedisを「キャッシュサーバー」として認識していますが、それだけではRedisの本質を十分に表現できません。Redisは単なるキャッシュではなく、豊富なデータ構造を扱える高速なデータストアとして設計されています。公式ドキュメントでも Redis は "Data Structure Server" と表現されており、Key-Value Storeの枠を超えた用途で利用されています。例えば以下のような用途があります。

  • キャッシュ
  • セッション管理
  • レートリミット
  • 分散ロック
  • ランキングシステム
  • Pub/Sub
  • イベントストリーム処理

現代のWebサービスにおいて、Redisは単なる補助的なミドルウェアではなく、システム全体の性能やスケーラビリティを支える重要なコンポーネントとなっています。

Redisはキャッシュサーバーと捉えるのは勿体無い

Redisを学び始めた頃、多くの人は次のような構成を目にします。

Application
    ↓
 Redis
    ↓
Database

データベースへのアクセスを削減するためにRedisへキャッシュする構成です。実際、この用途はRedisの代表的な利用方法です。しかしRedisの価値はキャッシュだけではありません。ここからはRedisが採用される代表的なユースケースを紹介します。

Redisが採用される代表的なユースケース

キャッシュ

最も一般的な用途です。頻繁に参照されるデータをRedisへ保存することで、データベース負荷を大幅に削減できます。例えば以下のようなデータが対象になります。

  • 商品情報
  • ユーザー情報
  • 設定情報
  • 集計結果

セッション管理

Redisは複数台のアプリケーションサーバーが存在する環境でも、共有ストレージとして利用することでセッション情報を一元管理できます。昨今のモダンな開発では、Cognitoが発行するBearer Token(JWT)を自社バックエンドでローカル検証(署名と有効期限のチェック)し、リクエストを認可する手法が一般的です。しかし、この手法単体では「管理者サイドから特定のユーザーを即時ログアウトさせる」といったリアルタイムな制御が難しいという課題があります。そのため、ログアウトされたトークンのID(jti)や停止されたユーザーIDを「ブラックリスト」としてRedisで管理する手法が有効です。これにより、JWTのローカル検証に成功した場合であっても、ブラックリストに該当するリクエストを確実に弾くことができます。こうした「JWT(ローカル検証) + Redis(ブラックリスト)」によるセッション管理の併用は、実務でも広く採用されている設計パターンです。さらに、Redisサーバーのダウン時にはブラックリストチェックを一時的にスキップ(フォールバック)させる実装を施すことで、Redisがシステム全体の単一障害点(SPOF)になるリスクを防止し、高い可用性を維持できます。

レートリミット

APIの利用回数制限を実装する際にもRedisは非常に相性が良いです。例えば以下のような要件です。

  • 1分間に100回まで
  • 1時間に1000回まで

RedisのINCRコマンドとTTLを組み合わせることで効率的に実装できます。

ランキングシステム

RedisのSorted Setはランキング用途に非常に適しています。

例えば、

  • ゲームランキング
  • 売上ランキング
  • 人気記事ランキング

などを高速に実現できます。

Pub/Sub

RedisのPub/Sub機能を利用することで、システム間の結合度を下げる(疎結合にする)メッセージング基盤を容易に構築できます。パブリッシャー(送信側)がチャネルへイベントを発行(Publish)すると、そのチャネルを購読(Subscribe)している複数のサブライバー(受信側)へ、ミリ秒単位で一斉にデータをリアルタイム配信(プッシュ配信)できます。複雑な設定なしでイベント駆動型のアーキテクチャを実現できるため、軽量な通知システムや即時同期処理に非常に有効です。

Stream処理

Redis Streamsを活用することで、大規模なApache Kafkaと同じような、信頼性の高いリアルタイムデータ処理(ストリーム処理)を構築できます。複数のサーバーで作業を安全に分担する「負荷分散」や、エラー時の「自動再試行」、データの「二重処理防止」といった重要な機能を標準で装備。専用の巨大なシステムを導入せず、すでに実績のあるRedisだけで実現できるため、小〜中規模のシステム開発において非常にスマートな選択肢となります。

Redisはなぜ高速なのか

Redisは「高速なキャッシュサーバー」として広く知られています。実際にRedisを利用すると、データベースへのアクセス回数を大幅に削減でき、アプリケーションのレスポンス改善を体感できます。しかし、「Redisはなぜ速いのか?」と聞かれたときに説明できるでしょうか。Redisの性能を理解するためには、まずRedisのアーキテクチャを理解する必要があります。

Redisはシングルスレッドで動作する

Redisの特徴としてよく挙げられるのが、

Redisはシングルスレッドで動作する

という点です。現代のサーバーアプリケーションの多くはマルチスレッドで実装されています。例えばWebサーバーやアプリケーションサーバーでは、複数のリクエストを同時に処理するために複数スレッドや複数プロセスが利用されます。一方Redisでは、クライアントから送信されたコマンドを基本的に1つずつ順番に処理します。

GET user:1
 ↓
SET user:2
 ↓
INCR counter
 ↓
ZADD ranking

この説明だけを聞くと、「CPUを1コアしか使えないので遅いのでは?」と思うかもしれません。しかし実際にはRedisは非常に高い性能を発揮します。ではなぜRedisはシングルスレッドという設計を採用し、それでも高速に動作できるのでしょうか。

Redisがシングルスレッドを選択した理由

Redisが設計された当時、開発者たちはCPU性能よりも別の問題に注目していました。それは並行処理による複雑さです。マルチスレッドで共有データを扱う場合、次のような問題が発生します。

  • ロック管理
  • デッドロック
  • コンテキストスイッチ
  • スレッド間競合

例えば2つのスレッドが同じデータを書き換えるケースを考えてみます。

Thread A
SET counter = 10

Thread B
SET counter = 20

どちらの値が最終結果になるのかを保証するためにはロックが必要になります。ロックは整合性を保つ上で重要ですが、その分オーバーヘッドも発生します。Redisはこの複雑さを排除するために、「コマンドを順番に処理する」というシンプルな設計を採用しました。その結果、

  • ロック不要
  • 実装が単純
  • レイテンシが予測しやすい

というメリットを得ています。

データがメモリ上に存在する

Redis最大の特徴は、データをメモリ上に保持することです。一般的なRDBMSでは最終的にディスクへのアクセスが発生します。

Application
     ↓
Database
     ↓
SSD

一方Redisは基本的にRAM上でデータを管理します。

Application
     ↓
Redis
     ↓
RAM

メモリアクセスはSSDアクセスと比較して圧倒的に高速です。Redisが高速である最大の理由はここにあります。

シンプルな処理モデル

Redisは複雑なクエリ解析を行いません。例えばRDBMSでは以下のような処理が発生します。

  • SQL解析
  • 実行計画作成
  • インデックス選択
  • JOIN処理

一方Redisのコマンドは非常にシンプルです。

GET user:1
SET user:1 value
INCR counter

そのため処理コストが小さく、高速に実行できます。

Redis 6からは一部マルチスレッド化された

なお、Redisは現在完全なシングルスレッドではありません。Redis 6以降ではネットワークI/O処理の一部がマルチスレッド化されています。ただし重要なのは、

コマンド実行そのものは現在もシングルスレッド

という点です。Redisのシンプルさと性能を両立するため、この設計は現在も維持されています。

Redisのデータ構造

前章では、Redisが高速な理由について説明しました。しかしRedisの価値は単に高速なだけではありません。Redisが多くのシステムで採用される理由の一つが、用途に応じた豊富なデータ構造を提供していることです。例えば、

  • ユーザー情報をキャッシュしたい
  • キューを実装したい
  • ランキングを管理したい
  • 重複を排除したい
  • 非同期イベントを処理したい

といった要件に対して、それぞれに適したデータ構造が用意されています。Redisが「Data Structure Server」と呼ばれる理由もここにあります。

String

StringはRedisで最も基本的なデータ構造です。名前からは単なる文字列を保存する機能に見えますが、実際にはJSONなどのシリアライズ済みデータを格納する用途で広く利用されています。例えば、アプリケーションで扱う構造体をJSONへ変換してRedisへ保存するケースは非常に一般的です。

SET user:1 '{"id":1,"name":"Alice","age":30}'

また、カウンターやレートリミットの実装にも利用されます。(Redisには独立したInteger型は存在しない)

INCR page_view

主な用途

  • オブジェクトキャッシュ
  • カウンター
  • レートリミット

Hash

Hashはフィールド単位でデータを管理できる構造です。

HSET user:1 name Alice
HSET user:1 age 30

StringでJSON全体を保持する方法と異なり、一部フィールドだけを更新できます。

HSET user:1 name Bob

ユーザー情報や設定情報など、フィールド単位で更新が発生するデータに適しています。

主な用途

  • ユーザープロフィール
  • 商品情報
  • 設定情報

List

Listは順序を持つデータ構造です。

LPUSH queue job1
LPUSH queue job2

追加されたデータを順番に取り出せるため、シンプルなキューとして利用できます。

RPOP queue

ただし、メッセージの再処理や複数コンシューマーでの処理が必要な場合は、後述するStreamsの方が適しています。

主な用途

  • タスクキュー
  • FIFO処理
  • シンプルなジョブキュー

Set

Setは重複を許さない集合です。

SADD online_users user1
SADD online_users user2

同じ値を追加しても重複しません。

SADD online_users user1

また、集合演算もサポートしています。

SINTER group_a group_b

例えば、

  • 両方のグループに所属するユーザー
  • 共通のタグを持つ記事

などを効率的に取得できます。

主な用途

  • オンラインユーザー管理
  • タグ管理
  • 重複排除
  • 集合演算

Sorted Set

Sorted SetはRedisを代表するデータ構造の一つです。値に対してスコアを持たせることができます。

ZADD ranking 1000 Alice
ZADD ranking 800 Bob
ZADD ranking 1200 Carol

スコア順に自動でソートされるため、ランキング機能を効率的に実装できます。例えば上位10件を取得する場合は次のようになります。

ZREVRANGE ranking 0 9

特定ユーザーの順位も取得できます。

ZREVRANK ranking Alice

内部的にはHash TableとSkip Listを組み合わせて実装されており、順位取得や範囲検索を高速に実行できます。

主な用途

  • ゲームランキング
  • 人気記事ランキング
  • 売上ランキング
  • スコア管理

Streams

Streamsはイベントやメッセージを扱うためのデータ構造です。RedisにはListも存在するため、

キューならListで十分では?

と思うかもしれません。実際、単純なジョブキューであればListでも実装できます。

LPUSH jobs job1
LPUSH jobs job2

Workerはジョブを取得して処理します。

RPOP jobs

しかし、実際のシステムでは次のような問題が発生します。

ジョブ取得
 ↓
DB更新中
 ↓
Worker障害

この場合、ジョブは既にListから取り出されています。Redisはそのジョブが

  • 正常に処理されたのか
  • 処理途中で失敗したのか

を把握できません。その結果、ジョブが失われる可能性があります。Streamsはこの問題を解決するために設計されています。Workerがメッセージを取得しただけでは、Redisはそのメッセージを完了扱いしません。

Producer
    ↓
Streams
    ↓
Worker

メッセージは「処理中」として管理されます。処理が成功した場合、WorkerはRedisへACKを送信します。

Worker
    ↓
処理成功
    ↓
ACK

Redisは初めて

このメッセージは処理完了

と認識します。もしWorkerが処理途中で停止した場合はどうでしょうか。

Worker
 ↓
メッセージ取得
 ↓
処理中
 ↓
障害発生

この場合、RedisはACKを受け取っていないため、

未処理メッセージ

として管理し続けます。そのため別のWorkerが後から再処理できます。またStreamsはConsumer Groupをサポートしています。

Order Stream
       ↓
Order Consumer Group
       ↓
┌──────┬──────┬──────┐
↓      ↓      ↓
Pod1  Pod2  Pod3

複数のWorkerへジョブを分散できるため、大量のイベントも効率的に処理できます。このようにStreamsの本質は、単にメッセージを保存することではありません。

「誰が処理したか」「処理が完了したか」をRedisが管理できること にあります。

そのためStreamsは、

  • 非同期ジョブ処理
  • イベント処理
  • メッセージキュー
  • イベント駆動アーキテクチャ

など、信頼性が求められるシステムで利用されています。

主な用途

  • 非同期ジョブ処理
  • イベント処理
  • メッセージキュー
  • イベント駆動アーキテクチャ

Redisの永続化

これまでRedisを

  • 高速なデータストア
  • インメモリデータベース

として紹介してきました。ここで一つ疑問が生まれます。

Redisはメモリ上にデータを保持しているのに、サーバーが停止したらデータは消えてしまうのではないか?

その通りです。Redisはデータをメモリ上で管理しているため、何もしなければプロセス終了時にデータを失います。しかし実際のRedisは永続化機能を備えており、再起動後もデータを復元できます。Redisには主に2つの永続化方式があります。

  • RDB(Redis Database)
  • AOF(Append Only File)

それぞれ特徴とトレードオフが異なります。

RDB

RDBはスナップショット方式の永続化です。

一定間隔ごとにメモリ上のデータをファイルへ保存します。

Redis Memory
      ↓
 Snapshot
      ↓
 dump.rdb

例えば、「5分ごと」や「1000回更新されたら」といった条件でスナップショットを取得できます。

RDBのメリット

RDBは保存されたデータの状態をそのまま保持してコマンドまでは管理しないため、ファイルサイズが比較的小さくなります。また、Redis起動時の復元も高速です。そのため、

  • バックアップ
  • 障害復旧
  • レプリカ作成

などで広く利用されています。

RDBのデメリット

スナップショット取得後に障害が発生すると、その間のデータは失われます。例えば、

12:00 Snapshot
12:04 Redis障害

の場合、

12:00〜12:04

の更新内容は失われる可能性があります。つまりRDBは性能と引き換えに、一定量のデータ損失を許容する方式です。

AOF

AOFは操作ログ方式の永続化です。Redisへ実行されたコマンドをログとして記録します。例えば、

SET user:1 Alice
INCR page_view
ZADD ranking 100 Alice

がそのままファイルへ保存されます。

Redis Command
      ↓
 appendonly.aof

Redis再起動時は、このログを順番に再実行することでデータを復元します。

AOFのメリット

RDBよりもデータ損失を抑えられます。設定によっては、毎秒ログをディスクへ同期できます。そのため障害発生時でも、失われるデータを最小限にできます。

AOFのデメリット

コマンドを記録し続けるため、ファイルサイズが大きくなりやすいという欠点があります。例えば、

INCR counter
INCR counter
INCR counter

のような更新もすべて記録されます。そのためRedisは定期的にAOF Rewriteを実行し、不要な履歴を整理します。

AOF Rewrite

AOFはコマンドを記録し続けるため、長期間運用するとファイルサイズが肥大化します。例えば次のような操作を考えてみます。

SET counter 0
INCR counter
INCR counter
INCR counter
INCR counter
INCR counter

最終状態は

counter = 5

です。しかしAOFには全ての操作が記録されるため、更新回数が増えるほどファイルサイズも大きくなります。そこでRedisはAOF Rewriteという仕組みを提供しています。AOF Rewriteでは過去の操作履歴を保持するのではなく、現在の状態を再現するために必要な最小限のコマンドへ変換します。例えば先ほどの例であれば、

SET counter 5

へ置き換えることができます。これによってAOFファイルサイズを大幅に削減できます。

Rewrite中の更新はどうなるのか

ここで疑問になるのが、

Rewrite中に新しい書き込みが発生したらどうなるのか

という点です。RedisはRewrite専用の子プロセスを生成し、その時点のメモリ内容から新しいAOFファイルを作成します。

Redis
 ├─ Main Process
 └─ Rewrite Process

一方で、Rewrite中もRedisは通常通りリクエストを受け付けます。その間に発生した更新は、

旧AOF
+
AOF Rewrite Buffer

へ記録されます。Rewrite完了後にその差分を新しいAOFへ追記し、最後にファイルを切り替えます。

新AOF作成
      ↓
差分追記
      ↓
アトミックに切り替え

そのためサービス停止なしでAOF Rewriteを実行できます。

RDBとAOFの違い

両者を比較すると次のようになります。

項目 RDB AOF
保存方式 スナップショット コマンドログ
復元速度 高速 やや遅い
ファイルサイズ 小さい 大きくなりやすい
データ損失 多い 少ない
バックアップ用途

本番環境ではどちらを使うか

実際の本番環境では、

RDBかAOFか

ではなく、

RDBとAOFを組み合わせる

ケースが一般的です。

RDB
 ↓
スナップショット保存
 ↓
バックアップや高速な復旧に利用

AOF
 ↓
更新履歴を保存
 ↓
データ損失を最小限に抑える

RDBは現在の状態をスナップショットとして保存するため、バックアップや復旧時間の短縮に適しています。一方、AOFは更新履歴を記録するため、障害発生時のデータ損失を最小限に抑えることができます。RedisではAOFとRDBの両方が有効になっている場合、起動時はAOFを優先して復旧を行います。これは、AOFの方がより新しい状態を保持している可能性が高いためです。また、RDBはバックアップファイルとしても利用できるため、万が一AOFファイルが破損した場合や復旧できない場合には、運用者がRDBを利用して過去のスナップショット時点まで復旧することも可能です。そのため、RDBとAOFはどちらか一方を選択する関係ではなく、

RDB:バックアップと高速な復旧
AOF:データ損失の最小化

という補完関係にあると考えると理解しやすいでしょう。

キャッシュ用途なら永続化は不要な場合もある

ここで重要なのは、

すべてのRedisに永続化が必要なわけではない

ということです。例えばキャッシュ専用のRedisを考えてみます。

Application
      ↓
 Redis
      ↓ miss
 Database

Redisが消えても、データベースから再生成できます。この場合、

Redis再起動
 ↓
キャッシュ消失
 ↓
再構築

で問題ありません。そのためキャッシュ用途では永続化を無効化するケースもあります。一方、

  • セッション情報
  • ランキング
  • Streams
  • キュー

などRedis自体がデータソースとなっている場合は、永続化を検討する必要があります。

Redisは永続化が目的のデータベースではない

ここまで見ると、

Redisは永続化もできるならRDBMSの代わりになるのでは?

と思うかもしれません。しかしRedisの永続化は、高速なインメモリ処理を補完するための機能です。Redisの主目的は依然として高速なデータアクセスにあります。そのため、

  • 長期保存
  • 強い整合性
  • 大容量データ

が必要な場合は、RDBMSやNoSQLデータベースとの併用が一般的です。

Pub/Sub・Streamsの使い分け

前章ではRedis Streamsについて紹介しました。StreamsはConsumer GroupやACKをサポートしており、メッセージキューやイベント処理基盤として利用できます。しかしここで疑問が生まれます。RedisにはPub/Subも存在します。では、

  • Pub/Sub
  • Streams

は何が違うのでしょうか。ここではそれぞれの特徴と使い分けについて見ていきます。

Pub/Sub

Pub/SubはPublish/Subscribeモデルを実現する仕組みです。Publisherがメッセージを送信し、Subscriberが受信します。

Publisher
    ↓
 Redis Pub/Sub
    ↓
Subscriber

Pub/Subの最大の特徴はリアルタイム性です。Subscriberが接続していれば、メッセージは即座に配信されます。一方で、メッセージは保持されません。例えば、

Subscriber停止
 ↓
Message送信

した場合、そのメッセージは失われます。後から取得することはできません。

Pub/Subが向いている用途

  • チャット
  • 通知
  • リアルタイム配信
  • WebSocket連携

リアルタイム性を重視し、メッセージ損失を許容できるケースに向いています。

Streams

Streamsはイベントやメッセージを保持するためのデータ構造です。

Producer
    ↓
 Redis Streams
    ↓
Consumer

Pub/Subとの大きな違いは、メッセージを保持できることです。StreamsではConsumerがメッセージを取得しても、Redisはすぐに完了扱いしません。

Message取得
      ↓
処理
      ↓
ACK

が行われて初めて処理完了になります。そのため、

Message取得
      ↓
Consumer障害

が発生しても、未ACKメッセージとして再処理できます。

Streamsが向いている用途

  • 非同期ジョブ処理
  • イベント処理
  • メッセージキュー
  • イベント駆動アーキテクチャ

メッセージを失いたくない場合に適しています。

Pub/SubとStreamsの違い

両者の違いを整理すると次のようになります。

項目 Pub/Sub Streams
メッセージ保持 ×
再処理 ×
Consumer Group ×
ACK ×
リアルタイム性

Pub/Subは通知、Streamsはジョブ処理やイベント処理と考えると分かりやすいでしょう。

Redisと他の技術の使い分け

ここまでRedisの特徴や内部構造について説明してきました。ここからはRedisと他の代表的な技術を比較しながら、それぞれの使い分けについて整理します。

Redis vs RDBMS

RedisとRDBMSは比較されることがありますが、役割は大きく異なります。

項目 Redis RDBMS
保存場所 メモリ ディスク
速度 非常に高速 高速
永続化 補助的 主目的
トランザクション 限定的 強力
複雑な検索 苦手 得意

例えばユーザー情報を考えてみます。

User Table

のような永続データはRDBMSが適しています。一方、

user:1

のような頻繁に参照されるデータはRedisへキャッシュすることで性能を向上できます。RedisはRDBMSの代替ではなく、補完する存在として利用されることが一般的です。

Redis vs Memcached

Redisとよく比較されるキャッシュサーバーがMemcachedです。

項目 Redis Memcached
データ構造 豊富 Key-Valueのみ
永続化 ×
レプリケーション ×
メモリ効率
機能 多い 少ない

純粋なキャッシュ用途であればMemcachedでも十分な場合があります。

しかし現在では、

  • Sorted Set
  • Streams
  • Pub/Sub
  • 永続化

などの機能を利用できるRedisが選ばれるケースが増えています。

Redis vs Kafka

Redis StreamsとKafkaは比較されることがあります。

項目 Redis Streams Kafka
導入難易度 低い 高い
運用コスト 低い 高い
スケーラビリティ
保持期間 短い 長い
主な用途 ジョブ処理 イベント基盤

例えば、

会員登録
 ↓
メール送信

程度の非同期処理であればRedis Streamsで十分です。

一方、

全サービスのイベント収集
 ↓
分析基盤
 ↓
機械学習基盤

のような大規模なイベント処理ではKafkaが適しています。

Redis vs RabbitMQ

RabbitMQもメッセージブローカーとして広く利用されています。

項目 Redis Streams RabbitMQ
導入難易度 低い
ルーティング機能 限定的 強力
メッセージング機能
データ構造 豊富 なし

Redis Streamsはメッセージの保持や再処理に優れています。一方RabbitMQはExchangeによる柔軟なルーティング機能を備えており、「どのサービスへイベントを配送するか」を細かく制御できます。特に複雑なイベント配信や多様な購読パターンが求められるシステムではRabbitMQが選択されることがあります。

発展:Redisによる分散ロックの導入

複数のサーバーやプロセスが同時に同じリソース(特定のエンティティIDなど)を操作するのを防ぐため、Redisを用いた分散ロック(Distributed Lock)を導入して排他制御を行うことも可能です。

Redis側で実行される生のコマンド

Redisを使った分散ロックでは、「すでに別のプロセスが処理中のため、ロックの取得に失敗した際」に、自動で順番待ちをさせるようなキュー機能はデフォルトでは働きません。
そのため、アプリケーションから以下のようなコマンドを発行し、その成否(戻り値)を自前でハンドリングして処理を制御する必要があります。

ロックの取得(SETコマンド)

# キーが存在しない場合のみ(NX)、30秒間(PX 30000)の期限付きで、識別用UUIDを保存
SET lock:user:12345 "64f3b18d-4f05-4e78-9e6b-67a32b2e811c" NX PX 30000
  • 成功時: OK が返り、処理を続行できます。
  • 失敗時(すでに他がロック中): (nil) が返ります。アプリ側でこれを検知し、待機(リトライ)するかエラーを返します。

ロックの安全な解除(Luaスクリプト)

他プロセスのロックを誤って消さないよう、**「自分がセットしたUUIDと一致する場合のみ削除する」**という処理を、Luaスクリプトを用いてアトミックに実行します。

EVAL "if redis.call('get', KEYS) == ARGV then return redis.call('del', KEYS) else return 0 end" 1 lock:user:12345 "64f3b18d-4f05-4e78-9e6b-67a32b2e811c"

キーに lock: を付与する理由

Redisのキー名を単に user:12345 とするのではなく、lock:user:12345 のように lock: という接頭辞(プレフィックス)をつけるのには、運用の継続性において非常に重要な意味があります。

  1. データの「実体」と「ロック用フラグ」の混同防止
    Redisをキャッシュ(ユーザー情報の保管など)としても使用している場合、lock: をつけないと「キャッシュデータ」を「ロック用のトークン」で上書きして消し去ってしまう致命的なバグの原因になります。
  2. 運用の可視化
    障害発生時などに redis-cli でキー一覧を確認する際、lock: がついているだけで「今まさに排他制御中で、時間が経てば自動消滅(TTLで削除)するキーだな」と即座に判別できます。
  3. 一括管理の容易さ
    Redisの慣習通りコロン( : )で階層化しておくことで、万が一デッドロックなどのトラブルが起きた際も、他のキャッシュデータに影響を与えず lock:* という条件だけで安全に一括走査・削除が可能になります。

## 値に「識別用UUID」をセットする理由

ロックを取得する際、値として 1 などの固定値ではなく、毎回生成した uuid.New().String() をセットします。これは、「そのロックをかけた『実行プロセス(スレッド)』自身にしかロックを解除させないため」です。

固定値(例: "1")にしてしまった場合のバグ

  1. プロセスAがロック(値: "1")を取得し、重い処理を開始する。
  2. 処理が長引き、プロセスAのロックの有効期限(TTL)が切れて自動消滅する。
  3. プロセスBがやってきて、新しくロック(値: "1")を取得して処理を開始する。
  4. 遅れてプロセスAの処理が完了し、ロック解除(DEL)を実行する。
  5. 値が同じ "1" なので、プロセスAはプロセスBが今まさに使っているロックを誤って消し去ってしまう
  6. 結果、守りが崩れてプロセスCまで同時に侵入できるようになり、ロックが機能しなくなる。

UUID(プロセス固有の値)にした場合の挙動

値に プロセスA専用のUUID を入れておけば、4の手順でプロセスAが解除しようとした際、Luaスクリプト内部で「今RedisにあるのはプロセスBのUUIDだな」と判定できます。これにより、他人のロックを誤って消す事故を確実に防ぐことができます。

Go言語による実装例

Goの定番Redisクライアントである go-redis/v9 を使用し、他プロセスが処理中でロックが取得できなかった場合に、一定時間リトライ(スピンロック)を行う実装例です。

package main

import (
	"context"
	"errors"
	"fmt"
	"time"

	"://github.com"
	"://github.com"
)

var ctx = context.Background()

// ロック取得を試みる(スピンロック実装)
func acquireLockWithRetry(rdb *redis.Client, lockKey string, token string, ttl time.Duration, maxRetries int, retryDelay time.Duration) (bool, error) {
	for i := 0; i < maxRetries; i++ {
		// 生のコマンド: SET key token NX PX ttl
		success, err := rdb.SetNX(ctx, lockKey, token, ttl).Result()
		if err != nil {
			return false, err
		}
		if success {
			return true, nil // ロック取得成功
		}

		// 他のプロセスが処理中で取得失敗した場合は、一定時間待機してリトライ
		time.Sleep(retryDelay)
	}
	return false, nil // リトライ上限に達し、ロック取得失敗
}

// ロックを安全に解除する(Luaスクリプト実行)
func releaseLock(rdb *redis.Client, lockKey string, token string) error {
	// 自分が取得したロック(トークンが一致)のみ削除するLuaスクリプト
	var luaReleaseScript = redis.NewScript(`
		if redis.call("get", KEYS) == ARGV then
			return redis.call("del", KEYS)
		else
			return 0
		end
	`)

	// 生のコマンド: EVAL script 1 key token
	res, err := luaReleaseScript.Run(ctx, rdb, []string{lockKey}, token).Result()
	if err != nil {
		return err
	}

	if res.(int64) == 0 {
		return errors.New("lock could not be released (token mismatch or expired)")
	}
	return nil
}

func main() {
	rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
	
	lockKey := "lock:user:12345"
	token := uuid.New().String() // クライアント(プロセス)固有の一意のID
	ttl := 30 * time.Second

	// 1. ロックの取得(最大5回リトライ、100ms間隔)
	locked, err := acquireLockWithRetry(rdb, lockKey, token, ttl, 5, 100*time.Millisecond)
	if err != nil {
		panic(err)
	}
	if !locked {
		fmt.Println("二重実行防止:ロックの取得に失敗しました。")
		return
	}
	
	// 確実にロックを解除する
	defer func() {
		if err := releaseLock(rdb, lockKey, token); err != nil {
			fmt.Printf("ロック解除失敗: %v\n", err)
		} else {
			fmt.Println("ロックを安全に解除しました。")
		}
	}()

	// 2. メインのビジネスロジックをここに記述
	fmt.Println("ロック取得成功。排他処理を実行中...")
	time.Sleep(2 * time.Second) 
}

導入時のポイント(競合と有効期限の管理)

  1. 競合時の挙動(デフォルトは非ブロッキング)
    前述の通り、Redisの SetNX非ブロッキングなコマンドです。誰かが処理中のときは即座に失敗を返すため、順番に処理を待たせたい場合は上記のGoコードのように time.Sleep を挟んだリトライ制御(スピンロック)の自作か、既存の検証済みライブラリ(Redsync など)の導入が必要です。
  2. 適切なTTL(有効期限)の設定
    プロセスが途中で強制終了した際のデッドロックを防ぐためにTTLは必須ですが、実際の処理時間がTTL(上記例では30秒)を超えると、処理の途中でロックが自動解放されてしまいます。処理時間に対して十分余裕を持った値を設定してください。

難所:処理が長引いてRedisのTTLが切れた場合の対策

「DBのSQL実行や外部APIのレスポンス待ちで処理が長引き、RedisのTTL(有効期限)が切れたが、プロセス自体はまだ動いている」という状況が発生すると、別のプロセスがロックを取得できてしまい、排他制御が崩壊します。

この「Redisのロック状態」と「実際の処理状態」のズレを防ぐには、主に3つの解決アプローチがあります。

① ロックの自動延長(ウォッチドッグ機構)の導入

処理が続いている間、バックグラウンドでタイマーを動かし、**RedisのTTLを定期的に後ろへ延ばし続ける(生存報告をする)**仕組みです。

  • 仕組み: 例えばTTLが30秒なら、10秒ごとに「まだ処理中だからTTLをあと30秒にリセットして」というコマンドをRedisに送り続けます。処理が終わったら通常通りロックを削除します。
  • Goでの実現方法: 自前でゴルーチン(go func())と time.Ticker を使って実装することも可能ですが、バグを生みやすいため、この機構が最初から組み込まれている検証済みライブラリを導入するのが業界の定石です。

② DB負荷を抑えつつ、Redis障害やTTL切れに備える「悲観的ロック」との組み合わせ

Redisで分散ロックを行う最大の目的は、大量のリクエストを前段の軽量なRedisで弾き、DB側での悲観的ロック(FOR UPDATEなど)の多発によるコネクション占有やデッドロック(=DBパフォーマンスの大幅な悪化)を防ぐことです。

基本的にはRedis側でリクエストが1つに絞られるため、DB側で重ねて悲観的ロックをかけるのは無駄に見えますが、「RedisのTTL切れ」や「Redis自体の障害」に対する最強の最終防衛線(防御的プログラミング)として非常に有用です。

  • 仕組み: アプリ層ではRedisロックで99.9%の重複リクエストを即座に弾きます。Redisを通過した「たった1つの本職プロセス」だけが、DB側で短時間の SELECT ... FOR UPDATE(悲観的ロック)を実行します。
  • 相乗効果(メリット):
    • パフォーマンスの維持: 同時多発的な重いDBロックは発生しないため、データベースのパフォーマンスは高く保たれます。
    • Redisの「TTL切れ」の救済: 万が一、ネットワーク遅延や処理遅延でRedisのTTLが切れて「2つ目のプロセス」が侵入してきても、DB側の悲観的ロックによって書き込み手前(SELECT 時点)で確実にブロック(待機)されます。
    • Redisの「予期せぬ障害」への耐性: Redisのノードが突然ダウンしたり、フェイルオーバー(主従切り替え)時の同期ズレによって一時的にロックの整合性が崩れたりすることがあります。そうしたRedis側のインフラ障害が起きた瞬間でも、DB側の悲観的ロックが最後の砦として機能するため、データの不整合(レースコンディション)が起きるリスクを絶対ゼロに抑え込めます。

③ 諦めて「超長いTTL」を設定する

対象の処理が「数秒〜数十秒で確実に終わるもの」であり、万が一システムがクラッシュしてデッドロックになっても数分後に自動解除されれば問題ないレベルであれば、実際の最大処理時間の3〜5倍のTTL(例: 5分など)を最初から設定しておくという割り切ったアプローチです。
単純ですが、ウォッチドッグなどの複雑なコードを管理しなくて済むというメリットがあります。

まとめ

Redisというとキャッシュサーバーというイメージを持つ方も多いかもしれません。実際、Redisは高速なインメモリデータベースとして広く利用されており、データベース負荷の軽減やレスポンス速度の向上に大きく貢献します。しかし本記事で見てきたように、Redisの価値は単なるキャッシュに留まりません。Redisは豊富なデータ構造を備えており、

  • キャッシュ
  • ランキング
  • レートリミット
  • 分散ロック
  • 非同期ジョブ処理
  • イベント処理

など、様々な用途で活用できます。また、

  • RDBとAOFによる永続化
  • Streamsによるメッセージング

といった機能によって、単なるKey-Valueストア以上の役割を担うことができます。一方でRedisは万能な技術ではありません。長期的なデータ保存や複雑な検索処理はRDBMSが得意ですし、大規模なイベントストリーミング基盤ではKafkaやRabbitMQといった選択肢が適している場合もあります。Redisの内部構造や特徴を理解しておくことで、

「なぜRedisを採用するのか」
「なぜRedisではなく別の技術を選ぶのか」

といった技術選定の判断もできるようになります。

本記事が、Redisを単なるキャッシュサーバーとしてではなく、システム設計を支える重要なコンポーネントとして理解するきっかけになれば幸いです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?