Redis 복제(Replication)를 사용한 상품 재고 관리
서론
상품 재고 관리의 중요성
상품 재고 관리는 커머스 프로젝트에 있어 핵심적인 과제 중 하나이다.
재고 부족으로 인한 판매 기회 손실, 정확도 저하로 인한 고객 만족도 감소 등 다양한 문제가 발생할 수 있다.
이로인해 실시간으로 정확한 재고 파악과 이를 기반으로 빠르게 의사결정을 내리는 것이 중요하다.
레디스(Redis)를 활용한 재고 관리 시스템의 필요성
이러한 상품 재고 관리에 있어 레디스(Redis)를 활용한 방식은 큰 강점을 가진다.
레디스는 키-값 저장소로써, 빠른 데이터 처리 속도와 싱글 쓰레드를 통한 높은 동시성 처리 능력을 가진다.
이를 통해 대규모 트래픽이나 대량의 데이터가 발생하는 상황에서도 실시간으로 재고 정보를 업데이트하고 조회할 수 있다.
현재 커머스 프로젝트에서 상품 재고를 레디스 캐시(Cache)를 사용해 관리한다.
이때 캐시 쓰기 전략 중 하나인 Write-Back 전략을 사용해 캐시로만 재고를 관리하고 스케줄러를 사용해 특정 주기마다 캐시된 상품 재고를 데이터베이스에 업데이트시켜준다.
Write-Back 쓰기 전략은 인메모리 저장소인 레디스에만 쓰기 지연이 발생하기에 빠른 성능을 자랑하지만, 메모리 저장소이므로 데이터가 휘발될 가능성을 고려해 적절한 대처가 필요하다.
레디스 데이터 안전성 높이기
레디스에서 데이터 안전성을 높이기 위한 주요 방법으로 지속성 설정(AOF 또는 RDB) 그리고 복제(Replicaiton)이 존재한다.
지속성과 복제 모두 사용하는 것이 이상적일 수 잇지만, 이번에는 복제 방식을 선택하였다.
복제는 데이터를 여러 노드에 분산시켜 하나의 노드가 실패하더라도 다른 노드에서 데이터를 제공할 수 있도록 함으로써 시스템의 전반적인 가용성을 높인다. 이는 특히 중요한 데이터를 다루거나 높은 가용성이 요구되는 서비스에 있어 중요한 역할을 한다.
지속성을 설정하는 경우 특히 AOF의 경우 디스크 쓰기 작업 시 성능 저하를 일으킬 수 있으나 복제를 사용하는 경우 성능 저하 없이 데이터의 안전성을 보장할 수 있다. 또한 AOF나 RDB의 경우 스냅샷의 타이밍 등 추가적인 고려사항이 생기지만, 복제의 경우 데이터를 여러 노드에 분산시키면 특정 노드에 문제가 발생하더라도 다른 노드에서 데이터를 복구할 수 있어 간편하다.
이로인해 복제만을 사용하여도 레디스의 데이터 안전성과 시스템의 가용성을 충분히 보장할 수 있을 것이라 판단하였다.
Redis Replication 적용
docker-compose.yml
goods-redis-master:
container_name: goods_redis_master
image: redis
ports:
- "6381:6379"
volumes:
- redis_master_goods_data:/data
networks:
- miri-network
goods-redis-slave:
container_name: goods_redis_slave
image: redis
ports:
- "6382:6379"
volumes:
- redis_slave_goods_data:/data
command: redis-server --slaveof goods-redis-master 6379
networks:
- miri-network
depends_on:
- goods-redis-master
- goods-redis-master
- 레디스 마스터
- goods-redis-slave
- 레디스 슬레이브
- command: redis-server --slaveof goods-redis-master 6379
- goods-redis-slave 컨테이너를 Redis 슬레이브 모드로 실행하고, goods-redis-master를 마스터로 지정해 6379 포트에서 연결을 시도하라는 의미
application.yml
spring:
data:
redis:
master:
host: 127.0.0.1
port: 6381
slave:
host: 127.0.0.1
port: 6382
- 레디스 마스터와 슬레이브 각각의 IP 주소와 포트 번호를 지정
RedisProperties
@Data
@Component
@ConfigurationProperties(prefix = "spring.data.redis")
public class RedisProperties {
private Master master;
private Slave slave;
@Data
public static class Master {
private String host;
private Integer port;
}
@Data
public static class Slave {
private String host;
private Integer port;
}
}
RedisConfig
@Configuration
@EnableRedisRepositories
public class RedisConfig {
private final RedisProperties redisProperties;
public RedisConfig(RedisProperties redisProperties) {
this.redisProperties = redisProperties;
}
@Bean
public RedisConnectionFactory redisConnectionFactory() {
LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.readFrom(ReadFrom.REPLICA_PREFERRED)
.build();
RedisStaticMasterReplicaConfiguration masterReplicaConfig
= new RedisStaticMasterReplicaConfiguration(redisProperties.getMaster().getHost(), redisProperties.getMaster().getPort());
masterReplicaConfig.addNode(redisProperties.getSlave().getHost(), redisProperties.getSlave().getPort());
return new LettuceConnectionFactory(masterReplicaConfig, clientConfig);
}
@Bean
public RedisTemplate<String, Integer> redisTemplate() {
RedisTemplate<String, Integer> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
return redisTemplate;
}
}
- RedisConnecitonFactory Bean 정의
- LettuceClientConfiguration을 사용해 클라이언트 설정을 정의한다.
- 복제(Replica)에서 읽기를 선호(ReadFrom.REPLICA_PREFERRED)하도록 설정한다.
- 부하 분산: 마스터는 쓰기 작업에 집중하고, 슬레이브는 읽기 작업을 처리함으로써 전체 시스템 부하를 분산시킨다.
- 가용성 향상: 마스터에 문제가 발생해도 슬레이브에서 읽기 작업을 계속할 수 있으므로, 가용성이 향상된다.
- RedisStaticMasterReplicaConfiguration을 사용해 마스터와 슬레이브의 호스트와 포트 번호를 설정한다.
ReadFrom.REPLICA_PREFERRED 이외에도 여러 설정들이 존재한다.
Redis 복제(Replication) 확인
redis-cli 설정 확인
INFO replication
- goods-redis-master
- 역할(role)이 마스터(master)로 설정된 것을 확인할 수 있다.
- 연결된 슬레이브의 숫자가 하나인 것을 확인할 수 있다.
- goods-redis-slave
- 역할(role)은 슬레이브(slave)
- 마스터의 호스트는 goods-redis-master
데이터 캐시 테스트
다음과 같이 상품을 등록하여 재고 레디스에 캐시 되는지 확인한다.
- 마스터와 슬레이브 모두 데이터가 정상적으로 캐시된 것을 확인할 수 있다.
데이터 삭제 테스트
- goods_stock:1과 goods_stock:2, 총 2개의 데이터가 마스터와 슬레이브 각각 존재한다.
- 마스터에서 goods_stock:2 데이터 삭제한다.
- 마스터와 슬레이브 모두 goods_stock:2 데이터가 삭제되었다.
데이터 갱신 테스트
다음과 같이 상품을 구매하고 레디스에 캐시된 상품 재고가 1000개에서 995개로 갱신되었는지 확인한다.
- 레디스에 "goods_stock:1"이 1000으로 등록되어 있다.
- 1번 상품을 5개 주문한다.
- 레디스에 캐시된 상품 개수가 1000개에서 995개로 줄어든 것을 확인할 수 있다.
데이터 조회 테스트
레디스에 캐시된 상품 재고를 조회한다.
- 995개가 정상적으로 조회된다.
슬레이브(Slave) 정지 후 테스트
상품 재고 조회
- 슬레이브가 정지되더라도 마스터에서 상품 재고를 정상적으로 조회한다.
상품 재고 수정
- 상품 재고가 995개에서 990개로 정상적으로 감소되었다.
상품 재고 캐시
- 상품 재고가 정상적으로 캐시되었다.
마스터(Master) 정지 후 테스트
상품 재고 조회
- 상품 재고 조회가 정상적으로 수행된다.
상품 재고 수정(RedisSystemException 발생)
Redis 마스터-슬레이브 구성에서 마스터 노드가 장애가 발생하면, 슬레이브 노드들은 여전히 읽기 작업을 처리할 수 있다.
하지만 쓰기 작업은 마스터 노드에서만 처리되므로, 마스터 노드가 다운되었을 때는 쓰기 작업이 불가능하다. 이는 Redis의 데이터 복제(replication) 메커니즘이 단방향으로, 마스터에서 슬레이브로 데이터가 복제되기 때문이다.
만약 마스터 노드가 실패한 경우, 일반적으로 Redis Sentinel 같은 고가용성(High Availability, HA) 솔루션을 사용하여 자동으로 슬레이브 노드 중 하나를 새로운 마스터 노드로 승격시키는 과정이 필요하다. 이를 통해 쓰기 작업이 중단 없이 계속될 수 있다. 그러나 마스터 노드의 장애가 발생하고 새로운 마스터로의 승격이 이루어지기 전까지는 쓰기 작업이 일시적으로 처리되지 않는 상태가 된다.
또는 AWS의 ElasticCache를 사용해서 개발자가 직접 관리하지 않도록 하는 것도 좋은 방법인 것 같다.