기본적으로 Redis는 메모리 기반 동작, MySQL은 디스크 동작 이에 따른 속도 차이, 비관계형 DB와 관계형 DB라는 차이 등등의 내용은 대표적인 둘의 차이이다.
- Redis: 메모리 기반 Key - Value 저장소, 다양한 자료구조 제공
- MySQL: 디스크 기반 RDBMS, ACID 트랜잭션 보장
메모리 기반이어서 Redis가 기본적으로 더 빠른 것은 맞지만, 이 외에도 속도의 차이가 나는 이유, 그렇게 설계된 이유가 있다. 그 차이에 대해 알아보려고 한다.
메모리 기반 vs 디스크 기반
- 메모리 저장소 - Redis
- 모든 데이터를 RAM에 저장하여 읽기/쓰기 지연을 최소화한다.
- RDB/AOF를 별도 스레드로 처리
- 스냅샷 복구 방식(RDB), 명령어 순차처리(AOF) 복구 방식 -> 디스크에 저장
- 메모리만으로 동작해 디스크 I/O 비용이 없다.
- 디스크 기반 RDBMS - MySQL
- 데이터와 인덱스를 디스크에 저장한다.
- 자주 쓰이는 페이지를 메모리에 캐싱한다. (InnoDB Buffer Pool)
- 트랜잭션 로그 (REDO/UNDO), WAL(Write-Ahead Log)으로 일관성을 보장한다.
- WAL = 데이터베이스에 업데이트가 써지기 전 관련된 UNDO 정보가 로그에 쓰여야 한다.
- 디스크 I/O 발생 시 지연이 발생한다.
종합적으로 Redis는 디스크 없이 메모리만 사용하는 단순화 덕분에 극단적인 성능을 챙길 수 있고, MySQL은 디스크 + 트랜잭션 로그를 통해 강력한 정합성을 얻는다.
왜 Redis는 극단적 성능을, MySQL은 강력한 정합성을 선택한걸까?
- Redis의 설계: 실시간성, 단순성
- 캐시, 세션, 큐잉과 같은 읽기/쓰기 지연의 최소화가 우선이다.
- 단일 스레드 + 자료구조별 특화 구현으로 락 관리와 동시성 오버헤드를 제거한다.
- 필요에 따라 RDB/AOF 조합을 통해 휘발성과 영속성도 조절이 가능하기도 한다.
- 메모리 기반이다보니 대용량에선 비교적 취약하고, 관계형 처리도 포기했지만 대신 지연없는 실시간 처리를 보장한다.
- MySQL의 설계: 관계형 모델, ACID 보장
- 금융, 전자상거래 등 데이터 정합성이 생명인 시스템에서 신뢰성이 최우선이다.
- 트랜잭션 격리, 복잡한 SQL (조인, 뷰, 프로시저), 인덱스 최적화등의 기능이 존재한다.
- WAL, REDO/UNDO 로그를 통해 장애 시 무결성을 보장한다.
- 구조가 복잡하고 디스크 I/O 비용이 있지만, 저지연 대신 안정성을 택한 구조이다.
자료구조에서의 차이
- MySQL
- 클러스터 인덱스: B+Tree
- PK 기반 자동 생성
- 데이터가 정렬되고 인덱스와 데이터가 리프노드에 함께 저장되는 구조
- 전체 데이터가 인덱스 순서대로 정렬되어 있어 범위 조회에 최적화 되어 있다.
- 검색/삽입/삭제: O(log N)
- 범위 조회: 리프 노드에서 순차 탐색하여 효율적이다.
- 보조 인덱스: B+Tree + PK 참조
- CREATE INDEX
- 리프 노드에 해당 키 값을 가진 행의 PK 값 (포인터)만 저장한다.
- 해당 보조 인덱스도 정렬되어 효율적으로 수행되지만, 실제 테이블에서 해당 행을 꺼내기 위해서는 정렬된 포인터를 이용해 클러스터 인덱스를 탐색해야 하고 이 때 지연이 발생할 수 있다.
- 실제 행 데이터를 읽으려면 클러스터 인덱스로 추가 탐색이 필요해 비교적 느리다.
- 인덱스 탐색: O(log N) + 클러스터 인덱스 추가 탐색: O(log N) = 총 O(2logN)
- 클러스터 인덱스: B+Tree
Redis는 여러가지 컬렉션들이 존재하고 많이 쓰이는 List, Set, ZSet을 정리해보고자 한다.
- Redis
- List 자료구조
- 양방향 연결리스트로 구현되어 있다. (QuickList)
- LPUSH/RPUSH, LPOP/RPOP: O(1)
- 인덱스 조회: O(N)
- LRANGE: O(N + M) -> 순차 탐색하는 첫 지점을 찾기 위한 O(N) + 범위 M
- Set 자료구조
- 해시 테이블 (dict) 기반 구현
- 추가/삭제/검색: O(1) -> 해시 함수를 통해 위치를 바로 찾을 수 있기 때문.
- ZSet 자료 구조
- 해시 테이블 + Skip List 기반 구현 -> 점수 조회에 해시테이블을 사용하고, 정렬 순위 기반 연산에 Skip List라는 score 순으로 정렬된 상태를 유지하는 자료구조를 이용한다. 다음 노드의 점수가 목표보다 낮으면 이동하는 레벨을 이동해 레벨이 0이되면 위치에 도달한다.
- 삽입/삭제: O(log N)
- 랭킹 조회: O(log N) -> 멤버의 점수 조회 O(1) + SkipList에서 해당 노드까지 가며 랭크 계산 O(log N)
- 범위 조회: O(log N + M) -> Skip List로 시작 위치까지 탐색 O(log N) + 레벨 0 포인터만 따라가며 순차적으로 M개 요소를 수집 O(M)
- List 자료구조
MySQL과 삽입/삭제, 범위 조회에서의 차이
- 삽입/삭제: O(log N)으로 동일
- 범위 조회: O(log N + M)으로 동일
결과적으로 알고리즘적인 복잡도는 동일하다. 하지만 실제로는 우선 메모리 vs 디스크에서의 속도 차이가 존재한다.
만약 둘 다 인메모리 기반 동작이라고 가정했을 때 그럼 차이가 없다고 생각해도 되는걸까?
그렇지 않다.
- MySQL
- MVCC
- 각 행에 대해 버전을 여러 개, 스냅샷을 제공하는데 읽기 트랜잭션은 락 없이도 자신이 시작할 때 보이던 버전을 읽는다.
- 쓰기 트랜잭션은 원본 데이터 복사, undo 로그 기록, 인덱스 잠금, 버전 체인 연결 과정을 거쳐야 해 오버헤드가 커진다.
- 락 관리
- 잠금 요청 시 내부 lock manager가 큐를 관리하고 락 충돌이 발생하면 트랜잭션이 대기했다가 깨어나야 한다.
- 대기 및 깨우기 비용, 잠금 대기 시간 계산, 데드락 감지 등 작업에 비용이 들어간다.
- MVCC
- Redis
- 싱글 스레드 이벤트 루프 모델
- 모든 커맨드를 단일 스레드에서 순차적으로 처리한다. 따라서 스레드 간 간섭에 의한 오버헤드가 전혀 없다.
- I/O 멀티 플렉싱 (하나를 여러 개처럼 보이게 동작한다)으로 네트워크 요청이나 응답 처리도 낮은 지연으로 일괄 처리가 가능하다.
- 싱글 스레드 이벤트 루프 모델
이러한 이유로 락 관리, 스냅샷, 트랜잭션 로그 동기화 같은 무거운 절차 없이 메모리 포인터 연산만으로 작업이 이루어지기 때문에 단순히 시간복잡도가 같다고 해서 MySQL이 인메모리에서 동작했을 때 Redis와 비슷한 성능을 낼 수 있다고 단언할 수 없다.
연산에서의 차이
Redis는 기본적으로 싱글 스레드 이벤트루프 모델이어서 모든 명령이 단일 스레드에서 순차 처리 되고 그 사이 다른 클라이언트 명령이 개입하지 못한다.
또, 루아 스크립트를 이용하면 여러 명령어를 묶어서 하나의 트랜잭션처럼 실행이 가능하고 스크립트가 끝나면 결과를 한 번에 반환한다.
이렇게 묶었을 때 네트워크 왕복 비용이 사라져 여러 키에 대해 연산할 시 레이턴시 절감 효과가 크다.
MySQL은 어떨까?
- MySQL
- 트랜잭션 단위 원자성
- MySQL에서는 여러 SQL문을 BEGIN..COMMIT 블록 안에 넣으면 ACID 특성이 보장되며 커밋 시점에 모두 적용이 된다.
- 하지만 각 SQL문은 별도의 네트워크 요청 및 응답이 필요하다.
- 여러 쿼리 묶기
- ; 구분으로 가능하지만 클라이언트 드라이버가 한 번에 여러 쿼리를 보내도 내부적으로는 파싱, 플래닝, 실행 단위가 각각 분리되어 응답도 각 쿼리별로 리턴되고, 클라이언트에서 결과를 집계해야 한다.
- 네트워크 RTT 비용
- 만약 재고 감소 로직을 MySQL에서 구현한다고 가정하자.
- SELECT stock FROM products WEHRE id = 1;
- if stock > 0 then UPDATE products SET stock = stock - 1 WHERE id 1; end
- COMMIT
- 이렇게 2~3회의 왕복이 필요하다. SELECT -> 클라이언트 처리 -> UPDATE -> COMMIT 순서로 레이턴시와 트랜잭션 격리 관리 비용이 누적된다.
- 트랜잭션 단위 원자성
결과적으로 Redis에서는 원자적 명령 그리고 루아 스크립트를 통해 다중 연산의 일관성 보장과 네트워크 RTT 비용을 절감하지만, MySQL은 ACID 보장이 강력한 대신 왕복 횟수와 MVCC, 락 관리 오버헤드 때문에 레이턴시가 더 높고 처리량이 더 낮은 것이다.
락에서의 차이
- Redis 단일 키 락
- Set key value NX PX 30000 명령어를 가정하자.
- 내부 해시 테이블에서 락 전용 Key에 대응하는 버킷을 해시 함수로 계산한다. 해당 버킷이 비어있는지 확인하고 없다면 새로 생성한다.
- NX 플래그를 통해 키가 없을 때만 SET이 동작하고 이미 존재한다면 실패를 리턴한다.
- PX 30000으로 TTL을 부여하고 이벤트 루프가 주기적으로 힙 최상단을 확인해 만료된 키는 자동으로 삭제한다.
- 락 해제는 일반적으로 DEL key로 해제하게 된다.
- O(1)의 속도로 해시 버킷 조회/삽입만 수행하고 단일 스레드이기 때문에 락 경합 시 스레드 문맥 전환이 일어나지 않는다.
- Redis 분산 락
- 단일 Redis 사용 시 락 정보가 한 대에 저장되는데 해당 노드가 죽으면 락 정보가 날아가거나 락 획득이 불가능해진다. 장애 복구 과정에서 락이 풀린 것으로 잘못 인식되어 동시 처리가 될 수 있어 분산락을 사용하게 된다.
- 복수의 Redis 인스턴스의 환경에서 클라이언트는 과반수 노드에서 락을 획득해야 성공한다.
- 과반수 노드에서 성공 응답을 받으면 락을 획득하고 TTL 내 작업을 수행한다.
- 각 노드에 값 일치 시 DEL Lua 스크립트를 전송해 과반수에서 지워지면 락이 해제된다.
- 네트워크 RTT 비용 * 노드 수이기 때문에 로컬 락보다 레이턴시는 비교적 증가한다.
- MySQL 비관적 락
- 데이터를 읽을 때 데이터베이스 테이블이나 레코드에 락을 거는 방식
- 데이터를 읽는 시점부터 락을 설정해 다른 트랜잭션이 동시에 해당 데이터를 수정하는 것을 방지한다.
- 공유 락과 배타 락 둘로 나뉘어 있는데, 공유 락은 데이터를 읽을 때 사용되며 다른 트랜잭션도 해당 데이터를 읽을 수 있도록 하고, 배타 락은 데이터를 수정할 때 사용되며 해당 데이터에 대한 다른 트랜잭션의 접근을 차단한다.
- MySQL 낙관적 락
- 데이터베이스 레벨이 아닌 애플리케이션 레벨에서 락을 핸들링한다.
- 동시에 여러 트랜잭션이 데이터를 읽을 수 있어 동시성이 향상된다.
- 커밋 시점에 버전 비교를 통해 충돌을 감지하여 충돌이 감지된다면 예외가 발생하고, 롤백 로직이 실행된다.
낙관적 락에서 만약 롤백 비용이 적다면 사용하는 것이 좋은걸까?
우선 낙관적 락의 장점을 보자면, 대기가 없기 때문에 동시성이 극대화 된다는 장점이 있다. 애플리케이션 레벨에서 충돌 감지 후 재시도만 처리한다.
충돌 빈도가 낮거나, 트랜잭션 규모가 작아 UNDO/REDO 로그가 거의 남지 않아 실패 오버헤드가 미미하면 롤백 비용이 낮다.
따라서 롤백 비용이 적다는 뜻에 위 개념이 포함된다면 낙관적 락을 사용하면 동시성이라는 장점을 챙길 수 있어 좋다.
하지만 단순히 롤백하는 비용이 적다고 가정하면 사용자 경험 상 재시도 지연을 감내할 수 있을 정도일 때 낙관적 락을 선택할 수 있겠지만, 그게 아닌 이상 충돌이 잦다고 가정했을 시 비관적 락이나 Redis 락 같은 사전 잠금 방식을 선택하는 것이 좋을 것 같다.
확장 및 장애 대응
- Redis의 확장 및 장애 대응
- 샤딩
- 전체 키 공간을 슬롯으로 쪼개고 노드에 분산한다.
- 각 키가 어떤 슬롯인지의 계산은 클라이언트 라이브러리가 자동 처리한다.
- 각 노드는 독립적인 인메모리 인스턴스이다. 하나의 물리 RAM이 있다면 그것을 나누어 할당받고 하나처럼 동작하는 것 처럼.
- Sentinel
- 마스터 노드 상태를 주기적으로 PING -> 응답 지연/실패를 감지하는 방식
- Sentinel 노드들이 과반수로 마스터 장애를 확인해야 장애로 처리하고 장애 발생 시 가장 최신 복제본을 새 마스터로 승격한다.
- Cluster
- 샤딩된 슬롯 분배와 Sentinel급 페일 오버를 하나의 프로토콜로 통합
- 마스터 장애 시 해당 슬롯을 Replica가 즉시 승격해 중단 없는 서비스를 유지한다.
- 샤딩
각 Redis 노드는 모두 할당된 슬롯 데이터를 유지하며 물리적인 RAM이 고장나지 않는 한 각각의 노드는 분리되어 있어 장애 복구가 가능하다. 또 디스크 연산 없이 동작해 빠르다.
- MySQL의 확장 및 장애 대응
- Primary - Replica 복제
- 샤딩
- 비슷하지만 스키마 분할, 라우팅, 원자적 트랜잭션 보장 측면에서 운영/일관성 보장이 더 복잡하다.
샤딩일 때 데이터가 어떤 노드에 있는지 아는 이유
키 혹은 인덱스를 이용해 데이터가 어디 있는지 빠르게 매핑한다.
Redis는 키를 해시해 어느 슬롯 -> 어느 노드로, MySQL은 인덱스를 따라 B+Tree를 탐색하거나 샤딩 키를 기반으로 어느 샤드일지 찾아간다.