안녕하세요, 이번글은 장바구니 로직을 로직완성을 하고 개선을 하기위해 겪었던 과정의 글입니다.
문제상황
저는 장바구니 로직중 메뉴를 지우는 로직은 이와같습니다.
1. 장바구니에 담긴 메뉴를 지웁니다.
2. 장바구니가 비어있는지 확인합니다.
3. 비어있다면 유저가 가진 매장정보 + 유저의 장바구니 정보도 지워줘야합니다.
이부분에서 로직한번에 db커넥션을 4번이나 해야하는 상황이 생깁니다.
🎈레디스도 커넥션 풀을 가질가?
레디스는 메인 프로세스가 싱글스레드인것이고 커넥션은 일반 디비처럼 커넥션풀을 씁니다.
레디스 커넥션 설정을 찾아보면 설정하는게 존해합니다.
커넥션이란게 어차피 데이터 전송이 다 끝나고 handshake까지 마쳐야 종료되는거라 메인 프로세스와는 별도로 여러개의 커넥션을 가지고 네트워크 round trip시간동안 유지하게됩니다
생각한 방법
매번 db connection을 매번 연결하는것보다 한번의 연결로 모든 명령어를 전달하면 좋다고 생각했습니다.
왜냐하면 db 커넥션풀에서 커넥션을 가져와 연결을하기때문에 반복되다보면 성능이 조금느려질 것이라고생각하기때문입니다.
+ 레디스는 싱글 스레드입니다. 그래서 동시보장을 해줍니다.
커넥션풀의 개수가 제한되어있으니 만약 많은 유저가 요청으로인해서 커넥션의 개수가 모자를수있는 상황이있다고 생각했습니다. 그런데 커넥션을 한번의 커넥션에 명령어들을 담아두면 좋을거라고생각했습니다.
제가 생각한 방법을 찾아본결과 Redis pipelining이 존재했습니다.
Redis pipelining이란?
Redis 파이프라이닝은 개별 명령에 대한 응답을 기다리지 않고 한 번에 여러 명령을 실행하여 성능을 향상시키는 기술입니다.
사례를 살펴보면 요청을 동일한 목록에 많은 요소를 추가하거나 많은 키로 db를 채우는경우 초당 100,000 개 처리를 할수있어도 RTT시간이 250밀리초가 걸린다면 초당4개의 요청을 처리할 수있게되는 문제를 개선할수있다고 적혀있습니다.
클라이언트가 이전 응답을 아직 읽지 않은 경우에도 새 요청을 처리할 수 있도록 요청/응답 서버를 구현할 수 있습니다. 이렇게 하면 응답을 전혀 기다리지 않고 서버에 여러 명령 을 보내고 마지막으로 한 단계에서 응답을 읽을 수 있습니다.
사용하면서 겪었던 문제
성능이 얼마나 개선될지 궁금하기때문에 성능측정을 해봤습니다.
1. 파이프라이닝을 적용한 시간입니다.
@Test
@DisplayName("삭제후 장바구니 비어있는경우")
void cartEmpty() throws Exception {
long before = System.currentTimeMillis();
pathValue = "/2_7_8";
mockMvc.perform(delete(url + pathValue).session(mockHttpSession))
.andExpect(jsonPath("$.status").value(HttpStatus.OK.value()))
.andDo(print());
System.out.println("time : "+ (System.currentTimeMillis() - before));
Object getStoreId = redisCartTemplate.opsForHash().get("STORE_ID", userId);
Assertions.assertNull(getStoreId);
}
public void deleteItem(String userId, String cartKey) {
redisCartTemplate.opsForHash().delete(userId, cartKey); // 메뉴하나삭제
if (redisCartTemplate.opsForHash().entries(userId).size() == 0) {
RedisSerializer keySerializer = redisCartTemplate.getStringSerializer();
RedisSerializer hashValueSerializer = redisCartTemplate.getHashValueSerializer();
redisCartTemplate.executePipelined(new RedisCallback<Object>() {
public Object doInRedis(RedisConnection connection) throws DataAccessException {
connection.del(keySerializer.serialize(userId));
connection.hDel(keySerializer.serialize(STORE), hashValueSerializer.serialize(userId));
return null;
}
});
}
}
시간측정 적용한 시간
time : 129
time : 127
2. 적용하지 않는 경우 입니다.
public void deleteItem(String userId, String cartKey) {
redisCartTemplate.opsForHash().delete(userId, cartKey); // 메뉴하나삭제
if (redisCartTemplate.opsForHash().entries(userId).size() == 0) {
redisCartTemplate.delete(userId);
redisCartTemplate.opsForHash().delete(STORE, userId);
}
}
time : 45
time : 46
이 나왔습니다.
왜 적용안한부분이 더 빠를가?
먼저 문제상황에서는 4개였지만 지금은 2개명령어를위해 파이프라인을 사용하고있습니다.
왜냐하면 조회를 해서 가져온 결과에따라 작동을 해야하기때문였습니다.
그래서 2개의 명령을 파이프라인으로 사용하면 파이프라인 메서드를 실행하는데 걸리는 시간이 더 길어진것같습니다.
(정확하진않지만 명령을 대기열에 넣는시간의 증가)
주로 사례를 조사해면서 문서에서도 나와있듯이 한꺼번에 데이터를 주입하는 경우에 주로 사용됩니다.
레디스 파이프라인 개별 명령의 수와 속도 향상 정도 사이에는 대략적인 상관 관계가 있다는 것을 알게되었습니다.
몇개부터일가?
+ 이부분은 몇개부터 개선되는지 실험을 한결과를 업로드하겠습니다.
느낀점
레디스 파인프라인을 사용하면 RTT시간을 줄일수있어 성능을 개선시킬수있지만 이번에는 무조건 1개초과인경우 저는 좋다고 판단했지만 결과가 아니기때문에 향 후 기술응용에있어서 기술을 적용하고 더 나빠진다면 이러한 원인을 분석하고 무조건 도입이 좋지않다는것을 느꼈습니다. (상관관계에대한 개수는 업데이트 하겠습니다)
https://redis.io/docs/manual/pipelining/
Redis pipelining
How to optimize round-trip times by batching Redis commands
redis.io
'Delivery' 카테고리의 다른 글
FCM 푸시알림 구현 이슈 - 비동기 처리(성능개선) (0) | 2022.08.23 |
---|---|
레디스 테스트코드 문제점 - TestContainer도입 (0) | 2022.08.19 |
Redis를 이용한 장바구니 - 1 (0) | 2022.08.15 |
리펙토링 디비조회 (0) | 2022.08.09 |
캐싱전략, 메모리 (0) | 2022.08.05 |