🔖
キャッシュスタンピードの対策
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