💻문제점1
다음과 같이 points 테이블이 있다.
포인트 사용을 테스트해보면 test1의 포인트는 100점 줄어들고, 스트리머인 test3의 포인트는 100점 증가한다.
@Test
@DisplayName("포인트 사용 테스트")
public void testUsePoint() {
PointUseDto pointUseDto = new PointUseDto("test3", 100, "message");
StepVerifier.create(
pointService.usePoint(createMember("test1"), pointUseDto)
).assertNext(responseEntity -> {
Assertions.assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
Assertions.assertThat(responseEntity.getBody()).isEqualTo(900);
}).verifyComplete();
}
동시성 테스트를 위해 회원을 test1~test6까지 생성했다. test1부터 test5까지는 test6에게 100포인트씩 후원한다.
다음과 같이 5개의 작업을 병렬적으로 수행하도록 했다.
만약 test6은 0포인트➡500포인트가 되어야 하고, 나머지는 1000포인트➡900 포인트가 되어야 한다.
@Test
@DisplayName("포인트 사용 동시성")
public void concurrentUsePointTest() {
PointUseDto pointUseDto = new PointUseDto("test6", 100, "message");
List<Member> members = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
String userId = "test" + Integer.toString(i);
members.add(createMember(userId));
}
Flux.fromIterable(members)
.parallel()
.runOn(Schedulers.newParallel("PAR"))
.doOnNext(member -> {
pointService.usePoint(member, pointUseDto).subscribe();
})
.sequential()
.blockLast();
}
private Member createMember(String userId) {
SignupRequestDto signupRequestDto = new SignupRequestDto(userId, "1234", userId);
return new Member(signupRequestDto, "1234", MemberRoleEnum.USER);
}
하지만 데이터베이스를 확인해보면 test6은 300점 밖에 얻지 못했다...! test1부터 test5까지 같은 데이터에 대해 동시에 접근하고 있기 때문에 동시성 처리가 필요하다. 이제 동시성 처리를 하러 가보자..! (#`-_ゝ-)
🔍문제점1-해결
낙관적 락과 비관적 락 중에서 어떤 방식을 사용해야 할까 고민했다.
낙관적 락은 트랜잭션을 필요로 하지 않는다. 성능적으로는 비관적 락보다 좋은데, 동시성 충돌이 많이 일어나지 않는 경우에 사용하는게 좋고, 비관적 락은 동시성 충돌이 많은 경우에 유리하다고 한다.
간단하게, 낙관적 락은 version 과 같은 별도의 구분 컬럼을 사용해 충돌 여부를 판단한다. 충돌이 발생할 경우 롤백을 하는데, 개발자가 직접 롤백 처리를 해주어야 하는게 단점이다.
비관적 락은 데이터를 수정하는 동안 해당 데이터에 락을 걸어서 다른 트랜잭션에서 접근을 막는 것이다. 다른 트랜잭션은 락이 해제될 때까지 대기한다.
스트리밍 방속 특성 상, 후원은 동시다발적으로 많이 일어나기 때문에 비관적 락이 더 나을 것이라 판단했다.
FOR UPDATE 절을 사용해서 row가 수정되는 동안 다른 트랜잭션이 해당 row에 대한 락을 획득할 때까지 대기하도록 했다.
public Mono<ResponseEntity<Integer>> usePoint(Member member, PointUseDto pointUseDto) {
return memberRepository.findByNickname(pointUseDto.getStreamer())
.switchIfEmpty(Mono.error(() -> new NoSuchElementException("존재하지 않는 사용자입니다.")))
.flatMap(streamer -> pointRepository.findByUserId(member.getUserId())
.switchIfEmpty(Mono.error(() -> new NoSuchElementException("존재하지 않는 사용자입니다.")))
.flatMap(memberPoints -> {
if (memberPoints.getPoints() - pointUseDto.getPoints() < 0) {
return Mono.error(() -> new NoSuchElementException("포인트가 부족합니다."));
}
log.info("streamer.userid: " + streamer.getUserId());
return databaseClient.sql("SELECT * FROM points WHERE user_id = :userId FOR UPDATE")
.bind("userId", streamer.getUserId())
.fetch()
.one()
.flatMap(row -> {
Points updatedStreamerPoints = new Points(
(Long) row.get("id"),
(String) row.get("user_id"),
(int) row.get("points")
);
updatedStreamerPoints.addPoints(pointUseDto.getPoints());
return pointRepository.save(updatedStreamerPoints)
.flatMap(savedStreamerPoints -> {
return pointRepository.save(memberPoints.usePoints(pointUseDto.getPoints()))
.map(savedMemberPoints -> ResponseEntity.ok(savedMemberPoints.getPoints()));
});
});
}));
}
한 명의 회원 test1이 스트리머 test6에게 100포인트를 후원하는 테스트를 수행하면 정상적으로 동작한다.
다시 초기화하고, 이제 아까처럼 여러 명이 동시에 같은 스트리머에 후원하는 경우를 확인해보자.
test1부터 test5까지 test6에게 100포인트씩 후원하므로 test6은 500포인트가 있어야 한다.
test6의 포인트가 500이 되는걸 확인할 수 있다!
더 많은 인원을 해봤다. test1 부터 test20 까지 test21 에게 후원을 하는 상황
2000포인트 획득
관련 포스팅
1. RSocket 통한 후원으로 변경
https://yeon-dev.tistory.com/223
[TIL - 20230623] rsocket 채팅창에 후원 내역 띄우기
💻문제점 후원을 하면 "{nickname}님이 {points}포인트 후원!" 그리고 후원 메시지를 띄우려고 했다. 어떻게 알림을 띄울까 하다가 어차피 이미 RSocket을 사용하고 있으니, 아프리카TV처럼 채팅창에 후
yeon-dev.tistory.com
2. Redis 분산 락 변경
https://yeon-dev.tistory.com/225
[TIL - 20230628] WebFlux Redis 분산 락 적용
💻구현 현재 동시성 제어를 비관적 락을 통해 해결하고 있다. https://yeon-dev.tistory.com/221 [TIL - 20230621] Webflux R2DBC 동시성 문제 해결, 비관적 락 💻문제점1 Junit5로 IntelliJ에서 테스트를 하려고 했는
yeon-dev.tistory.com
'TIL' 카테고리의 다른 글
[TIL - 20230623] rsocket 채팅창에 후원 내역 띄우기 (0) | 2023.06.24 |
---|---|
[TIL - 20230622] K6 성능 테스트 시나리오 작성 (0) | 2023.06.23 |
[TIL - 20230620] Webflux CORS (0) | 2023.06.21 |
[TIL - 20230616] (0) | 2023.06.17 |
[TIL - 20230615] EC2 + Docker Compose 통한 배포 (0) | 2023.06.16 |