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

소리쨩님의 프로필 이미지

작성한 질문수

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

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

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

작성

·

29

0

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 인턴입니다.

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

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

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

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

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

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

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

Redis Lettuce to Jedis 설정 참고