인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

인프런 커뮤니티 질문&답변

소리쨩님의 프로필 이미지

작성한 질문수

재고시스템으로 알아보는 동시성이슈 해결방법

Lettuce를 작성하여 재고감소 로직 작성하기

LettureLockStockFacadeTest에서 오류가 발생합니다.

작성

·

169

1

package com.example.stock.repository;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;

@Component
public class RedisLockRepository {

    private RedisTemplate<String, String> redisTemplate;

    public RedisLockRepository(RedisTemplate<String, String> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public Boolean lock(Long key) {
        return redisTemplate
                .opsForValue()
                .setIfAbsent(generateKey(key), "lock", Duration.ofMillis(3_000));
    }

    public void unlock(Long key) {
        redisTemplate.delete(generateKey(key));
    }

    private String generateKey(Long key) {
        return key.toString();
    }
}
package com.example.stock.facade;

import com.example.stock.repository.RedisLockRepository;
import com.example.stock.service.StockService;
import org.springframework.stereotype.Component;

@Component
public class LettuceLockStockFacade {

    private final RedisLockRepository redisLockRepository;

    private final StockService stockService;

    public LettuceLockStockFacade(RedisLockRepository redisLockRepository, StockService stockService) {
        this.redisLockRepository = redisLockRepository;
        this.stockService = stockService;
    }

    public void decrease(Long key, Long quantity) throws InterruptedException {
        while(!redisLockRepository.lock(key)){
            Thread.sleep(100);
        }
        try{
            stockService.decrease(key, quantity);
        }finally {
            redisLockRepository.unlock(key);
        }
    }
}
package com.example.stock.facade;

import com.example.stock.domain.Stock;
import com.example.stock.repository.StockRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class LettuceLockStockFacadeTest {
    @Autowired
    private LettuceLockStockFacade lettuceLockStockFacade;

    @Autowired
    private StockRepository stockRepository;

    @BeforeEach
    public void before(){
        stockRepository.saveAndFlush(new Stock(1L, 100L));
    }

    @AfterEach
    public void after(){
        stockRepository.deleteAll();
    }

    @Test
    public void 동시에_100개의_요청() throws InterruptedException {
        int threadCount = 100;
        ExecutorService executorService = Executors.newFixedThreadPool(32);
        CountDownLatch latch = new CountDownLatch(threadCount);

        for(int i = 0; i < threadCount; i++){
            executorService.submit(() -> {
                try{
                    lettuceLockStockFacade.decrease(1L, 1L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                } finally {
                    latch.countDown();
                }
            });
        }
        latch.await();

        Stock stock = stockRepository.findById(1L).orElseThrow();
        assertEquals(0, stock.getQuantity());
    }
}

이렇게 코딩했고 dockre에 redis를 설치하고 window Shell에서 데이터가 남아 있을까봐 1번 key 값을 삭제해서 돌렸는데도 오류가 발생했습니다.

또한 레디스가 실행이 되지 않은 채 했을까봐 ping을 입력했는데 pong이라는 응답을 받았습니다.

Hibernate: drop table if exists stock
2024-11-03T22:08:40.414+09:00 DEBUG 17108 --- [    Test worker] org.hibernate.SQL                        : create table stock (id bigint not null auto_increment, product_id bigint, quantity bigint, version bigint, primary key (id)) engine=InnoDB
Hibernate: create table stock (id bigint not null auto_increment, product_id bigint, quantity bigint, version bigint, primary key (id)) engine=InnoDB
2024-11-03T22:08:40.439+09:00  INFO 17108 --- [    Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2024-11-03T22:08:41.136+09:00  INFO 17108 --- [    Test worker] o.s.d.j.r.query.QueryEnhancerFactory     : Hibernate is in classpath; If applicable, HQL parser will be used.
2024-11-03T22:08:42.092+09:00  WARN 17108 --- [    Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2024-11-03T22:08:42.990+09:00  INFO 17108 --- [    Test worker] c.e.s.facade.LettuceLockStockFacadeTest  : Started LettuceLockStockFacadeTest in 6.21 seconds (process running for 7.345)
2024-11-03T22:08:43.504+09:00 DEBUG 17108 --- [    Test worker] org.hibernate.SQL                        : insert into stock (product_id,quantity,version) values (?,?,?)
Hibernate: insert into stock (product_id,quantity,version) values (?,?,?)
2024-11-03T22:08:44.203+09:00 DEBUG 17108 --- [    Test worker] org.hibernate.SQL                        : select s1_0.id,s1_0.product_id,s1_0.quantity,s1_0.version from stock s1_0 where s1_0.id=?
Hibernate: select s1_0.id,s1_0.product_id,s1_0.quantity,s1_0.version from stock s1_0 where s1_0.id=?
2024-11-03T22:08:44.245+09:00 DEBUG 17108 --- [    Test worker] org.hibernate.SQL                        : select s1_0.id,s1_0.product_id,s1_0.quantity,s1_0.version from stock s1_0
Hibernate: select s1_0.id,s1_0.product_id,s1_0.quantity,s1_0.version from stock s1_0
2024-11-03T22:08:44.252+09:00 DEBUG 17108 --- [    Test worker] org.hibernate.SQL                        : delete from stock where id=? and version=?
Hibernate: delete from stock where id=? and version=?

Expected :0
Actual   :100
<Click to see difference>

org.opentest4j.AssertionFailedError: expected: <0> but was: <100>
	at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151)
	at org.junit.jupiter.api.AssertionFailureBuilder.buildAndThrow(AssertionFailureBuilder.java:132)
	at org.junit.jupiter.api.AssertEquals.failNotEqual(AssertEquals.java:197)
	at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
	at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
	at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:639)
	at com.example.stock.facade.LettuceLockStockFacadeTest.동시에_100개의_요청(LettuceLockStockFacadeTest.java:55)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
	at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)


Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

 

어디를 더 수정해야 하는지 모르겠습니다..

답변 2

0

최상용님의 프로필 이미지
최상용
지식공유자

소리쨩님 안녕하세요.
첫번째로 강의에 첨부드린 github 에 있는 소스로 해도 테스트가 통과하지 않는지 궁금합니다.
두번째로 테스트가 통과하지 않는 소스코드를 github 에 업로드 해주시고 주소를 공유해주실 수 있으실까요?

디버깅을 해보면 좋을거같습니다

 

0

인프런 AI 인턴님의 프로필 이미지

안녕하세요, 인프런 AI 인턴입니다.

소리쨩님께서 직면하고 있는 문제는 Redis를 통한 분산 락이 예상대로 작동하지 않아서 발생하는 것으로 보입니다. Redis의 setIfAbsent 메서드가 기대한 대로 작동하지 않거나, Redis의 연결이나 설정에 문제가 있을 수 있습니다. 다음과 같은 점검 사항을 확인해 보면 좋겠습니다:

  1. Redis 연결 확인: Redis가 제대로 연결되어 있고 set 및 delete 명령이 올바르게 수행되는지 스프링 부트 애플리케이션과 Redis 간의 연결을 재차 확인해 보시기 바랍니다.

  2. 분산 락 시간 설정: setIfAbsent 메서드를 사용하여 락이 설정될 때 유효 시간을 지정하고 있습니다. 그러나 이 유효 시간이 완료되기 전에 스레드가 작업을 완료하지 못할 경우, 기존 락이 만료되어 다른 스레드가 다시 작업을 시도할 수 있습니다. 유효 시간을 늘려보는 것도 방법이 될 수 있습니다.

  3. 로그 추가: 애플리케이션에 로그를 추가하여 락이 언제 발급되고 해제되는지를 기록해 보세요. 이렇게 하면 어느 부분에서 문제가 발생하는지 더 명확히 알 수 있습니다.

  4. 트랜잭션 관리: stockService.decrease 메서드의 구현이 중요합니다. 해당 메서드가 스레드 세이프하게 작동하는지, 트랜잭션 내에서 작업이올바르게 처리되고 있는지 다시 확인하십시오.

추가적으로 로그와 코드를 확인하면서 문제를 해결해 보시길 권장드립니다.계속해서 난점이 해결되지 않는다면, 코드의 타 부분이나 잘못된 설정을 찾아내기 위한 보다 상세한 디버깅이 필요할 수 있습니다.

레퍼런스 정보에서는 Redis에 대한 정보와 Lettuce 및 Jedis 설정 방법을 제공하고 있습니다. 필요하시면 다음 URL을 참조하시기 바랍니다:

Redis Lettuce to Jedis 설정 참고