https://suho0303.tistory.com/50
지난 게시물까지 카프카를 활용해서 동시성 문제를 처리하는 방법을 한번 구현해보았습니다.
그러나 구현을 했음에도 이게 진짜 순서 보장이 되고 있는 것인가 에대한 고민이 계속 있었는데요.
그래서 카프카의 용도를 바꾸고, 분산락을 사용해서 동시성 처리를 하는 것으로 로직을 변경해서 테스트 해볼까합니다.
카프카
기존의 카프카를 사용하고 있었던 용도로는 카프카로 메세지의 순서를 보장하며 동시성 처리를 할 목정이었습니다.
이 목표는 단일 파티션에서는 유의미 하겠지만, 단일 파티션으로 로직을 구성하게되면 트래픽을 많이 받는 사이트의 경우, 한 파티션이 모든 메세지를 감당할 수 있을 수 없고, 이때문에 서버의 규모를 확장하는데 제한적입니다.
따라서 멀티 파티션으로 늘려 카프카의 메세지를 키값으로 나누고 메세지가 발급된 타임스탬프에 따라 우선순위큐에 입력받아서 순서를 보장하도록 했었는데요.
그러나 이 큐가 모든 스레드에 공유되는것도 아니고, 파티션에 먼저 발급된다고 해서 더 빨리 메세지를 컨슈밍되는것도 아니기 때문에 저는 카프카로 동시성 처리를 하기 보다는 분산락을 통해 동시성 처리를 구현하려합니다.
DB락
먼저 DB락의 종류를 설명해야겠는데요.
DB락에 대표적인 락으로는 비관적인 락, 낙관적인 락이 존재합니다.
비관적인 락
비관적 락은 데이터 충돌이 빈번하게 발생한다고 가정하고 데이터를 먼저 잠그는 방법입니다.
데이터를 읽을 때 락을 걸어 다른 트랜잭션이 해당 데이터를 수정하거나 삭제하는 것을 막습니다. 이때문에 동시성문제를해결하고자 할때 쓰이며, 데이터의 일관성을 유지할 수 있습니다.
단점으로는 동시성이 떨어질 수 있다는 것입니다. 왜냐하면 충돌을 방지하려 데이터를 먼저 잠그기 때문에 해당 데이터에 락이 걸려있을때 다른 트랙잭션은 접근을 할 수 없기 때문에 무작정 기다릴 수밖에 없고, 이러한 대기상태가 무한정 늘어날경우 데드락 상태가 발생할 수 있기 때문입니다.
낙관적인 락
낙관적인 락은 데이터 충돌이 그렇게 자주 발생하지 않는다고 가정하고, 데이터를 변경하려고 할 때 충돌이 발생했는지 확인하는 방법입니다. 낙관적 락은 데이터를 읽을 때 락을 걸지 않고, 데이터를 변경하려고 할 때 이전에 읽은 데이터가 변경되었는지 확인합니다.
그러나 재고의 대한 값이 수시로 바뀌는 제 프로젝트에서는 데이터 변경이 수시로 이뤄지기 때문에 충돌이 많이 발생할 수 있습니다.
그렇기 때문에 애초에 이 두 Lock을 고려하지 않고, 카프카로 동시성을 해결해보자! 하고 결심하게 된 것인데요.
그러나 카프카로도 멀티 파티션이 되게되면 순서보장을 하지 못하기 때문에 동시성을 완벽하게 해결할 수는 없었습니다.
따라서 저는 다른 방법을 찾아야했는데요.
분산락
분산 락은 여러 대의 서버나 데이터베이스가 있는 분산 시스템에서 동일한 데이터에 대한 동시 액세스를 제어하는 방법입니다. 분산 락은 데이터베이스의 락과 비슷하지만, 여러 대의 서버나 데이터베이스 간에 동기화를 제공합니다.
쿠버네티스 pod 두개로 띄워지는 제 서비스는 분산되어 띄워져있는 서버이기도하고 이후, 분산 DB를 계획하고 있어 분산락을 써서 이 문제를 보다 효과적으로 해결할 수 있지 않을까 생각했습니다.
분산락은 Redisson을 써서 구현했습니다.
이유로는 먼저 저희 프로젝트에서 Redis를 사용중이었어서 바로 적용할 수 있었고, Redis를 사용해서 구현하게 되면 메모리 기반 저장소이기 때문에 고성능의 분산락을 구현할 수 있었기 때문입니다.
거기에 Redisson이 Spring과 친화적이기도 했고 간단히 구현할 수 있어서 선택하게 되었습니다.
분산락을 적용한 코드
우선 카프카로 순서보장을 하고 있던 코드를 리팩토링 했습니다.
여기서의 카프카 용도는 오직 메세지를 발급해서 분산락 메소드로 보내는 역할만 하도록 합니다.
@KafkaListener(topics = "orders", groupId = "order")
public void orderToOrder(ConsumerRecord<String, String> message, Acknowledgment ack) {
try {
redissonService.processOrderWithLock(message.key());
ack.acknowledge();
} catch (Exception e) {
log.error(e.getMessage());
}
}
그다음 여기서 이제 분산락을 통해 주문 처리를 통한 재고의 값을 동시성 처리를 해주게됩니다.
public void processOrderWithLock(String orderKey) throws InterruptedException {
RLock lock = getDistributedLock(orderKey);
if (lock.tryLock(3, 3, TimeUnit.SECONDS)) {
try {
orderService.tryOrder(orderKey);
} catch (Exception e) {
log.error("Error processing order", e);
} finally {;
try {
lock.unlock();
} catch (IllegalMonitorStateException e) {
log.info("Redisson Lock Already UnLocked");
}
}
} else {
throw new RedisLockException("해당 락이 사용중입니다");
}
}
private RLock getDistributedLock(String key) {
return redissonClient.getLock("ORD_" + key);
}
위 코드를 보면 먼저 특정 주문에 대한 락을 생성합니다. 이 락은 redis로 저장이 되는데요. 이름은 주문 키를 사용해 생성됩니다. 이렇게 해서 여러 서버나 프로세스가 동일한 주문에 대해 동시 처리를 시도할 때, 서로 충돌하지 않도록 합니다.
그 다음 3초동안 락을 유지하면서 주문로직을 실행하게 됩니다.
주문로직을 마무리했다면, 락을 해제하여 다른 프로세스가 주문작업을 시작할 수 있도록 합니다.
2023-07-19T17:14:33.861+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 15
2023-07-19T17:14:33.865+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 14
2023-07-19T17:14:33.870+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 13
2023-07-19T17:14:33.875+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 12
2023-07-19T17:14:33.879+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 11
2023-07-19T17:14:33.884+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 10
2023-07-19T17:14:33.890+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 9
2023-07-19T17:14:33.894+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 8
2023-07-19T17:14:33.900+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 7
2023-07-19T17:14:33.903+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 6
2023-07-19T17:14:33.908+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 5
2023-07-19T17:14:33.912+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 4
2023-07-19T17:14:33.916+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 3
2023-07-19T17:14:33.921+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 2
2023-07-19T17:14:33.926+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 1
2023-07-19T17:14:33.932+09:00 INFO 23072 --- [ntainer#2-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 0
2023-07-19T17:14:33.940+09:00 ERROR 23072 --- [ntainer#2-0-C-1] p.t.d.c.r.service.RedissonService : Error processing order
project.trendpick_pro.domain.product.exception.ProductStockOutException: 재고가 부족합니다.
at project.trendpick_pro.domain.product.entity.productOption.ProductOption.decreaseStock(ProductOption.java:76) ~[classes/:na]
at jdk.internal.reflect.GeneratedMethodAccessor225.invoke(Unknown Source) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor.intercept(ByteBuddyInterceptor.java:55) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final]
at org.hibernate.proxy.ProxyConfiguration$InterceptorDispatcher.intercept(ProxyConfiguration.java:102) ~[hibernate-core-6.1.7.Final.jar:6.1.7.Final]
이렇게 하게되면 재고가 차례대로 줄어들면서 0개가 되었을때, 재고가 부족하다는 예외를 던지는 것을 확인했습니다.
문제점
분산락을 통해 재고가 잘 줄어들고 있는것은 알았지만, 과연 현재 카프카의 메세지가 유실되지 않고 잘 발급이 되는지 확인해보고싶었는데요. 따라서 컨슈밍되는 주문번호 값들을 로그로 같이 출력해보았습니다.
메세지 producer 쪽에서는 잘보내고 있지만,
2023-07-19T19:26:27.718+09:00 INFO 15728 --- [io-8080-exec-44] p.t.global.kafka.KafkaProducerService : Message sent: 207
2023-07-19T19:26:27.718+09:00 INFO 15728 --- [o-8080-exec-136] p.t.global.kafka.KafkaProducerService : Message sent: 206
2023-07-19T19:26:27.719+09:00 INFO 15728 --- [io-8080-exec-51] p.t.global.kafka.KafkaProducerService : Message sent: 208
2023-07-19T19:26:27.723+09:00 INFO 15728 --- [io-8080-exec-76] p.t.global.kafka.KafkaProducerService : Message sent: 210
2023-07-19T19:26:27.723+09:00 INFO 15728 --- [o-8080-exec-159] p.t.global.kafka.KafkaProducerService : Message sent: 209
2023-07-19T19:26:27.725+09:00 INFO 15728 --- [io-8080-exec-97] p.t.global.kafka.KafkaProducerService : Message sent: 211
2023-07-19T19:26:27.725+09:00 INFO 15728 --- [o-8080-exec-150] p.t.global.kafka.KafkaProducerService : Message sent: 212
컨슈밍되는 곳에
2023-07-19T19:26:27.803+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 150
2023-07-19T19:26:27.805+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_27
2023-07-19T19:26:27.808+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 149
2023-07-19T19:26:27.811+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_28
2023-07-19T19:26:27.814+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 148
2023-07-19T19:26:27.817+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_30
2023-07-19T19:26:27.819+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 147
음...어디선가 메세지가 유실되고 있는것이 명확하게 보였습니다.
그래서 쭉 확인해봤더니
2023-07-19T19:26:28.143+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_227
2023-07-19T19:26:28.145+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 70
2023-07-19T19:26:28.147+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_4
2023-07-19T19:26:28.149+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 69
2023-07-19T19:26:28.151+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_6
2023-07-19T19:26:28.153+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 68
2023-07-19T19:26:28.156+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_10
2023-07-19T19:26:28.158+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 67
2023-07-19T19:26:28.160+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_12
2023-07-19T19:26:28.163+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 66
2023-07-19T19:26:28.165+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_13
2023-07-19T19:26:28.166+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 65
2023-07-19T19:26:28.169+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_14
2023-07-19T19:26:28.171+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 64
2023-07-19T19:26:28.173+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_18
2023-07-19T19:26:28.175+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 63
2023-07-19T19:26:28.177+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_19
2023-07-19T19:26:28.179+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.o.service.impl.OrderServiceImpl : 재고 : 62
2023-07-19T19:26:28.181+09:00 INFO 15728 --- [ntainer#1-0-C-1] p.t.d.c.r.service.RedissonService : ORD_20
발급에 실패해서 다시보내기때문에 뒤늦게 컨슈밍이 되더군요.
이문제는 어떻게 발급을 실패하는지 공부해보고 또 바로 해결해봐야겠습니다.
참고
https://helloworld.kurly.com/blog/distributed-redisson-lock/
'나의 공부' 카테고리의 다른 글
깃허브 릴리즈 노트 자동화 하기 - 1 : 버전 업데이트하기 (0) | 2023.09.30 |
---|---|
Cache 탐험 (0) | 2023.08.28 |
Kafka를 활용한 재고에 대한 동시성 처리기 - 2 (0) | 2023.07.18 |
kafka를 활용한 재고에 대한 동시성 처리기 - 1 (0) | 2023.07.16 |
NCP에서 크레딧을 지원받았다.. (1) | 2023.06.19 |
댓글