
들어가며.
캐시 만료 시점에 모든 요청이 DB를 찌르게되어 부하가 발생하는 문제인 Cache Stampede는 개발을 할 때 주의해야하는 사항 중 하나이다.
관련 내용은 아래 글에 잘 나와있다.
캐시 문제 해결 가이드 - DB 과부하 방지 실전 팁
대용량 트래픽 환경에서 캐시를 사용할 때 주의해야할 위험 상황과 예방법을 소개합니다.
toss.tech
Cache Stampede를 해결하는 방식 중 분산락을 사용하는 방식이 있고, 그 중 가장 쉽게 생각할 수 있는 방식은 다음과 같다.
- 캐시가 만료되면 캐시 갱신에 대한 Lock을 획득한 요청이 캐시를 갱신하고 다른 요청들은 대기하는 구조로 만든다.
- Lock을 획득한 요청이 캐시를 갱신하면 다른 요청들이 캐시를 읽는다.
- 하지만 이렇게 만들게 되면 Lock을 획득한 요청이 캐시를 갱신시킬 때까지 다른 요청들은 대기하는 비용이 생긴다.
이러한 비용을 줄이기 위해 데이터 갱신을 실제 캐시 만료 시간보다 이전에 해서 Lock을 획득하지 못한 요청들은 아직 만료되지 않은 캐시에서 데이터를 들고가도록 하게끔 만들면 된다.
즉, 갱신을 위한 만료 시간(Soft TTL)과 실제 캐시 만료 시간(Hard TTL)을 구분하여 갱신을 수행하는 동안에도 캐시는 만료되지 않은 상태로 둔다. 이렇게 되면 Lock을 획득하지 못한 요청이 대기하지 않고 캐시에서 값을 읽어갈 수 있게 된다.
플로우
구현
@Service
class MemberService(
private val memberRepository: MemberRepository
) {
@TwoLevelTtlCacheable(
cacheName = "member",
key = "#name",
softTtlSeconds = 3,
hardTtlSeconds = 5,
)
fun findByName(name: String): Member? {
return memberRepository.findByName(name)
}
}
위와 같이 사용할 수 있도록 AOP를 이용해보자.
대략적인 코드는 아래와 같다.
@Aspect
@Component
class TwoLevelTtlAspect(
private val redisCacheService: RedisCacheService
) {
@Around("@annotation(TwoLevelTtlCacheable)")
fun around(pjp: ProceedingJoinPoint): Any? {
val cachedData = redisCacheService.getCachedData(redisKey)
// 1. 캐시 미스
if (cachedData == null) {
return fillCacheAndReturn(...)
}
// 2. Soft TTL 체크
if (now > cachedData.softTtl) {
val tryLock = redisCacheService.tryLock(lockKey, 3)
return if (tryLock) {
// 3. 락 획득 성공 - 캐시 갱신
fillCacheAndReturn(...)
} else {
// 4. 락 획득 실패 - 기존 데이터 반환
objectMapper.readValue(cachedData.data, returnType)
}
}
// 5. 캐시 히트
return objectMapper.readValue(cachedData.data, returnType)
}
}
구현 코드 링크
GitHub - JwahoonKim/two-level-ttl-cache: 🌵 Cache Stampede 문제를 해결하기 위한 2-Level-TTL-Cache
🌵 Cache Stampede 문제를 해결하기 위한 2-Level-TTL-Cache. Contribute to JwahoonKim/two-level-ttl-cache development by creating an account on GitHub.
github.com
'Spring' 카테고리의 다른 글
스프링 부트는 어떻게 라이브러리 버전 관리를 자동으로 하지? (1) | 2024.12.12 |
---|---|
Private Method를 테스트를 하고싶으면 어떡하지? (2) | 2024.12.04 |
[Spring] 동시성 이슈 해결하는 방법 찍먹하기 (1) | 2023.07.22 |
[Spring & Java] 왜 난 개발할 때 Runtime Exception을 써왔지? (3) | 2023.05.16 |
[Spring] 의존성 주입과 의존성 주입 방법에 대하여. (0) | 2023.04.30 |