キャッシュとは、頻繁にアクセスされるデータを、それが必要とされる場所の近くに保持しておく一時的なストレージ層のことです。 主な目的は、データ取得の高速化と、データベースのような低速な一次データソースへの負荷軽減です。
例え キッチンまで毎回取りに行く代わりに、お気に入りのコーヒーマグをデスクに置いておくようなものです。
- キャッシュからデータを直接提供
- 遅いデータベースクエリを回避
- サブミリ秒単位のレイテンシ
- トラフィックスパイク(急増)を吸収
- データベースのボトルネックを防止
- データベースクエリ数の削減
- ネットワーク帯域幅の使用量削減
- サーバーリソース消費の低減
実例 E コマースプラットフォームでは、フラッシュセール中にデータベースがダウンしないよう、商品詳細をキャッシュしています。
- アプリケーションのメモリ(RAM)に保存
- 非常に高速なアクセス
- 容量に制限がある
- プロセスのクラッシュでデータが失われる
- 複数のサーバー間で共有される
- 水平スケールが可能
- ローカルキャッシュより耐障害性が高い
- ローカルメモリよりわずかにレイテンシが高い
- 地理的にユーザーに近い場所に配置
- 通常、CDN を介して実装される
- インターネットレイテンシを最小化
- グローバルなユーザー体験を向上
- 書き込みはキャッシュとデータベースに同期的に行われる
- 強い整合性
- 書き込みレイテンシの増加
- 書き込みはまずキャッシュに行われる
- データベースは非同期で更新される
- 高速な書き込み
- キャッシュ障害時にデータ損失のリスクがある
- アプリケーションはまずキャッシュを確認する
- ミス(見つからない)場合、データベースからロードしてキャッシュに保存する
- パフォーマンスと鮮度のバランスが良い
- 実務で最も一般的に使用される
「計算機科学にはハードなことが2つだけある。キャッシュの無効化と、名前付けだ。」
一次データの情報源(Source of Truth)が変更されると、キャッシュされたデータは**古く(Stale)**なります。
- キャッシュエントリは一定時間後に自動的に期限切れになる
- データ変更時に明示的にキャッシュエントリを削除する
- 注意深い調整が必要
- データベースイベントやメッセージキューに基づいてキャッシュをクリアする
- 複雑だが、より正確
Redis は、オープンソースのインメモリデータストアで、一般的に以下として使用されます:
- キャッシュ
- データベース
- メッセージブローカー
高性能なシステムで広く採用されています。
- サブミリ秒の読み書きレイテンシ
- 豊富なデータ構造:
- 文字列 (Strings)
- ハッシュ (Hashes)
- リスト (Lists)
- セット (Sets)
- ソート済みセット (Sorted Sets)
- オプションのディスク永続化
- アトミック操作
- 高速な認証
- サービス間でのセッション状態の共有
- ソート済みセットを使用したリアルタイムランキング
- ゲームや競技プラットフォーム
- カウンター
- ステリーミングメトリクス
- ダッシュボードとモニタリング
- 読み取り頻度が高いワークロード
- 比較的安定したデータ
- コストの高い計算やクエリ
- 書き込み頻度が極端に高いシステム
- 強力かつ即時の整合性が求められる要件
- 無制限または予測不可能なデータセット
- システムの複雑さの増加
- 古いデータのリスク
- 運用上のオーバーヘッド
-
キャッシュはレイテンシを削減する 高速なアクセス、バックエンドの圧力軽減、より良いユーザー体験。
-
適切な戦略を選ぶ キャッシュの種類、配置、無効化戦略はシステム要件に合わせる必要がある。
-
Redisは柔軟性を提供する 機能豊富で、現代のアプリケーション向けにスケーラブル。
-
実験と計測 シンプルに始め、全てを計測し、実際のメトリクスで検証する。
文字列キーをバイトスライスの値にマッピングし、生存時間(TTL)を設定して期限切れ管理を行う基本的なキャッシュパターンです。 Mutex 同期によりスレッドセーフになっており、並行するゴルーチンから安全にアクセスできます。
flowchart TD
A[Get] --> B{エントリ存在?}
B -- No --> C[キャッシュミス]
B -- Yes --> D{期限切れ?}
D -- Yes --> E[エントリ削除]
E --> C
D -- No --> F[値を返す]
C --> G[ソースから取得]
G --> H[TTLを設定]
H --> F
sync.RWMutexで並行アクセスを保護- 各エントリの保持内容:
- 値
- 有効期限のタイムスタンプ
Get操作時の遅延有効期限チェック(Lazy Expiration)- 外部依存なし
分散要件のない単一インスタンスのアプリケーションでの基本的なキャッシュに最適です。
典型的なユースケース:
- 設定データ
- 計算結果
- 予測可能な寿命を持つ API レスポンス
- アクティブなクリーンアップがないとメモリが無制限に増加する可能性がある
- LRU(Least Recently Used)エビクション(追い出し)がない
- 有効期限管理は完全に TTL 値に依存する
software/go_cache_patterns/pattern1_ttl_cache/main.go
Least Recently Used (LRU) ポリシーを使用して、冷たい(使われていない)データを自動的に追い出し、メモリ使用量を予測可能にするキャッシュです。
flowchart TD
A[Get/Put] --> B{キー存在?}
B -- Yes --> C[先頭へ移動]
B -- No --> D[先頭に挿入]
D --> E{容量オーバー?}
E -- Yes --> F[末尾(最古)を削除]
E -- No --> G[完了]
C --> G
-
容量を指定して初期化
- 最大エントリ数を設定
container/list(双方向連結リスト)を使用
-
アクセス順序の追跡
- アクセスされたアイテムをリストの先頭に移動
- 最新性(Recency)の順序を維持
-
オーバーフロー時の追い出し
- 容量に達した場合、リストの末尾から最も使われていないエントリを削除
-
ハッシュマップの維持
- キーをリストノードにマッピング
- O(1) のルックアップ性能を保証
container/list パッケージは以下の操作に対して O(1) を提供します:
- アクセス
- 更新
- 追い出し (Eviction)
これにより、メモリ制約のある環境でもプロダクション利用に耐えうるパターンとなります。
software/go_cache_patterns/pattern2_lru_cache/main.go
本番システムで最も広く展開されているキャッシュパターンです。 多数の同時キャッシュミスがバックエンドを圧倒するキャッシュスタンピードシナリオを防ぐように設計されています。
flowchart TD
A[リクエスト] --> B{キャッシュヒット?}
B -- Yes --> C[キャッシュ値を返す]
B -- No --> D[singleflight.Do]
D --> E[ソースから取得]
E --> F[キャッシュ保存 + TTL]
F --> C
-
キャッシュ確認
- まずキャッシュ層からの読み取りを試みる
-
ソースへのクエリ
- キャッシュミス時、
singleflightを使用してデータベースや API からフェッチする
- キャッシュミス時、
-
キャッシュへの配置
- 取得したデータを適切な TTL と共に保存する
-
結果を返す
- 待機中のすべてのリクエストに結果を提供する
golang.org/x/sync/singleflight パッケージは、重複するリクエストを単一のバックエンド呼び出しにまとめます。
例: 200 のゴルーチンが同時に同じ未キャッシュのキーをリクエストした場合:
- 1つだけがデータベースにアクセスします
- 他の 199 は共有された結果を待ちます
- 200 の同時データベースクエリ
- バックエンドの過負荷
- レスポンスタイムの急増
- 連鎖的なサービス障害
- 1 つのデータベースクエリ
- 安定したレスポンスタイム
- トラフィックスパイクの優雅な処理
software/go_cache_patterns/pattern3_cache_aside_singleflight/main.go
Redis は業界標準の分散キャッシュソリューションです。 複数のアプリケーションインスタンスが単一のキャッシュを共有し、水平スケーリングと状態共有を可能にします。
flowchart TD
subgraph Apps
A1[Service Instance A]
A2[Service Instance B]
end
A1 --> R[(Redis)]
A2 --> R
A1 -->|cache miss| DB[(DB/API)]
DB --> A1
A1 -->|set JSON + TTL| R
- Go の構造体を JSON にマーシャル(変換)
- 人間が読める形式
redis-cliでデバッグ可能
- Redis が有効期限を自動的に処理
- 書き込み時に TTL を設定
- 手動クリーンアップ不要
- 全てのアプリケーションインスタンスが同じキャッシュにアクセス
- 以下の用途に最適:
- マイクロサービス
- ロードバランスされたデプロイメント
- セッション整合性
go-redis/redis/v8 は以下を提供します:
- Go らしい(Idiomatic な)API
- コネクションプーリング
- パイプライン処理
- クラスタサポート
本番利用に必要なラッパーコードは最小限で済みます。
software/go_cache_patterns/pattern4_redis_cache/main.go
書き込みごとにデータベースとキャッシュの両方を同期的に更新します。 このパターンは書き込み性能よりも整合性を優先します。
flowchart TD
W[書き込みリクエスト] --> A[App]
A --> D[(Database)]
A --> C[(Cache)]
R[読み取りリクエスト] --> A2[App]
A2 --> C2{キャッシュヒット?}
C2 -- Yes --> R2[キャッシュ値を返す]
C2 -- No --> D2[(Database)]
D2 --> C3[キャッシュ保存]
C3 --> R2
-
書き込みリクエスト受信
- アプリケーションがクライアントからデータ更新を受け取る
-
データベース更新
- プライマリデータストアに変更を永続化
-
キャッシュ更新
- 新しいデータで即座にキャッシュを同期
-
成功確認
- 両方の操作が完了した後にのみ成功を返す
- キャッシュの整合性が保証される
- 書き込み後の読み取りで古いデータ(Stale Read)が発生しない
- データ状態のメンタルモデルがシンプル
- 書き込み操作が遅くなる
- キャッシュが利用可能である必要がある
- 書き込みレイテンシの増加
software/go_cache_patterns/pattern5_write_through/main.go