트랜잭션(transaction)의 관리
격리성(Isolation) : 일련의 트랜잭션을 작업 중에는 다른 트랜잭션의 연산 작업이 끼어들지 못하게 격리시킨다.
동시에 공유 리소스에 접근시 독립성을 유지하기 위해 대기한다.
트랜잭션의 isolation level 상승
@Transactional(isolation= isolation.SERIALIZABLE)
트랜잭션 내에서 선택(select)된 리소스는 공유 잠금(write제한)된다.
성능적으로 동시처리 성능이 가장 나지만 단순하고 가장 엄격한 관리 수준
SERIALIZABLE
만 해둔다면 쓰기 작업은 안되겠지만 읽기 작업이 동시에 가능하기에 비즈니스 로직에서는 잘 고려해야한다.
트랜잭션 격리 수준의 종류
- repatable-Read(default) : 조회하는 데이터에 대해 공유락을 걸어서 데이터의 변경 불가능을 보장
- read-uncommited : 다른 트랜잭션에서 업데이트가 커밋되지 않더라도 변경된 데이터를 조회 가능(dirty-read)
- A 트랜잭션에서 사용자 정보를 조회하는 도중, B 트랜잭션에서 사용자 정보가 변경되고 커밋되지 않았더라도 A에서는 커밋되지 않은 변경된 데이터를 조회하게 된다.
- read-commited : Commit되기 이전의 작업은 DB의 Undo 영역에 저장해 둔 후 Commit된 이후 실행되게 된다. (Dirty Read 방지)
- 트랜잭션 시작전에 커밋된 내용만 반영한 데이터를 조회할 수 있다.
- 트랜잭션 도중 다른 트랜잭션에서 업데이트가 커밋되면 변경된 내용을 조회 가능
- Serializable : Select된 리소스는 공유 락돼서 수정 및 입력 불가능
Transactional의 propagation 옵션
- Propagation.REQUIRED (default)
- 별도의 트랜잭션이 설정되지 않으면 트랜잭션을 새로 시작한다. (새로운 connection 생성)
- 이미 트랜잭션이 설정되어 있다면 기존 트랜잭션에서 진행한다.(동일한 connection에서 실행)
- 다른 쓰레드로 트랜잭션을 전파하진 않는다. (Spring은 TLS에 트랜잭션 정보를 기록하기 때문)
- Propagation.REQUIRES_NEW
- 매번 새로운 트랜잭션을 생성한다.
- 이미 트랜잭션이 설정되어 있으면 기존 트랜잭션을 잠시 대기 상태로 두고 자기 트랜잭션을 실행한다.
- (2개의 트랜잭션은 완전히 독립적인 별개의 단위)
- Propagation.NESTED
Propagation.REQUIRED
와 동일하지만 Savepoint를 지정한 시점까지 부분적으로 롤백한다.- Oracle처럼 데이터베이스가 세이브포인트를 지원해야한다.
- Propagation.MANDATORY
- 무조건 부모 트랜잭션에 합류시키며, 부모가 없으면 예외를 발생시킨다.
- Propagation.NEVER
- 트랜잭션이 존재하면 exception을 발생시킨다.
- Propagation.SUPPORTS
- 트랜잭션이 존재하면 사용하는데, 없어도 정상적으로 작동한다.
트랜잭션 분리로 베타락 구현
트랜잭션 어노테이션에서 propagation 옵션을 Propagation.REQUIRES\_NEW
으로 트랜잭션을 분리한다면 베타락을 구현할 수 있다.
kafka-querydsl/CouponService.java
@Transactional(propagation = Propagation.REQUIRED)
public Coupon generateRandomCoupon() {
List<String> couponTypes = Arrays.asList("문화상품권 1만원권", "해피머니상품권 5천원권", "비트코인");
String couponType = couponTypes.get(new Random().nextInt(couponTypes.size()));
Long couponCount = couponRepository.countByType(couponType);
if (couponCount < MAX_COUPONS) {
Vendor vendor = vendorRepository.findById(1L).orElseThrow();
Coupon coupon = new Coupon();
coupon.setType(couponType);
coupon.setCount(5000);
coupon.setValue(getCouponValue(couponType));
coupon.setVendor(vendor);
return couponRepository.save(coupon);
}
return null;
}
단, 이 경우는 Hikari Connection Pool
과 Tomcat의 Max Worker Threads
의 개수를 잘 조절해야한다.
connection이 distributed lock을 획득했지만, 새로운 연결을 대기하는 connection 발생으로 DeadLock이 발생할 수 있다.
테스트코드를 통해 알아본 동시성 실험 결과
테스트 코드를 통해 lock 여부에 따른 동시성 제어 실험을 진행했다.
InventoryServiceTest 에서 잠금 여부에 따른 BuyWithoutLock
와 BuyWithLock
를 구성했다.
아이템은 총수량 100개로 존재하며, 각 함수를 실행하는 Worker는 50명이 동시에 2번씩 아이템 구매 요청을 보내서 개수를 줄인다.
실험 결과로 BuyWithoutLock
함수의 경우 중복해서 개수를 소모하지 않는 경우를 확인했다.
반대로 분산 락을 적용한 BuyWithLock
함수같은 경우는 동시 접근이 제한되어 원하는 로직이 잘 수행되었다.
저번에 Kafka의 성능 테스트를 진행했던 프로젝트에 이어서 작업했다.
https://github.com/downfa11/kafka-concurrency
GitHub - downfa11/kafka-concurrency: kafka 트랜잭션 관리와 Redisson 분산 락 처리
kafka 트랜잭션 관리와 Redisson 분산 락 처리. Contribute to downfa11/kafka-concurrency development by creating an account on GitHub.
github.com
'backend' 카테고리의 다른 글
Spring의 비동기 프로그래밍 @Async에 대해 알아보자 (1) | 2024.11.23 |
---|---|
Axon와 Kafka는 어떻게 다른가? (0) | 2024.11.23 |
토이프로젝트 - Redis를 이용한 분산 락(Distributed Lock) (0) | 2024.11.22 |
HTTP multipart/form-data 파일 업로드 문제 해결 (0) | 2024.11.21 |
Hexagonal 아키텍처와 MVC 패턴 비교 (0) | 2024.11.21 |