티스토리 뷰
728x90
반응형
Reddison Vs Lettuce
우선 시작하기 전에 Spring Boot에서 Redis를 사용하기 위해서는 Redisson과 Lettuce 둘 중 하나를 사용할 수 있습니다. 해당 글에서는 Redisson을 사용하였으며, 왜 Redisson을 사용했는지 두개의 라이브러리가 어떤 차이점을 가지고 있는지 살펴보고 넘어가겠습니다.
💡 Redisson
- Redisson은 우선 Publisher와 Subscriber 기반으로 Lock을 획득 및 제공하고 있습니다.
- Lettuce와 다르게 SpinLock 방식이 아닙니다.
- SpinLock의 경우 순회를 하면서 Lock의 획득 여부를 계속하여 물어보게 됩니다. 이는 Busy Waiting이 발생하게 되며 많은 CPU 자원을 낭비하게 됩니다.
💡 Lettuce
- Lettuce는 setnx 명령어를 사용하여 분산락을 구현하고 있습니다. setnx는 Set if not exists의 약자입니다.
- 락을 획득하기 위해서는 "락이 존재하는지 확인한다", 그리고 "존재하지 않는다면 락을 획득한다" 라는 두 연산인 Atomic하게 이루어져야 하는데 Lettuce에서 setnx라는 명령어를 사용하여 "값이 존재하지 않으면 세팅한다" 처럼 Atomic하게 사용됩니다.
- Lettuce는 SpinLock 방식을 통해 Lock을 획득합니다. 이는 앞에서 말한거처럼 Busy Waiting이 발생하게 되며 많은 CPU 자원을 낭비하게 됩니다.
Spring Boot Reddison 세팅
💡 Gradle 설정
// redisson
implementation 'org.redisson:redisson-spring-boot-starter:3.18.0'
💡 yml 설정
spring:
redis:
host: localhost
port: 6379
💡 Redisson Config 설정
@Configuration
public class RedissonConfig {
private static final String REDISSON_HOST_PREFIX = "redis://";
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private String port;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + host + ":" + port);
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
Redis와 @Transactional 그리고 AOP에 대해
먼저 Redis와 @Transactional 어노테이션과 같이 사용시 어떻게 동작하는지 살펴 볼 필요성이 있습니다.
💡 Presentation Layer
- Presentation Layer에서는 사용자의 요청이 들어오면 Application Layer로 보내고 있습니다.
@RestController
@RequestMapping("/received-gift-box")
@RequiredArgsConstructor
public class ReceivedGiftBoxApi {
private final ReceivedGiftService receivedGiftService;
@PostMapping
public BaseResponse<Void> create(@Valid @RequestBody ReceivedGiftBoxCreateRequest request) {
receivedGiftService.create(request.getGiftSerialCode(), request.getMemberId());
return new BaseResponse<>(CODE_201);
}
}
💡 Application Layer
- Service 로직에서는 코드를 살펴보면 redisson을 통해 Lock을 획득하고 DB에 데이터를 조회한 뒤, 데이터를 저장 후 Lock을 반납하고 있습니다.
- 하지만 아래 코드는 제대로 동작하지 않습니다. 우선 그 이유가 어떤건지 살펴보고 넘어가겟습니다.
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class ReceivedGiftService {
private static final int WAIT_TIME = 1;
private static final int LEASE_TIME = 2;
private static final String GIFT_LOCK = "gift_lock";
private final RedissonClient redissonClient;
private final SendGiftBoxDao sendGiftBoxDao;
private final ReceivedGiftBoxRepository receivedGiftBoxRepository;
public void create(String giftSerialCode, String memberId) {
RLock lock = redissonClient.getLock(GIFT_LOCK);
try {
boolean hasLock = lock.tryLock(WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS);
if (!hasLock) {
log.info("lock 획득 실패");
throw new IllegalStateException("Lock을 획득하지 못하였습니다.");
}
log.info("lock 획득!!");
// DB 정보 조회 및 저장 시작 !!
SendGiftBox sendGiftBox = sendGiftBoxDao.findByGiftSerialCode(giftSerialCode);
if (sendGiftBox.hasAvailableQuantity()) {
sendGiftBox.decreaseAvailableQuantity();
ReceivedGiftBox receivedGiftBox = new ReceivedGiftBox(memberId, sendGiftBox);
receivedGiftBoxRepository.save(receivedGiftBox);
}
// DB 정보 조회 및 저장 끝 !!
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
log.info("lock 반납");
}
}
}
💡 Test Code
@SpringBootTest
class ReceivedGiftBoxApiTest {
private static final ExecutorService executor = Executors.newFixedThreadPool(30);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Autowired
private SendGiftBoxService sendGiftBoxService;
@Test
void 동시에_1000명이_요청하는_테소트() throws InterruptedException {
int threadCount = 1000;
CountDownLatch latch = new CountDownLatch(threadCount);
String giftSerialCode = "2307262320C3PT3LGE:23072623205CI9WVH9";
for (int i = 0; i < threadCount; i++) {
CompletableFuture.runAsync(() -> {
try {
ReceivedGiftBoxCreateRequest request = new ReceivedGiftBoxCreateRequest(String.valueOf(System.currentTimeMillis()), giftSerialCode);
String requestBody = OBJECT_MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(request);
HttpRequest.BodyPublisher body = HttpRequest.BodyPublishers.ofString(requestBody);
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/received-gift-box"))
.POST(body)
.header("Content-Type", "application/json")
.build();
HttpClient client = HttpClient.newBuilder().build();
client.send(httpRequest, HttpResponse.BodyHandlers.discarding());
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
latch.countDown();
}
}, executor);
}
latch.await();
}
}
💡 결과
- 결과를 보면 이용가능한 재고는 처음에는 10개였지만 이후 0개로 정상적으로 감소한 것을 볼 수 있습니다. 하지만 선물을 받은 사용자를 조회하면 20명이 선물을 받았다는 것을 확인할 수 있습니다. 이는 Redisson을 통해 Lock을 획득하고, 정상적으로 Lock을 반납했다고 생각을 할 수 있지만 실제로는 정상적으로 수행되지 않습니다.
💡 원인
- 원인은 사실 AOP로 동작하는 @Transactional 어노테이션에 있습니다.
- 우리가 원하는 것은 Lock을 점유한 상태에서 Transaction을 얻어 정상적인 로직을 수행 후 Transaction을 commit하고 Lock을 반납하는 과정입니다.
- 하지만 과정은 아래와 같이 동작하게 됩니다.
문제를 해결해보자.
해당 문제는 Facade의 역할을 수행하는 클래스를 앞단에 배치함으로써 문제를 해결할 수 있습니다.
💡 Presentation Layer
- 기존 Presentation Layer에서 바로 Application Layer에게 위임을 하였습니다. 하지만 이번에는 퍼사드의 역할을 수행하는 메서드에게 위임을 시킵니다.
@RestController
@RequestMapping("/received-gift-box")
@RequiredArgsConstructor
public class ReceivedGiftBoxApi {
private final RedissonLockFacade redissonLockFacade;
@PostMapping
public BaseResponse<Void> create(@Valid @RequestBody ReceivedGiftBoxCreateRequest request) {
redissonLockFacade.create(request.getGiftSerialCode(), request.getMemberId());
return new BaseResponse<>(CODE_201);
}
}
💡 Facade
- Service를 호출하기 전에 Redis의 Lock을 먼저 획득하고 Service의 Transaction이 수행될 수 있도록 퍼사드 역할을 수행하는 클래스를 하나 둡니다.
@Slf4j
@Component
@RequiredArgsConstructor
public class RedissonLockFacade {
private static final int WAIT_TIME = 1;
private static final int LEASE_TIME = 2;
private static final String GIFT_LOCK = "gift_lock";
private final RedissonClient redissonClient;
private final ReceivedGiftService receivedGiftService;
public void create(String giftSerialCode, String memberId) {
RLock lock = redissonClient.getLock(GIFT_LOCK);
try {
boolean hasLock = lock.tryLock(WAIT_TIME, LEASE_TIME, TimeUnit.SECONDS);
if (!hasLock) {
log.info("lock 획득 실패");
throw new IllegalStateException("Lock을 획득하지 못하였습니다.");
}
log.info("lock 획득!!");
// Application Layer 호출
receivedGiftService.create(giftSerialCode, memberId);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
log.info("lock 반납");
}
}
}
💡 Application Layer
@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class ReceivedGiftService {
private final SendGiftBoxDao sendGiftBoxDao;
private final ReceivedGiftBoxRepository receivedGiftBoxRepository;
public void create(String giftSerialCode, String memberId) {
SendGiftBox sendGiftBox = sendGiftBoxDao.findByGiftSerialCode(giftSerialCode);
if (sendGiftBox.hasAvailableQuantity()) {
sendGiftBox.decreaseAvailableQuantity();
ReceivedGiftBox receivedGiftBox = new ReceivedGiftBox(memberId, sendGiftBox);
receivedGiftBoxRepository.save(receivedGiftBox);
}
}
}
💡 결과
- 결과를 보면 재고가 정상적으로 감소하고, 이벤트 담청자도 10명인 것을 확인할 수 있습니다.
💡 과정
- Facade의 역할을 수행하는 클래스 덕분에 Lock을 획득하고 트랜잭션 수행 후 Lock을 반납하는 과정으로 인해 동시성 제어를 제대로 할 수 있습니다.
💡 마치며
- 지금까지 redisson을 사용하여 어떻게 동시성을 제어할 수 있는지 알아보았습니다. 그리고 Lock을 획득하고 반납하는 과정은 코드의 복잡성과 중복을 초래할 수 있다고 생각합니다. 그렇기 때문에 AOP를 적용하면 조금 더 깔끔한 코드가 될거 같습니다.
- 잘못된 내용이 있을 수 있으니, 언제나 피드백은 환영입니다!!
- 글을 읽어 주셔서 감사합니다!! 😁
참고
- https://hyperconnect.github.io/2019/11/15/redis-distributed-lock-1.html
- https://helloworld.kurly.com/blog/distributed-redisson-lock/
728x90
반응형
'JAVA > SpringBoot' 카테고리의 다른 글
Transactional Outbox Pattern with Polling Publisher (1) | 2023.12.28 |
---|---|
MSA 환경에서 Transactional OutBox Pattern (2) | 2023.12.02 |
Spring Boot & Redis Sorted Set을 사용하여 대기열 구현 (0) | 2023.07.22 |
Spring Boot Excel Download 2편 (0) | 2023.05.02 |
Spring Boot Excel Download 1편 (0) | 2023.05.02 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- 공간 기반 아키텍처
- spring boot redisson sorted set
- space based architecture
- redis 대기열 구현
- 자바 백엔드 개발자 추천 도서
- pipe and filter architecture
- 레이어드 아키텍처란
- redis sorted set으로 대기열 구현
- service based architecture
- spring boot excel download oom
- spring boot excel download paging
- java ThreadLocal
- redis sorted set
- JDK Dynamic Proxy와 CGLIB의 차이
- @ControllerAdvice
- 람다 표현식
- 서비스 기반 아키텍처
- pipeline architecture
- spring boot redisson destributed lock
- spring boot redis 대기열 구현
- microkernel architecture
- 트랜잭셔널 아웃박스 패턴 스프링부트
- polling publisher spring boot
- spring boot redisson 분산락 구현
- spring boot 엑셀 다운로드
- transactional outbox pattern spring boot
- java userThread와 DaemonThread
- transactional outbox pattern
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- spring boot poi excel download
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
글 보관함