개발놀이터

Redis와 Memcached 동시성 문제 본문

CS 지식/데이터베이스

Redis와 Memcached 동시성 문제

마늘냄새폴폴 2024. 1. 14. 21:40

이번 포스팅에선 Redis와 Memcached의 동시성 문제에 대해서 궁금증이 생겨서 공유하고자 글을 쓰게 되었습니다. 

 

구글링하다 Redis-Semaphore로 Mutex 해결하기 라는 제목의 포스팅을 발견했습니다. 처음엔 보고 "으잉? 이게 무슨말이야?" 하게 됐는데 Mutex는 동시성 문제를 해결하는 방법론인데 Mutex를 해결..? 

 

아마 포스팅 쓰신 글쓴이분께서 Mutex의 뜻을 착각하고 계신게 아닌가 싶었습니다. 그래서 그 부분은 넘어가도록 하고 Redis-Semaphore라는게 걸려서 이번에도 역시 GPT로 공부해봤습니다. 

 

Redis와 Memcached의 차이

우선 이 둘의 차이부터 짚고 넘어가도록 하겠습니다. 

 

Redis

  • 싱글스레드이다.
  • 여러가지 자료형을 제공한다. String, Hash, List, Sorted List 등등..
  • 배포 과정에서 레플리케이션 방식인 Santinel과 샤딩 방식인 Cluster를 제공해준다. 
  • Spring Data Redis로 스프링에서 사용하기 편하게 high-level의 구현체를 만들어뒀습니다. 

Memcached

  • 멀티스레드이다.
  • String 자료형 하나만 제공해준다. 
  • 배포 과정에서 레플리케이션 방식만 제공해준다. 
  • 스프링에서 구현체를 만들어주지 않았다. (만들어주세요 제발 ㅜㅜ)

 

Redis와 Memcached의 동시성문제

Memcached는 멀티스레드이기 때문에 동시성 문제가 발생하는 것이 어떻게 보면 자명해보이지만 Redis는 싱글스레드인데 동시성 문제가 발생한다는 것이 쉽게 떠오르지 않았습니다. 

 

Redis에서 어떤 동시성 문제가 있을까요? 

 

만약 여러명의 사용자가 동시에 Redis의 데이터에 접근하려고 하는 상황을 가정해볼 수 있습니다. 

 

그리고 이 데이터의 값을 변경하려고 한다면 어떻게 될까요? 

 

10이라는 데이터를 두명이 동시에 접근해서 1을 올리려고 합니다. 싱글스레드이기 때문에 한번에 하나의 연산만을 보장하는 원자성때문에 둘 다 11을 리턴받을 것입니다. 우리가 예상한 12가 아니구요. 

 

이 문제는 운영체제에서 흔히 말하는 race condition 이라고 부르는 동시성 문제 상황입니다. 

 

Redis에서 동시성 문제를 해결하는 방법

Spring Data JPA에선 비관적 락과 낙관적 락을 @Lock, @Version 이 두개의 어노테이션을 통해 제공해줍니다. JPA의 락은 굉장히 사용하기 편하다는 느낌을 받지만...

 

Redis는 자체적으로 락킹매커니즘을 가지고 있지 않습니다. 우리가 직접 구현해줘야하죠!

 

Redis의 동시성 문제를 막기위한 방법은 어느정도 메뉴얼화 되어있습니다. 보통 낙관적 락으로 동시성 문제를 해결한다고 하네요. 

 

예시를 한번 보겠습니다. 

 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.SessionCallback;
import org.springframework.stereotype.Service;

@Service
public class YourEntityService {

    @Autowired
    private RedisTemplate<String, YourEntity> redisTemplate;

    public boolean updateEntityOptimistically(String key, YourEntity updatedEntity) {
        return redisTemplate.execute(new SessionCallback<Boolean>() {
            @SuppressWarnings("unchecked")
            @Override
            public <K, V> Boolean execute(RedisOperations<K, V> operations) {
                operations.watch((K) key);
                YourEntity currentEntity = (YourEntity) operations.opsForValue().get((K) key);

                // 엔티티의 업데이트가 필요한지 여부를 확인하는 로직
                if (currentEntity == null || !shouldUpdate(currentEntity, updatedEntity)) {
                    operations.unwatch();
                    return false;
                }

                operations.multi();
                operations.opsForValue().set((K) key, updatedEntity);
                List<Object> result = operations.exec();

                return result != null && !result.isEmpty();
            }
        });
    }

    private boolean shouldUpdate(YourEntity currentEntity, YourEntity updatedEntity) {
        // 우리의 로직을 업데이트할 것인지 하지 않을 것인지 결정하는 로직입니다.
        return true;
    }
}

 

주의해서 봐야할 키워드는 watch, multi, exec입니다. 

 

watch는 JPA의 낙관적 락에서 @Version에 해당합니다. 

 

JPA의 비관적 락과 낙관적 락에 대해서 자세히 알고싶으신 분들은 아래의 링크를 확인해주세요!

https://coding-review.tistory.com/421

 

JPA의 동시성 컨트롤 (낙관적 락, 비관적 락)

이번 포스팅에선 JPA가 어떻게 동시성 문제를 컨트롤하는지에 대해서 포스팅해보겠습니다. 개인적으로 동시성 문제는 애플리케이션에서 일어날 수 있는 가장 최악의 버그라고 생각합니다. 그

coding-review.tistory.com

 

watch로 version을 확인합니다. 그리고 multi에서 충돌을 확인하죠. 그리고 exec를 이용해서 동시성 문제를 처리합니다. 

 

 

Memcached에서의 동시성 문제

Redis가 싱글스레드임에도 동시성 문제가 발생한다는 것이 조금 놀라웠습니다. 이젠 Memcached의 동시성 문제에 대해서 알아보죠. 

 

Memcached는 멀티스레드이기 때문에 더 다양한 동시성 문제가 발생합니다. 

 

  • Cache Stampede (캐시의 무너짐) : 이 경우는 이미 만료되었거나 존재하지 않는 키를 여러명의 클라이언트가 지속적으로 요청하는 경우에 생기는 문제입니다. 이 경우 모든 프로세스가 동시에 값을 계산하고 캐시에 다시 쓰도록 유도합니다.

    이 상황을 완화하기 위한 방법으로 "lock and compute" 방식을 사용할 수 있습니다. 말그대로 락을 걸고 계산하는거죠. 
  • Dirty Read : 격리수준에서 공부했던 Dirty Read 부정합문제 할 때의 그 더티리드 맞습니다. 하나의 스레드가 데이터를 변경하기 전에 다른 스레드가 이 값을 참조해서 변경하려고 한다면 일관성이 깨진다는 부정합 문제입니다. 

    이 상황을 완화하기 위해서 캐시 전체를 versioning하던가 더 정교한 캐싱 패턴을 적용해야합니다. 예를 들어서 원자적으로 읽고-작업하고-쓴다 라는 로직을 정립해야하죠. 
  • Race Condition : 위에서 설명했던 Redis의 상황과 마찬가지입니다. 넘어가도록 하겠습니다. 
  • Replication and Consistency : Memcached는 Redis와 마찬가지로 레플리케이션 배포 방식을 제공한다고 했습니다. 만약 여러개로 쪼개진 네트워크 상황에선 동시성 문제가 더 심각하게 발생할 수 있습니다. 

 

읽어보면 조금 이상합니다. "해결" 방법이 아니고 "완화" 방법이라고요? 문제를 해결하는게 아니라 완화한다고요? 그럼 해결을 못한다는건가요?

 

"네"

 

Memcached는 동시성 문제를 해결하지 못해서 문제를 완화하는 방법만 존재합니다. 

 

응? 그럼 동시성 문제가 얼마나 심각한 문제인데 방치하겠다는 것인가요?

 

음... 사실 Memcached는 동시성 문제가 그리 심각한 문제가 아닙니다. 

 

여기서 CAP 정리를 짚고 넘어가야겠습니다. 

 

CAP에 대한 자세한 내용은 아래의 링크에서 확인하세요!

https://coding-review.tistory.com/312

 

CAP 정리, PACELC 정리

이번 포스팅에서는 Brewer (브루어)의 CAP정리에 대해서 알아보도록 하겠습니다. CAP정리가 20년도 더 된 정리이기 때문에 그걸 보완한 PACELC 정리에 대해서도 알아보겠습니다. CAP에 대해서 구글링을

coding-review.tistory.com

 

CAP 정리?

위의 링크에서 자세히 볼 수 있지만 한 번 더 설명드리겠습니다. 

 

CAP 정리란 2000년에 Brewer가 발표한 "분산 시스템에서 C (Consistency), A (Availability), P (Partition Tolerance) 셋을 만족하는 데이터베이스는 없다!" 라는 정리입니다. 

 

이 정리는 반은 맞고 반은 틀린 정리라 (오래된 정리라 그렇습니다) 다시 보완하자면 

 

Partition (장애) 상황에서 C와 A중 선택해야한다! 가 올바른 설명입니다. 그럼 장애 상황이 아니면요? 

 

그럼 C랑 A둘 다 만족하는거죠. 

 

그래서 지금 이 얘기를 왜하냐

 

Memcached는 Consistency 즉 일관성을 포기한 AP 데이터베이스이기 때문입니다. 하지만 정상 상황일땐 (동시성 문제가 발생하지 않을 때) C를 만족합니다. 

 

데이터베이스가 일관성이 안맞으면 어떡하냐? 떼잉...

 

이건 RDBMS적인 생각이고 NoSQL은 일관성을 포기한 대신 RDBMS보다 뛰어난 성능과 가용성을 챙겼습니다. 일관성은 BASE 속성 중 E에 해당하는 Eventual Consistency를 통해 맞추고 그 외에 상황에는 뛰어난 성능과 가용성으로 휘어잡는 것이죠. 

 

사실 Memcached는 캐싱 데이터를 저장하는 데이터베이스입니다. 당연한데 왜 짚고 넘어가냐하면 캐싱 데이터는 일관성이 맞을 필요가 전혀 없습니다. 

 

캐싱 데이터는 업데이트가 자주 일어나지 않습니다. 저도 캐싱 솔루션을 프로젝트에 적용해봤지만 캐싱 데이터는 한번 저장하면 TTL이 끝날때까지 변하지 않아도 상관 없는 경우가 대부분입니다. 

 

그래서 제가 "Memcached는 동시성 문제가 큰 문제가 아닙니다" 라고 말한 것입니다. 

 

 

정리

자! 여기까지 Redis와 Memcached의 동시성 문제에 대해서 알아봤습니다. 포스팅의 결론이 좀 이상하긴 합니다. 동시성 문제에 대해서 다뤘는데 결론이 동시성 문제가 별로 중요하지 않다는 것이니까요. 

 

애초에 이 주제를 궁금해 했던 것이 제 뇌는 아직 RDBMS적인 사고를 하고 있기 때문입니다. 

 

  1. Redis-Semaphore가 뭐지?
  2. Redis에서도 Semaphore로 동시성 문제를 해결하는구나...
  3. Redis에서 동시성문제가 발생하면 어떻게되지?
  4. Memcached는?

이런식으로 사고하다보니 동시성 문제가 발생해도 상관없다는 결론이 나오기까지 시간이 걸렸습니다. 

 

RDBMS를 다루면서 동시성문제는 꽤 큰 문제이기 때문에 저도 모르게 NoSQL도 같은 논리로 적용한 것이 조금은 부끄럽네요. 

 

긴 글 읽어주셔서 감사합니다. 오늘도 즐거운 하루 되세요~

 

 

출처

Chat GPT 4.0 - Open AI