Carpe Diem

備忘録

ネガティブキャッシュによるデータベース負荷対策

概要

通常データベースへの負荷を軽減するためにキャッシュレイヤを導入します。

しかしよくあるのは正常系のみキャッシュしており、

  • ゲームのイベント前でまだ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
  }
}

ref: https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_backend_bucket.html

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つの例としてネガティブキャッシュを紹介しました。