캐시 침투 문제 해결: 뮤텍스 잠금을 이용한 상점 조회 시스템

캐시 침투란 무엇인가?

  • 캐시 침투 문제는 핫키 문제로도 불립니다.
  • 높은 동시성으로 접근되며 캐시 재생성 비즈니스가 복잡한 키가 갑자기 만료되는 경우를 말합니다.
  • 무수한 요청이 순간적으로 데이터베이스에 엄청난 부하를 줍니다.

상점 조회 + 뮤텍스 잠금 로직

public Shop queryShopWithMutex(Long id) 메서드 로직

1. 먼저 Redis에서 키로 조회

String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

2. 데이터가 있으면 상점 객체를 바로 반환

if (StrUtil.isNotBlank(shopJson)) {
    Shop shop = JSONUtil.toBean(shopJson, Shop.class);
    return shop;
}

3. 빈 문자열이 조회되면 실패 반환

if (shopJson != null) {
    return null;
}

4. 뮤텍스 잠금을 획득하고 데이터베이스 조회

4.0 잠금 구현
/*
 * 아래는 핫키 고동시성 접근에서 발생하는 캐시 침투 문제 해결을 위한 코드입니다.
 * Redis 기반 분산 잠금 메커니즘을 구현하며, 잠금 획득과 해제 로직을 포함합니다.
 */

// 1. 잠금 획득 로직
private boolean tryLock(String key) {
    Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

// 2. 잠금 해제
private void unlock(String key) {
    stringRedisTemplate.delete(key);
}
4.1 잠금 획득 시도
boolean flag = tryLock(LOCK_SHOP_KEY + id);

while (!flag) {
    Thread.sleep(50);
    return queryShopWithMutex(id);
}

5. 데이터베이스에도 없으면 Redis에 빈 문자열 저장

if (shop == null) {
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
    return null;
}

6. 데이터베이스에 있으면 Redis 재건

String jsonStr = JSONUtil.toJsonStr(shop);
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, CACHE_SHOP_TTL, TimeUnit.MINUTES);

7. 잠금 해제

unlock(LOCK_SHOP_KEY + id);

전체 예제 코드

/*
 * 캐시 침투 해결 방법 1: 뮤텍스 잠금
 * "뮤텍스 잠금"을 도입하여 데이터베이스 조회와 캐시 재건을 수행하는 스레드가 하나만 되도록 보장합니다.
 * 다른 스레드는 대기하거나 재시도합니다.
 */
public Shop queryShopWithMutex(Long id) {
    // 1. 먼저 Redis에서 조회
    String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    
    /*
     * 2. 캐시 히트 여부 판단
     * 2.1 캐시 히트 시
     * 2.1.1 shopJson이 비어있지 않다면(null, "", 전체 공백 제외), 캐시에 데이터가 있는 것입니다.
     * JSON 문자열을 Shop 객체로 변환하여 바로 반환하며 데이터베이스 접근이 필요 없습니다.
     */
    if (StrUtil.isNotBlank(shopJson)) {
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return shop;
    }

    /*
     * 2.1.2 빈 문자열 ""이 조회되면, 이는 우리가 캐시한 빈 데이터입니다.
     * 이전에 데이터베이스에서 이 데이터를 찾지 못했기 때문에 빈 문자열을 캐시에 저장했습니다.
     * 이렇게 하면 후속 요청이 이 빈 문자열을 찾아 데이터베이스 접근을 피하게 되어 캐시 침투를 방지합니다.
     */
    if (shopJson != null) {
        return null;
    }
    
    /*
     * 2.2 캐시 미스 시, 캐시 재건을 위한 잠금 시작
     * 고동시성 환경에서 캐시 재건 구현
     * tryLock()을 호출하여 뮤텍스 잠금 획득 시도 (보통 Redis의 SETNX 구현)
     */

    Shop shop = null;
    try {
        // 뮤텍스 잠금 획득
        boolean flag = tryLock(LOCK_SHOP_KEY + id);
        /*
         * 2.2.1 잠금 획득 실패 시 대기 후 재시도 (스핀)
         * 잠금을 얻지 못하면 다른 스레드가 캐시를 재건 중인 것입니다.
         * 현재 스레드는 50밀리초 대기 후 자기 자신을 재귀적으로 호출하여 다시 캐시 조회를 시도합니다.
         * 이 과정을 "스핀 잠금" 또는 "지연 재시도"라고 합니다.
         */
        while (!flag) {
            Thread.sleep(50);
            return queryShopWithMutex(id);
        }
        // 2.2.2 획득 성공 -> 데이터베이스 읽기, 캐시 재건
        // 2.2.2.1 조회되지 않으면, 빈 값을 Redis에 쓰기
        shop = getById(id);
        if (shop == null) {
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        /*
         * 2.2.2.2 데이터베이스에 데이터가 있으면 캐시에 쓰기
         * 조회되면 JSON 문자열로 변환
         */
        String jsonStr = JSONUtil.toJsonStr(shop);
        // Redis에 저장하고 TTL 설정
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, jsonStr, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        // 최종적으로 조회된 상점 정보를 프론트엔드에 반환
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        // 2.2.3 마지막: 잠금 해제
        unlock(LOCK_SHOP_KEY + id);
    }
    return shop;
}

태그: Redis 캐시관리 분산시스템 뮤텍스잠금 상점시스템

5월 21일 07:55에 게시됨