🔖

キャッシュスタンピードの対策

2025/02/03に公開

Daily Blogging44日目

スタンピードって言われるとワンピースの映画が頭に浮かぶ

キャッシュスタンピードって何?

webアプリケーションなどで、apiの実行結果をキャッシュに格納しておくことで
毎回DBへのアクセスが走って負荷がかからないようにする手法がある。
ただし、キャッシュが切れたタイミングでリクエストが集中した時に高負荷がかかってしまうことがある。
これがキャッシュスタンピード

キャッシュスタンピードを解決する方法はいくつかあるみたいだが、今回は排他制御による解決法をまとめてく

セマフォで排他制御

キャッシュの排他制御はセマフォと呼ばれる仕組みで実現できる

セマフォはリソースへのアクセス制御を行い、複数のプロセスやスレッドが同時に同じリソースにアクセスして問題を起こさないようにします。

Redis+Railsだと、setメソッドにnxとexの引数を渡すことで実現できる。
nx: キーが存在しない場合にのみキーを新しくセットする
ex: キーの有効期限

セマフォキーによるロックをかけることで、最初のリクエストだけがDBにアクセスしてキャッシュデータを生成する。
他のリクエストはDBアクセスできない状態になる。

class SemaphoreService
  LOCK_KEY = 'semaphore_lock'
  LOCK_TIMEOUT = 10 # ロックの有効期限(秒)

  def self.perform_task
    # ロックを取得
    lock_acquired = Redis.current.set(LOCK_KEY, 'locked', nx: true, ex: LOCK_TIMEOUT)

    if lock_acquired
      begin
        # ロック取得成功
        puts "リソースを使用中..."
      ensure
        # ロックを解放
        # 処理が途中で失敗した時に解放されないことがあるので明示的に解放する
        Redis.current.del(LOCK_KEY)
        puts "ロック解放"
      end
    else
      # ロック取得失敗
      puts "ロック取得失敗"
    end
  end
end

これだとロックを取得できなかったリクエストがただ失敗するだけなので、必要に応じて再度キャッシュにアクセスするようにする必要がある。

class SemaphoreService
  LOCK_KEY = 'semaphore_lock'
  LOCK_TIMEOUT = 10 # ロックの有効期限(秒)
  MAX_RETRIES = 5  # 最大再試行回数

  def self.perform_task
    retries = 0
    loop do
      # ロックを取得
      lock_acquired = Redis.current.set(LOCK_KEY, 'locked', nx: true, ex: LOCK_TIMEOUT)

      if lock_acquired
        begin
          # ロック取得成功
          puts "リソースを使用中..."
          # 実際の処理をここに記述
        ensure
          # ロックを解放
          Redis.current.del(LOCK_KEY)
          puts "ロック解放"
        end
        break  # 処理が完了したのでループを抜ける
      else
        # ロック取得失敗
        puts "ロック取得失敗"
        retries += 1
        if retries >= MAX_RETRIES
          puts "最大再試行回数に達しました"
          break
        end
        # ロック取得失敗の場合、待機して再試行
        sleep(0.1)  # 少し待ってから再試行
      end
    end
  end
end

これでキャッシュが切れた時にDBに高負荷がかかることもなくなる

Discussion