티스토리 뷰
서론
해당 포스팅을 작성하는 이유는 CAS에 대한 개념을 이해하고 좀 더 좋은 성능을 알아보기 위하여 작성한 글입니다.
기존 Lock 방식을 이용한 여러 동시성 제어 방식들에 대한 이해가 바탕이 되어야 하며, MySQL의 격리 수준에 대한 개념이 어느 정도 지식을 갖추고 있어야 이해하기가 더 편합니다.
참고 - MySQL 격리수준 개념 정리
동시성 제어에 종류
- Java에서 제공하는 Lock 기법
- DB에서 사용하는 비관적 락
- JPA에서 제공하는 낙관적 락(@VERSION 락)
- Hazelcast , Redis와 같은 분산 락
- CAS(CompareAndSet) 방식
이커머스와 같이 재고관리 또는 예약관리 등에서는 동시성 제어하는 것은 필수적인 조건입니다.
동시성 제어를 위한 방법들은 위와 같이 여러 가지가 있지만, 제일 단순하면서, 적합성과 성능을 높여줄 수 있는 방법에 대하여 공유하고자 합니다.
일반적인 재고 또는 예약 횟수에 대한 차감 처리 방식을 ‘쓰기 스큐’ 라고 합니다.
UPDATE {TABLE} set remain = remain - @amount //사용된 재고만큼 차감
where item_no = @itemNo
or
UPDATE {TABLE} set remain = remain // 서버단에서 계산하여 업데이트
where item_no = @itemNo
위와 같은 방식을 사용했을 경우 REPEATABLE READ 기준으로 보게 된다면, 트랜잭션이 발생한 시점에서 일관성 있는 테이블을 제공하고 이러한 경우 ‘재고가 부족해지면 멈추고 재고를 원상 복구한다’라는 개념을 수행할 수 있을 거라 생각합니다.
하지만 이러한 경우 우리가 생각한 방법과 달리, 재고의 개수가 마이너스가 되어 나타나게 되는 경우가 종종 발생하게 됩니다.
만약 이러한 적합성 이슈를 해결하기 위한 방법은 간단합니다. REPEATABLE READ 이상의 격리 수준을 설정하는 것입니다.
바로 DB에 LOCK을 거는 것 또는 SERIALABLE 단계의 격리 수준으로 하여 테이블에 동시 접근을 제어하는 것입니다.
하지만 이러한 경우 적합성 이슈는 없겠지만 성능 이슈가 발생하게 됩니다.
그렇다면 격리 수준을 낮추면서, 적합성을 높일 수 있는 방법은 무엇인가??
적어도 READ COMMITED 격리 수준에서 쓰기 스큐를 했을 때 적합성과 성능을 올릴 수 있는 방법이 있습니다.
CompareAndSet 방식을 적용하여 처리할 수 있습니다.
사용 조건
- 단일 테이블에 대한 관리여야 한다.
- 특정 상황에 대한 경쟁 트랜잭션이 있어야 한다.
- 적합성을 요구해야 한다.
UPDATE {TABLE} set remain = remain = @amount
where item_no = @item_no and remain >= @amount
위의 내용인 경우 특정 상황에 대한 경쟁 트랜잭션이 발생하는 경우 적합성을 요구할 때 효과적인 방법입니다.
이와 같은 방법이 가능한 이유는 MYSQL의 격리 수준에 따른 효과에서 발현된 내용이라고 할 수 있습니다.
예를 들어 A와 B의 트랜잭션이 동시에 접근을 했을 때 동시에 들어왔더라도 MYSQL에서는 각각 트랜잭션에 순서 번호를 매깁니다.
격리 수준에 따라 MVCC의 영향을 받게 되고 언두 로그를 통해 각각의 일관성 있는 데이터베이스를 유지하게 됩니다.
하지만 그 과정에서 update가 발생하게 되면 그에 대한 결과는 테이블 기준으로 영향을 주기 때문에 위와 같은 쿼리를 발생 시 예외 처리가 가능하게 됩니다.
낙관적 락 방식과 CAS 방식을 이용한 동시성 제어 비교하기
JPA에서는 @Version 칼럼을 이용한 트랜잭션 관리를 하게 됩니다.
이번에 성능 테스트를 하게 된 건 @Version 테스트가 아닌 CAS(CompareAndSet) 방식을 이용하여 동시성을 제어하는 방식을 적용한 테스트를 해보려고 합니다.
@Version 방식과 CAS(CompareAndSet) 방식의 성능 차이를 비교해 보겠습니다.
성능 테스트 조건
stages: [
{duration :'8m', target : 50},
],
thresholds: {
http_req_duration : ['avg < 1000','p(95) < 1000'], //95%가 100ms 안에 응답되도록 해보기
http_req_failed: ['rate < 5.00'], //fail이 5% 아래여야함
}
낙관적 락 (Version) 일치여부를 이용한 테스트
JPA에서 제공하는 Lock 방식에 해당합니다.
@Lock(LockModeType.OPTIMISTIC_FORCE_INCREMENT)
@Query("select p from Product p where p.pNum = :productNum")
Product selectProductoptimisticLock(@Param("productNum") String productNum);
재고 업데이트 방식
UPDATE는 service단에서 재고 정보를 minus메서드를 이용하여 차감하고 그 결과를 업데이트하는 방식을 적용.
//ProductItem Dto 메소드에 해당합니다.
public void minusItemCnt(int cnt) {
this.pItmCnt -= cnt;
}
//minusItemCnt 결과를 바로 상품 재고에 덮어쓰는 방식을 적용
public boolean updateProductItemPcnt(ProductItemDto itemDto) throws Exception {
QProductItem qProductItem = QProductItem.productItem;
return jpaQueryFactory.update(qProductItem)
.set(qProductItem.pItmCnt,itemDto.getPItmCnt())
.where(new BooleanBuilder().and(qProductItem.idx.eq(itemDto.getIdx())))
.execute() > 0;
}
성능 테스트 결과
응답 속도는 1000ms 아래인 것을 확인할 수 있었습니다.
적합성 결과 확인
- 왼쪽은 주문 결과이며 , 오른쪽은 상품 재고 현황입니다.
- 적합성 결과 서로 일치함을 확인하였습니다.
CAS(CompareAndSet) 방식을 이용한 동시성 제어 성능 테스트
동시성 제어 체크를 위한 코드
@Modifying
@Query(value="update product_item set p_itm_cnt = p_itm_cnt - :cnt where idx = :idx and p_itm_cnt >= :cnt",nativeQuery = true)
int updateProductItemPcntCompareAndSet(@Param("idx") long idx, @Param("cnt") int cnt);
위 코드의 조건은 해당 상품의 재고가 구매하는 개수보다 크거나 같을 경우에만 업데이트되도록 하는 쿼리입니다.
- 총 10개 항목의 재고수는 모두 1000개입니다.
부하 테스트 결과
http_reqs : 총 1098335번의 요청이 발생하였고, 구매 성공률은 0.26%에 해당합니다.
http_req_duration : 클라이언트로부터의 요청 응답시간은 처음 목표했던 1000ms 보다 훨씬 아래인 23.04ms 결과가 나왔습니다.
적합성 결과 확인
- 왼쪽은 사용자 상품 구매 개수이며, 오른쪽은 상품에 대한 재고 수량입니다.
상품 구매 개수와 남은 재고수량의 합이 모두 1000이라는 일관성 있는 결과가 나왔으므로, 동시성 제어가 완벽하게 이루어진 것임을 확인할 수 있었습니다.
JPA @Version (낙관적 락)과 CAS를 이용한 동시성 제어에 대한 성능차이 결과
Version을 이용한 성능 부하 테스트에서는 95% 응답 결과가 26.2ms 결과가 나왔고, CAS를 이용하여 성능테스트 결과는 95% 응답 결과가 23.04로 좀 더 우세한 결과를 도출하였습니다.
만약 10만 이상의 재고량을 기준으로 테스트했을 경우 이보다 더 큰 시간 차이의 결과를 나타낼 수 있지 않을까 라는 추측을 해볼 수 있습니다.
다만 동시성 제어에 대한 두 방식 모두 적합성에서 우수하게 나타내고 있다는 점은 동일하게 볼 수 있었습니다.
'데이터베이스 > MYSQL' 카테고리의 다른 글
MySQL 격리 수준 개념 정리 (1) (0) | 2024.07.30 |
---|---|
db lock (하드락) 처리 방법 (0) | 2023.05.01 |
- Total
- Today
- Yesterday
- 권한
- 캘린더
- Quartz
- LocalDate
- 스케줄러
- 네이버 클라우드
- Cache
- dfs
- 격리수준
- 이미지
- leatcode
- 개념 이해하기
- 컨테이너
- insert
- Linux
- spring
- dockerfile
- mybatis
- Lock
- 알고리즘
- 도커
- hazelcast
- MySQL
- 리눅스
- centos7
- ncp
- Java
- docker
- 정의
- 캐시
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |