설정을 DB로 옮겼는데 다른 워커는 옛 값을 쓴다 — 프로세스별 캐시는 버전 번호로 무효화한다
Django 기본 캐시(LocMem)는 워커마다 따로라 한 곳에서 저장해도 다른 워커와 스케줄러는 모릅니다. 저장할 때 버전 한 줄을 올리고, 읽을 때 그 한 줄만 짧은 간격으로 확인합니다.
운영 설정을 .env에서 DB로 옮기고 관리 화면에서 바꿀 수 있게 했습니다. 읽을 때마다 DB를 치기는 아까워 캐시에 담았는데, 화면에서 값을 바꾼 뒤에도 일부 요청과 배경 스케줄러는 옛 값으로 계속 동작합니다. 같은 요청을 몇 번 보내면 새 값과 옛 값이 번갈아 나옵니다.
원인은 캐시가 프로세스 메모리에 있다는 것입니다. Django의 기본 캐시 백엔드(LocMem)는 프로세스마다 따로 있습니다. gunicorn 워커가 둘이고 스케줄러가 하나면 캐시도 셋입니다. 저장한 워커는 자기 캐시를 비우지만, 나머지 둘은 저장이 있었다는 사실을 알 길이 없습니다.
해법은 프로세스끼리 이미 함께 보는 곳(DB나 Redis)에 버전 번호 하나를 두는 것입니다. 저장·지우기는 같은 트랜잭션에서 버전을 F() + 1로 올립니다. 읽는 쪽은 값 전체가 아니라 버전 한 줄만 짧은 간격(예: 1초)으로 확인하고, 바뀌었을 때만 전체를 다시 읽습니다. 확인 비용은 간격마다 기본 키 조회 한 번입니다.
import time
from django.db.models import F
_cache = {"version": None, "values": {}, "checked_at": 0.0}
CHECK_SECONDS = 1.0
def get(name, default=None):
now = time.monotonic()
if now - _cache["checked_at"] >= CHECK_SECONDS:
v = SettingsVersion.objects.values_list("n", flat=True).get(pk=1)
if v != _cache["version"]:
_cache["values"] = dict(Setting.objects.values_list("name", "value"))
_cache["version"] = v
_cache["checked_at"] = now
return _cache["values"].get(name, default)
def save(name, value):
with transaction.atomic():
Setting.objects.update_or_create(name=name, defaults={"value": value})
SettingsVersion.objects.filter(pk=1).update(n=F("n") + 1)
_cache["checked_at"] = 0.0 # 저장한 프로세스는 바로 다시 읽게
값 자체는 공용 캐시에 올리지 않고 버전만 공유하면, 설정에 API 키 같은 비밀이 섞여 있어도 Redis에 평문이 앉지 않습니다. 시험은 캐시를 공유하지 않는 두 번째 인스턴스를 만들고 시계를 고정해, 「간격 안에서는 옛 값, 간격이 지나면 새 값」을 확인합니다.