概要
通常データベースへの負荷を軽減するためにキャッシュレイヤを導入します。
しかしよくあるのは正常系のみキャッシュしており、
- ゲームのイベント前でまだ404
- ゲームのイベント期間が終了して404
- 動画の配信前でまだ404
- 動画の配信期間が終了して404
といった異常系が考慮されていないパターンです。
サーバ上はデータの最新性から↑が正しいのですが、クライアントサイドにどこかしら導線が存在し、リクエストを送ってしまい、それがキャッシュレイヤを通過しどんどんDBまで貫通してしまうようなケースがあります。
このような課題においてはネガティブキャッシュが有効です。
ネガティブキャッシュとは
ネガティブキャッシュとは3xx, 4xx, 5xxといった正常系ではない時にキャッシュする手法です。
たまに404が出るようなケースでは不要ですが、前述のようなケースでは大量のユーザが同じタイミングでリクエストをするため、DBに貫通すると大きな負荷になってしまうので導入を検討すべきです。
実装例
CDNでの実装
手軽に導入できるのはCDNです。
今回はCloudCDNの例を紹介します。
CloudCDN
a. Webコンソール
Webコンソールでポチポチ作成する場合、デフォルトで有効化されています。

デフォルト値は次の通りです。

ref: https://docs.cloud.google.com/cdn/docs/using-negative-caching?hl=ja
b. Terraform
terraformではcdn_policyで有効化できます。
resource "google_compute_backend_bucket" "default" { name = "cat-backend-bucket" description = "Contains beautiful images" bucket_name = google_storage_bucket.default.name enable_cdn = true cdn_policy { cache_mode = "CACHE_ALL_STATIC" client_ttl = 3600 default_ttl = 3600 max_ttl = 86400 negative_caching = true serve_while_stale = 86400 } }
c. Kubernetes
KubernetesだとBackendConfigを以下のように設定します。
apiVersion: cloud.google.com/v1 kind: BackendConfig metadata: name: my-backendconfig spec: cdn: enabled: true cachePolicy: includeHost: true includeProtocol: true includeQueryString: true negativeCaching: true negativeCachingPolicy: code: 404 ttl: 15
ref: https://docs.cloud.google.com/kubernetes-engine/docs/how-to/ingress-configuration?hl=ja#cloud_cdn
アプリケーションレイヤでの実装
アプリケーションレイヤにおいては
- 複数取得の際に一部だけデータがない
といったユースケースで有効です。
Go実装
package main import ( "fmt" "log" "time" "github.com/patrickmn/go-cache" ) // CacheItem はキャッシュするデータの構造 type UserProfile struct { Name string } // ユーザーが見つからない状態も含めたラッパー type CachedResult struct { Data *UserProfile Exists bool } var c = cache.New(5*time.Minute, 10*time.Minute) func GetUserProfile(userID string) (*UserProfile, error) { // 1. キャッシュ確認 if val, found := c.Get(userID); found { result := val.(CachedResult) if !result.Exists { return nil, fmt.Errorf("user not found (cached)") } return result.Data, nil } // 2. データベース等への問い合わせ(シミュレーション) user, err := fetchFromDB(userID) if err != nil { // 3. ネガティブキャッシュの保存 (短いTTLで保存) c.Set(userID, CachedResult{Data: nil, Exists: false}, 1*time.Minute) return nil, err } // 4. 通常のキャッシュ保存 c.Set(userID, CachedResult{Data: user, Exists: true}, 5*time.Minute) return user, nil } func fetchFromDB(id string) (*UserProfile, error) { time.Sleep(3 * time.Second) return nil, fmt.Errorf("not found") } func main() { log.Println("Get 1st") user, err := GetUserProfile("user_123") fmt.Println(user, err) log.Println("Get 1st done") log.Println("Get 2nd") user2, err2 := GetUserProfile("user_123") fmt.Println(user2, err2) log.Println("Get 2nd done") }
動作確認
次のように、DB取得後にネガティブキャッシュから返るようになりました。
$ go run main.go 2025/12/23 09:33:36 Get 1st <nil> not found 2025/12/23 09:33:39 Get 1st done 2025/12/23 09:33:39 Get 2nd <nil> user not found (cached) 2025/12/23 09:33:39 Get 2nd done
まとめ
負荷対策の1つの例としてネガティブキャッシュを紹介しました。