Search

[Applicaion]09.동시성 문제: 방안(3) MySQL Lock

Publish Date
2024/11/14
Tags
Status
Done
1 more property
동시성 문제: MySQL Lock
MySQL 비관적 락(SELECT FOR UPDATE)을 통해 동시성 문제를 해결하고 데이터의 정합성을 보장하는 것을 목표로 한다.

배경 및 목표

쿠폰 발급 서비스에서 높은 동시 요청이 발생할 때, 정확한 수량 제어와 데이터 정합성을 유지하는 것이 중요하다. Redis Lock 구현에서는 약간의 오차(예: 발급 수량 30,000개 중 30,003개 발급)와 높은 실패율(55.73%)이 관찰되었다. 이를 해결하기 위해 데이터베이스 수준에서 동시성 문제를 제어할 수 있는 MySQL의 Pessimistic Lock을 도입하여 정확도를 높이고 실패율을 감소시키고자 한다.
MySQL Lock은 트랜잭션 내에서 데이터의 일관성을 유지하고, 동시성 문제를 방지하기 위해 특정 레코드에 대한 접근을 제한하는 기능이다. 이를 구현하기 위해 FOR UPDATE 키워드를 사용하며, 트랜잭션이 종료될 때까지 다른 트랜잭션이 동일한 레코드에 접근하지 못하게 한다. 예를 들어, SELECT ... FOR UPDATE를 사용하면 해당 레코드에 쓰기 잠금을 적용하여 데이터 정합성을 보장할 수 있다.
SELECT * FROM coupons WHERE id = 1 FOR UPDATE;
SQL
복사
장점:
데이터베이스 수준의 동시성 문제 효과적
데이터 정합성을 보장, 정확한 결과 반환
단점:
잠금으로 인해 트랜잭션 처리 속도가 저하
높은 동시성 요청 시 병목 발생 가능성

MySQL Lock 구현

구조 및 구현

coupon-BE/ ├── coupon-core/ │ ├── src/ │ ├── main/ │ │ ├── java/com/example/couponcore/ │ │ │ ├── repository/mysql/ │ │ │ │ └── CouponJpaRepository.java # Pessimistic Lock을 적용 │ │ │ ├── service/ │ │ │ │ └── CouponIssueService.java # Lock을 적용한 Service
Java
복사
파일명
설명
CouponJpaRepository.java
Pessimistic Lock을 적용하여 데이터베이스 레코드 잠금
CouponIssueService.java
쿠폰 발급 로직에서 Lock 적용을 통한 동시성 제어 구현

소스코드

CouponJpaRepository.java
MySQL FOR UPDATE를 활용하여 데이터베이스 레벨에서 레코드를 잠금 처리, 데이터 정합성을 보장한다.
package com.example.couponcore.repository.mysql; import com.example.couponcore.model.Coupon; import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Query; import java.util.Optional; public interface CouponJpaRepository extends JpaRepository<Coupon, Long> { @Lock(LockModeType.PESSIMISTIC_WRITE) // Pessimistic Lock 적용 @Query("SELECT c FROM Coupon c WHERE c.id = :id") Optional<Coupon> findCouponWithLock(long id); }
Java
복사
코드 요소
설명
@Lock(LockModeType.PESSIMISTIC_WRITE)
MySQL FOR UPDATE를 적용하여 데이터베이스 레코드 잠금
findCouponWithLock(long id)
쿠폰 데이터를 잠금 상태로 조회하여 동시성 문제 방지
Optional 반환
조회 결과가 없을 경우를 안전하게 처리 가능
CouponIssueService.java
쿠폰 발급 로직에서 Pessimistic Lock을 활용하여 동시성 문제를 해결한다.
package com.example.couponcore.service; import com.example.couponcore.model.Coupon; import com.example.couponcore.repository.mysql.CouponJpaRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service public class CouponIssueService { private final CouponJpaRepository couponJpaRepository; @Transactional public void issue(long couponId, long userId) { // 특정 사용자가 특정 쿠폰을 발급하는 로직 Coupon coupon = findCouponWithLock(couponId); // 레코드 락을 걸어 쿠폰 조회 coupon.issue(); // 쿠폰 발급 처리 (issuedQuantity 증가) saveCouponIssue(couponId, userId); // 쿠폰 발급 정보 저장 } @Transactional public Coupon findCouponWithLock(long couponId) { return couponJpaRepository.findCouponWithLock(couponId).orElseThrow(() -> { throw new IllegalArgumentException("쿠폰이 존재하지 않습니다: " + couponId); }); } private void saveCouponIssue(long couponId, long userId) { // 쿠폰 발급 정보 저장 로직 } }
Java
복사
코드 요소
설명
findCouponWithLock
Pessimistic Lock을 적용하여 쿠폰 데이터를 조회

성능테스트 결론

MySQL Lock (v1.3)

버전 정보

버전
변경 사항
v1.3
동시성 문제: MySQL Lock - CouponJpaRepository.java - CouponIssueService.java

테스트 결과

테스트 환경: Local 환경에서 성능 테스트 3회 실시, 평균 값 도출
소스코드버전정보
인프라 환경
쿠폰 발급 수량 비교(정상발급확인)
#Request
#Fails
Failures (%)
Average(ms)
RPS
MySQL CPU(%)
Redis CPU(%)
v1.3
local
일치
46,585
13,020
29.56
6,378.69
161.27
63.04
0.12

테스트 결과 요약

MySQL의 비관적 락(SELECT FOR UPDATE)을 사용한 결과,
쿠폰 발급 수량은 정확하게 일치하였으나,
평균 응답 시간(Average ms)이 상대적으로 길어졌고 RPS는 낮아졌다.
MySQL CPU 사용률이 크게 증가하였으며, Redis 리소스 활용은 거의 없었다.
Failures(%)는 29.56%로 감소했으나 여전히 개선의 여지가 있다.
MySQL Lock은 데이터 정합성을 보장하지만, 성능 측면에서 Redis Lock보다 병목 현상이 두드러진다.
Search
Main PageCategoryTagskkogggokkAbout MeContact