Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] 음식 상품 재고수 Redis 캐싱 #145

Merged

Conversation

june-777
Copy link
Member

💡 다음 이슈를 해결했어요.

Discussion Link

Issue Link


💡 이슈를 처리하면서 추가된 코드가 있어요.

RedisMenuStockCacheService

Redisson 클라이언트를 기반으로 메뉴 재고수를 캐싱 및 동기화하는 역할을 담당합니다.

package camp.woowak.lab.infra.cache.redis;
import org.redisson.api.RAtomicLong;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;
import camp.woowak.lab.infra.cache.MenuStockCacheService;
import camp.woowak.lab.infra.cache.exception.CacheMissException;
import camp.woowak.lab.infra.cache.exception.RedisLockAcquisitionException;
import camp.woowak.lab.menu.exception.NotEnoughStockException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
@RequiredArgsConstructor
public class RedisMenuStockCacheService implements MenuStockCacheService {
private final RedissonClient redissonClient;
public Long updateStock(Long menuId, Long stock) {
// redis 에 {메뉴ID: 재고} 가 있는 지 확인
try {
for (int retryCount = 0; retryCount < RedisCacheConstants.RETRY_COUNT; retryCount++) {
// 캐시에 재고가 있는 지 확인
RAtomicLong cachedStock = redissonClient.getAtomicLong(RedisCacheConstants.MENU_STOCK_PREFIX + menuId);
if (cachedStock.isExists()) { // 캐시에 재고가 있는 경우: 캐시 반환
return cachedStock.get();
}
// 캐시에 재고가 없는 경우: 락 확인
// 락이 걸려있지 않은 경우 락 걸고 {메뉴ID:재고} 캐시 생성
if (doWithLock(RedisCacheConstants.MENU_PREFIX + menuId,
() -> cachedStock.set(stock))) { // action 성공 시 재시도 탈출
break;
}
}
return stock;
} catch (Exception e) { // redis client 를 연결하지 못하면 넘어가도록 처리
log.error("[Redis Cache Error]: {}", e.getMessage());
return stock;
}
}
@Override
public Long addAtomicStock(Long menuId, int amount) {
RAtomicLong cachedStock = redissonClient.getAtomicLong(RedisCacheConstants.MENU_STOCK_PREFIX + menuId);
if (!cachedStock.isExists()) { // 캐시 미스
throw new CacheMissException("메뉴 재고 캐시 미스");
}
// cache 원자적 재고감소
Long newStock = cachedStock.addAndGet(amount);
if (newStock < 0) {
// 원복
cachedStock.addAndGet(-amount);
throw new NotEnoughStockException("MenuId(" + menuId + ") 재고가 부족합니다.");
}
return newStock;
}
public boolean doWithLock(String key, Runnable runnable) {
RLock lock = redissonClient.getLock(RedisCacheConstants.LOCK_PREFIX + key);
boolean lockAcquired = false;
try {
lockAcquired = lock.tryLock(
RedisCacheConstants.LOCK_WAIT_TIME,
RedisCacheConstants.LOCK_LEASE_TIME,
RedisCacheConstants.LOCK_TIME_UNIT
);
if (!lockAcquired) {
return false;
}
runnable.run();
return true;
} catch (InterruptedException e) {
throw new RedisLockAcquisitionException("[redis lock] 락 획득에 실패했습니다.", e);
} finally {
if (lockAcquired) {
releaseLock(lock, RedisCacheConstants.LOCK_PREFIX + key);
}
}
}
private void releaseLock(RLock rLock, String key) {
try {
if (rLock.isHeldByCurrentThread()) {
rLock.unlock();
log.info("Released lock with key {}", key);
}
} catch (IllegalMonitorStateException e) {
// 이 예외가 발생하면 심각한 로직 오류일 수 있으므로 ERROR 레벨로 로깅
log.error(
"Unexpected state: Failed to release lock with key {}. This might indicate a serious logic error.", key,
e);
// 예외를 다시 던지지 않고, 상위 레벨에서 처리하도록 함
}
}
}

데이터 정합성 및 동시성 테스트

카트 담기/주문 결제 동시성 테스트 / Redis 재고수 캐싱-RDB 재고수 정합성 테스트



💡 이런 고민을 했어요.

Cache Warming

메뉴 재고수를 어느 시점에 캐싱해둘지 고민했습니다.

  • 방안1. 서버 로드 시점에 모든 메뉴 재고수를 캐싱 ❌

  • 방안2. 서버 로드 시점에 인기 가게의 메뉴만 재고수를 캐싱 ❌

  • 방안3. 카트에 메뉴를 담는 시점에 캐싱 ✅

  • 방안1의 경우 Redis 메모리 이슈가 있을 것으로 판단했습니다.

    • 현재 저희 서비스는 메뉴가 750만건 있는데, 이를 모두 캐싱하기엔 무리가 있다고 판단했습니다.
  • 방안2의 경우 서비스 운영 시점에 고려해볼만하다 판단했습니다.

  • 주문을 하기 위해선 카트에 메뉴를 담는 과정이 선행되어야하기 때문에, 해당 시점에 캐싱해두는 것이 적절하다 판단했습니다.

재고 수량 Redis-RDB 정합성 문제

  • 방안1. 주문 시점에 RDB 갱신

  • 방안2. 임의 시점에 RDB 갱신 스케줄링

  • 방안2가 DB connection 관리에 용이하지만 현재 서버가 죽을 경우 데이터 정합성 문제에 대한 대책이 없기 떄문에 방안1을 채택함.



✅ 셀프 체크리스트

  • 내 코드를 스스로 검토했습니다.
  • 필요한 테스트를 추가했습니다.
  • 모든 테스트를 통과합니다.
  • 브랜치 전략에 맞는 브랜치에 PR을 올리고 있습니다.
  • 커밋 메세지를 컨벤션에 맞추었습니다.
  • wiki를 수정했습니다.

kimhyun5u and others added 30 commits August 24, 2024 17:28
- Redis 설정 추가

Co-authored-by: june-777 <[email protected]>
- Reddison 의존성 추가

Co-authored-by: june-777 <[email protected]>
-  Redis 메뉴 재고 캐싱 기능 테스트
    - 재고수가 캐싱되어 있지 않고, 메뉴ID에 락이 걸려있지 않는 경우
    - 재고수가 캐싱되어 있지 않고, 메뉴ID에 락이 걸려있는 경우
    - 재고수가 캐싱되어 있는 경우
    - RedissonClient 가 Transaction 생성에 실패하는 경우
    - RedissonClient 가 Lock 생성에 실패하는 경우
    - Redis 연결이 중간에 실패하는 경우

Co-authored-by: june-777 <[email protected]>
- 음식 상품 재고 수 레디스 캐싱 기능 구현

Co-authored-by: june-777 <[email protected]>
- Redis 설정 및 캐시 관련 상수

Co-authored-by: june-777 <[email protected]>
- `@Disabled` 추가

Co-authored-by: june-777 <[email protected]>
- CartService: RedisMenuStockCacheService -> MenuStockCacheService 변경
- FakeMenuStockCacheService: 테스트에서 사용할 FakeMenuStockCacheService
- MenuStockCacheService: 음식 상품 재고 캐시 인터페이스

Co-authored-by: june-777 <[email protected]>
- 음식 상품 재고 Redis 캐시 적용 시 다른 트랜잭션에서 발생한 익셉션에 의한 롤백을 적용시키기 위해 구조 변경

Co-authored-by: june-777 <[email protected]>
- StockRequester.request: 변경된 재고를 롤백하기 위해 request 에서 성공한 변경 사항 반환하도록 수정
- StockRequester.rollback: 카트 아이템 리스트를 받아서 다시 재고 수량을 롤백하는 기능 구현

Co-authored-by: june-777 <[email protected]>
- RedisMenuStockCacheService.doWithMenuIdLock: 기존 락을 걸고 수행하는 작업을 분리해서 어떤 작업을 락을 걸고 수행할 수 있는 메서드로 분리

- RedisMenuStockCacheService.addAtomicStock: 음식 상품 재고에 대해서 원자적 연산을 수행하는 메서드

Co-authored-by: june-777 <[email protected]>
- doWithMenuIdLock(Long, Runnable) -> doWithLock(String, Runnable)
- 메뉴ID에 한정하지 않기 위함

Co-authored-by: june-777 <[email protected]>
- 재고 업데이트 연산이 원자적으로 일어나지 않는 문제 해결

Co-authored-by: june-777 <[email protected]>
- 락 해제 실패 시 예외 핸들링 추가
- 락을 획득했을 때만 해제 시도하도록 수정
- 인터럽트 예외 시 RedisLockAcquisitionException 예외 던지도록 수정

Co-authored-by: june-777 <[email protected]>
- MENU_PREFIX 추가

Co-authored-by: june-777 <[email protected]>
- RedissonClient 동작 검증을 위한 코드

Co-authored-by: june-777 <[email protected]>
- 재고 감소 재시도 로직 추가

Co-authored-by: june-777 <[email protected]>
- TODO 제거

Co-authored-by: june-777 <[email protected]>
- decreaseStock 추가

Co-authored-by: june-777 <[email protected]>
- 사용하지 않는 메소드 제

Co-authored-by: june-777 <[email protected]>
- StockRequester 의 변화에 의한 테스트 코드 변경

Co-authored-by: june-777 <[email protected]>
- StockRequester 의 변화에 의한 테스트 코드 변경

Co-authored-by: june-777 <[email protected]>
kimhyun5u and others added 5 commits August 26, 2024 16:27
- 레디스에서 반환된 재고를 업데이트 하는 게 아닌 검증된 주문의 아이템 수량을 감소하도록 로직 변경

Co-authored-by: june-777 <[email protected]>
테스트 시나리오:
- 최소 주문 금액 미만으로 주문 실패시, 캐싱된 재고수와 RDB 재고수가 롤백
- 계좌 잔액 부족으로 주문 실패시, 캐싱된 재고수와 RDB 재고수가 롤백

Co-authored-by: kimhyun5u <[email protected]>
@june-777 june-777 added the 💪 Improve 기능 고도화 & 개선 label Aug 26, 2024
@june-777 june-777 added this to the 1차 고도화 milestone Aug 26, 2024
@june-777 june-777 linked an issue Aug 26, 2024 that may be closed by this pull request
@june-777 june-777 linked an issue Aug 26, 2024 that may be closed by this pull request
kimhyun5u and others added 6 commits August 27, 2024 20:20
- 데드락 해결

Co-authored-by: june-777 <[email protected]>
…kimhyun5u_음식-상품-재고-캐싱

# Conflicts:
#	build.gradle
#	src/test/java/camp/woowak/lab/container/ContainerSettingTest.java
…kimhyun5u_음식-상품-재고-캐싱

# Conflicts:
#	build.gradle
#	src/main/java/camp/woowak/lab/infra/config/RedissonConfiguration.java
#	src/main/resources/application.yaml
#	src/test/resources/application.yml
Copy link
Contributor

@Hyeon-Uk Hyeon-Uk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM !

kimhyun5u and others added 3 commits August 28, 2024 22:03
- redis 네트워크 한번만 타도록 변경

Co-authored-by: Dr-KoKo <[email protected]>
- 임계값 테스트를 통과하지 않는 코드 수정

Co-authored-by: Dr-KoKo <[email protected]>
Copy link
Member

@Dr-KoKo Dr-KoKo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@kimhyun5u kimhyun5u merged commit 537cdf3 into main Aug 28, 2024
1 check passed
@kimhyun5u kimhyun5u deleted the feature/143_june-777-kimhyun5u_음식-상품-재고-캐싱 branch August 28, 2024 13:12
@kimhyun5u kimhyun5u restored the feature/143_june-777-kimhyun5u_음식-상품-재고-캐싱 branch August 28, 2024 13:12
@kimhyun5u kimhyun5u deleted the feature/143_june-777-kimhyun5u_음식-상품-재고-캐싱 branch September 19, 2024 09:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
💪 Improve 기능 고도화 & 개선
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

[기능] 음식 상품 재고 캐싱
4 participants