๋ณธ๋ฌธ ๋ฐ”๋กœ๊ฐ€๊ธฐ

database

[Anifriends] Redis ์บ์‹œ์—์„œ ๋ฐœ์ƒํ•œ ๋™์‹œ์„ฑ ์ด์Šˆ ํ•ด๊ฒฐ๊ธฐ

๐Ÿš€  ๊ฐœ์š”

๋ฐ๋ธŒ์ฝ”์Šค 4๊ธฐ์—์„œ ๋ด‰์‚ฌ ํ”Œ๋žซํผ์ธ Anifriends ์—์„œ ๋ณดํ˜ธ ๋™๋ฌผ ์กฐํšŒ ์„ฑ๋Šฅ์„ ๋†’์ด๊ธฐ ์œ„ํ•ด ์ฒซ ํŽ˜์ด์ง€์™€ ์ด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜๋ฅผ ์บ์‹œํ•˜์˜€๋‹ค. ๊ฐœ๋ฐœํ•œ ์ง€ 3๊ฐœ์›” ์ง€๋‚ฌ์ง€๋งŒ, Redis์™€ ๋™์‹œ์„ฑ์— ๋Œ€ํ•ด ๊ณต๋ถ€๋ฅผ ๋” ํ•˜๊ณ  ๋‹ค์‹œ ์ฝ”๋“œ๋ฅผ ๋ณด๋‹ˆ ๋™์‹œ์„ฑ ๋ฌธ์ œ๊ฐ€ ์žˆ์„ ๋งŒํ•œ ์ฝ”๋“œ๊ฐ€ ๋ณด์—ฌ์„œ ์ˆ˜์ •์„ ํ•˜๋ ค๊ณ  ํ•œ๋‹ค.

์บ์‹œ ์ž๋ฃŒ ๊ตฌ์กฐ

  • ๋ณดํ˜ธ ๋™๋ฌผ ์ฒซ ํŽ˜์ด์ง€: ๋ณดํ˜ธ ๋™๋ฌผ ๋ชฉ๋ก์€ ์ƒ์„ฑ ์‹œ๊ฐ„ ๊ธฐ์ค€ ๋‚ด๋ฆผ ์ฐจ์ˆœ์ด๊ณ , Redis ์˜ ZSet(Sorted Sets)์„ ์‚ฌ์šฉํ•˜์˜€๋‹ค. ZSet ์€ score ๊ธฐ์ค€ ์˜ค๋ฆ„์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜๋ฏ€๋กœ, score ์„ -created_at ๋กœ ์„ค์ •ํ•˜์—ฌ ์ƒ์„ฑ ์‹œ๊ฐ„ ๊ธฐ์ค€์œผ๋กœ ๋‚ด๋ฆผ ์ฐจ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์บ์‹œํ•˜์˜€๋‹ค.
  • ๋ณดํ˜ธ ๋™๋ฌผ ์ด ์ˆ˜: Redis ์˜ Strings๋ฅผ ์‚ฌ์šฉํ•˜์˜€๋‹ค.

์บ์‹œ ์ „๋žต

์ฝ๊ธฐ ์ „๋žต: Look-Aside

  • ์บ์‹œ์— ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜๋ฉด ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ์‘๋‹ต
  • ์บ์‹œ์— ๋ฐ์ดํ„ฐ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฉด DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•˜๊ณ  ์บ์‹ฑ ํ›„ ์‘๋‹ต
  • Cache Warming ์„ ํ†ตํ•ด ์–ดํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋„์›Œ์งˆ ๋•Œ ์ฒซ ํŽ˜์ด์ง€ ์บ์‹œ

์“ฐ๊ธฐ ์ „๋žต: Write Through

  • ์ƒ์„ฑ: ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ ์บ์‹œ, ์ด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ์ฆ๊ฐ€
  • ์ˆ˜์ •: ์บ์‹œ ๋Œ€์ƒ์ผ ๊ฒฝ์šฐ ํ•ด๋‹น ๋ฐ์ดํ„ฐ ์‚ญ์ œ ํ›„ ์žฌ์บ์‹œ
  • ์‚ญ์ œ: ์บ์‹œ ๋Œ€์ƒ์ผ ๊ฒฝ์šฐ ํ•ด๋‹น ๋ฐ์ดํ„ฐ ์‚ญ์ œ, ์ด ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ๊ฐ์†Œ

๐Ÿšจ ๋ฌธ์ œ 1. ์บ์‹œ ํฌ๊ธฐ๋ฅผ ์œ ์ง€ํ•˜๊ธฐ ์œ„ํ•œ ๋กœ์ง์—์„œ ๋™์‹œ์„ฑ ๋ฌธ์ œ ๋ฐœ์ƒ

์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๊ฐ€ ์ถ”๊ฐ€๋  ๊ฒฝ์šฐ, ์บ์‹œ์˜ ํฌ๊ธฐ๋ฅผ ์กฐํšŒํ•˜์—ฌ ์ตœ๋Œ€ ํฌ๊ธฐ๋ฅผ ์ดˆ๊ณผํ•œ๋‹ค๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์ด ZSet ์˜ ZREMRANGEBYSCORE ์ปค๋งจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•œ๋‹ค.

private void trimCache() {
    if (zSetOperations.size(ANIMAL_ZSET_KEY) > ANIMAL_CACHE_SIZE) {
        zSetOperations.removeRange(ANIMAL_ZSET_KEY, LAST_INDEX, LAST_INDEX);
    }
}

์บ์‹œ์˜ ์ตœ๋Œ€ ํฌ๊ธฐ๊ฐ€ 30์ด๊ณ  ๋™์‹œ์— 50๊ฐœ์˜ ๋ณดํ˜ธ ๋™๋ฌผ ์ƒ์„ฑ ์š”์ฒญ์ด ๋ฐœ์ƒํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์˜€๋‹ค. ์ •์ƒ์ ์ธ ๋กœ์ง์—์„œ๋Š” 30๊ฐœ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์บ์‹œ ๋˜์–ด์•ผ ํ•œ๋‹ค.

@Test
@DisplayName("์„ฑ๊ณต: ๋™์‹œ์— ๋ณดํ˜ธ ๋™๋ฌผ ์ถ”๊ฐ€")
void saveAnimal() throws InterruptedException {
    // given
    int count = 50;
    int cacheSize = 30;
    Shelter shelter = shelter();
    List<Animal> animals = animals(shelter, count);
    long id = 1;
    for (Animal animal : animals) {
        ReflectionTestUtils.setField(animal, "animalId", id++);
        ReflectionTestUtils.setField(animal, "createdAt", LocalDateTime.now());
    }

    // when
    ExecutorService executorService = Executors.newFixedThreadPool(count);
    CountDownLatch latch = new CountDownLatch(count);

    // when
    for (int i = 0; i < count; i++) {
        int finalI = i;
        executorService.submit(() -> {
            try {
                animalRedisRepository.saveAnimal(animals.get(finalI));
            } finally {
                latch.countDown();
            }
        });
    }

    latch.await();

    // then
    assertThat(redisTemplate.opsForZSet().range("animal:animals", 0, -1))
        .hasSize(cacheSize);
}

์—ญ์‹œ๋‚˜ ๋ฐ์ดํ„ฐ๊ฐ€ ์ œ๋Œ€๋กœ ์บ์‹œ ๋˜์ง€ ์•Š์•˜๋‹ค. ์‹ฌ์ง€์–ด ๋‹จ ํ•œ ๊ฐœ์˜ ๋ฐ์ดํ„ฐ๋„ ์บ์‹œ๋˜์ง€ ์•Š์•˜๋‹ค. 50๊ฐœ์˜ ํŠธ๋žœ์žญ์…˜์—์„œ ์บ์‹œ์˜ ์ตœ๋Œ€ ํฌ๊ธฐ๋ฅผ ์ดˆ๊ณผํ–ˆ๋‹ค๊ณ  ํŒ๋‹จํ•˜๋ฉฐ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

ZSet ์˜ ZREMRANGEBYSCORE ์ปค๋งจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋งˆ์ง€๋ง‰ ๋ฐ์ดํ„ฐ๊ฐ€ ์•„๋‹Œ ์บ์‹œ์˜ ์ตœ๋Œ€ ๊ฐœ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ญ์ œํ•˜๋„๋ก ์ˆ˜์ •ํ•˜์˜€๋‹ค.

private void trimCache() {
    zSetOperations.removeRange(ANIMAL_ZSET_KEY, ANIMAL_CACHE_SIZE, LAST_INDEX);
}

๐Ÿšจ ๋ฌธ์ œ 2. ๋ฐ์ดํ„ฐ ๊ฐœ์ˆ˜ ์ฆ๊ฐ ๋กœ์ง์—์„œ ๊ฐฑ์‹  ์†์‹ค ๋ฐœ์ƒ

๋ฐ์ดํ„ฐ ์ถ”๊ฐ€๋˜๊ฑฐ๋‚˜ ์‚ญ์ œ๋  ๋•Œ ์บ์‹œ๋œ ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•˜์—ฌ (ํ˜„์žฌ ๊ฐœ์ˆ˜ + 1)์„ ํ•˜์—ฌ ์ €์žฅํ•œ๋‹ค.

@Override
public void increaseTotalNumberOfAnimals() {
    Long cachedCount = getTotalNumberOfAnimals();
    valueOperations.set(TOTAL_NUMBER_OF_ANIMALS_KEY, cachedCount + 1);
}

๋™์‹œ์— 10๊ฐœ์˜ ๋ณดํ˜ธ ๋™๋ฌผ ์ƒ์„ฑ ์š”์ฒญ์ด ๋ฐœ์ƒํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜์˜€๋‹ค. ์ •์ƒ์ ์ธ ๋กœ์ง์—์„œ๋Š” 10๊ฐœ๋กœ ์บ์‹œ ๋˜์–ด์•ผ ํ•œ๋‹ค.

    @Test
    @DisplayName("์„ฑ๊ณต: ๋™์‹œ์— ์ด ๋™๋ฌผ ์ˆ˜ ์ฆ๊ฐ€")
    void increaseTotalNumberOfAnimals() throws InterruptedException {
        // given
        int count = 10;

        // when
        ExecutorService executorService = Executors.newFixedThreadPool(count);
        CountDownLatch latch = new CountDownLatch(count);

        // when
        for (int i = 0; i < count; i++) {
            executorService.submit(() -> {
                try {
                    animalRedisRepository.increaseTotalNumberOfAnimals();
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();

        // then
        assertThat(animalRedisRepository.getTotalNumberOfAnimals()).isEqualTo(count);
    }

๊ฒฐ๊ณผ๋Š” 10๊ฐœ๊ฐ€ ์•„๋‹Œ 1๊ฐœ์˜€๋‹ค. ์—ฌ๋Ÿฌ ํŠธ๋žœ์žญ์…˜์—์„œ ๋™์ผํ•œ ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•˜๊ณ  ๊ฐฑ์‹ ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

ZSet ์˜ INCR ์ปค๋งจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์›์ž์ ์œผ๋กœ ๊ฐœ์ˆ˜๊ฐ€ ์ฆ๊ฐ€๋˜๋„๋ก ์ˆ˜์ •ํ•˜์˜€๋‹ค.

public void increaseTotalNumberOfAnimals() {
    valueOperations.increment(TOTAL_NUMBER_OF_ANIMALS_KEY);
}

๊ฐ์†Œํ•  ๋•Œ๋„ ๋™์ผํ•œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๊ณ , DECR ์ปค๋งจ๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ•ด๊ฒฐํ•˜์˜€๋‹ค.