작성
·
67
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의 연결이나 설정에 문제가 있을 수 있습니다. 다음과 같은 점검 사항을 확인해 보면 좋겠습니다:
Redis 연결 확인: Redis가 제대로 연결되어 있고 set 및 delete 명령이 올바르게 수행되는지 스프링 부트 애플리케이션과 Redis 간의 연결을 재차 확인해 보시기 바랍니다.
분산 락 시간 설정: setIfAbsent
메서드를 사용하여 락이 설정될 때 유효 시간을 지정하고 있습니다. 그러나 이 유효 시간이 완료되기 전에 스레드가 작업을 완료하지 못할 경우, 기존 락이 만료되어 다른 스레드가 다시 작업을 시도할 수 있습니다. 유효 시간을 늘려보는 것도 방법이 될 수 있습니다.
로그 추가: 애플리케이션에 로그를 추가하여 락이 언제 발급되고 해제되는지를 기록해 보세요. 이렇게 하면 어느 부분에서 문제가 발생하는지 더 명확히 알 수 있습니다.
트랜잭션 관리: stockService.decrease
메서드의 구현이 중요합니다. 해당 메서드가 스레드 세이프하게 작동하는지, 트랜잭션 내에서 작업이올바르게 처리되고 있는지 다시 확인하십시오.
추가적으로 로그와 코드를 확인하면서 문제를 해결해 보시길 권장드립니다.계속해서 난점이 해결되지 않는다면, 코드의 타 부분이나 잘못된 설정을 찾아내기 위한 보다 상세한 디버깅이 필요할 수 있습니다.
레퍼런스 정보에서는 Redis에 대한 정보와 Lettuce 및 Jedis 설정 방법을 제공하고 있습니다. 필요하시면 다음 URL을 참조하시기 바랍니다: