-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
[Dev] 재고 감소 로직 Redis로 전환
- Loading branch information
Showing
11 changed files
with
215 additions
and
93 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 10 additions & 10 deletions
20
src/main/java/com/mini/joymall/commons/config/KafkaTopicConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,18 @@ | ||
package com.mini.joymall.commons.config; | ||
|
||
import org.apache.kafka.clients.admin.NewTopic; | ||
import org.springframework.context.annotation.Bean; | ||
//import org.apache.kafka.clients.admin.NewTopic; | ||
//import org.springframework.context.annotation.Bean; | ||
//import org.springframework.kafka.config.TopicBuilder; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.kafka.config.TopicBuilder; | ||
|
||
@Configuration | ||
public class KafkaTopicConfig { | ||
|
||
@Bean | ||
public NewTopic newStockDecreaseTopic() { | ||
return TopicBuilder.name("new-stock-decrease") | ||
.partitions(1) | ||
.replicas(3) | ||
.build(); | ||
} | ||
// @Bean | ||
// public NewTopic newStockDecreaseTopic() { | ||
// return TopicBuilder.name("new-stock-decrease") | ||
// .partitions(1) | ||
// .replicas(3) | ||
// .build(); | ||
// } | ||
} |
52 changes: 52 additions & 0 deletions
52
src/main/java/com/mini/joymall/commons/schedule/SalesProductSyncScheduler.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package com.mini.joymall.commons.schedule; | ||
|
||
import com.mini.joymall.sale.domain.entity.SalesProduct; | ||
import com.mini.joymall.sale.domain.repository.SalesProductRepository; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.scheduling.annotation.Scheduled; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.NoSuchElementException; | ||
import java.util.Set; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class SalesProductSyncScheduler { | ||
|
||
private final SalesProductRepository salesProductRepository; | ||
private final RedisTemplate<String, String> redisTemplate; | ||
private final SchedulerLeader schedulerLeader; | ||
private static final String CHANGE_LOG_KEY = "salesProduct_stock_change_log"; | ||
|
||
@Scheduled(fixedRate = 6000) | ||
public void syncStockToDB() { | ||
if (!schedulerLeader.isLeader()) { | ||
System.out.println("not Leader"); | ||
return; | ||
} | ||
|
||
Set<String> stockKeys = redisTemplate.opsForSet().members(CHANGE_LOG_KEY); | ||
if (stockKeys.isEmpty()) return; | ||
|
||
for (String stockKey : stockKeys) { | ||
if (stockKey.isEmpty()) { | ||
return; | ||
} | ||
|
||
Long salesProductId = Long.valueOf(stockKey.replace("salesProduct_stock:", "")); | ||
String stockInRedis = redisTemplate.opsForValue().get(stockKey); | ||
|
||
if (stockInRedis != null) { | ||
int stock = Integer.parseInt(stockInRedis); | ||
SalesProduct salesProduct = salesProductRepository.findById(salesProductId) | ||
.orElseThrow(NoSuchElementException::new); | ||
|
||
if (salesProduct != null) { | ||
salesProduct.decreaseStock(salesProduct.getSalesStock() - stock); | ||
salesProductRepository.save(salesProduct); | ||
} | ||
} | ||
} | ||
} | ||
} |
30 changes: 30 additions & 0 deletions
30
src/main/java/com/mini/joymall/commons/schedule/SchedulerLeader.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package com.mini.joymall.commons.schedule; | ||
|
||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.UUID; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class SchedulerLeader { | ||
|
||
private final RedisTemplate<String, String> redisTemplate; | ||
|
||
private static final String LEADER_KEY = "scheduler:leader"; | ||
private static final long LEADER_TIMEOUT = 60000; // 60 seconds | ||
private String instanceId = UUID.randomUUID().toString(); | ||
|
||
public boolean isLeader() { | ||
Boolean isLeader = redisTemplate.opsForValue().setIfAbsent(LEADER_KEY, instanceId, LEADER_TIMEOUT, TimeUnit.MILLISECONDS); | ||
|
||
if (Boolean.TRUE.equals(isLeader) || instanceId.equals(redisTemplate.opsForValue().get(LEADER_KEY))) { | ||
redisTemplate.expire(LEADER_KEY, LEADER_TIMEOUT, TimeUnit.MILLISECONDS); | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
42 changes: 18 additions & 24 deletions
42
src/main/java/com/mini/joymall/sale/service/SalesProductFacadeRedis.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,44 +1,38 @@ | ||
package com.mini.joymall.sale.service; | ||
|
||
import com.mini.joymall.order.domain.entity.OrderItem; | ||
import jakarta.annotation.PostConstruct; | ||
import lombok.RequiredArgsConstructor; | ||
import org.redisson.api.RLock; | ||
import org.redisson.api.RedissonClient; | ||
import org.springframework.data.redis.core.RedisTemplate; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.Set; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
@Component("salesProductFacadeRedis") | ||
@RequiredArgsConstructor | ||
public class SalesProductFacadeRedis implements SalesProductFacade { | ||
|
||
private final RedissonClient redissonClient; | ||
private final SalesProductService salesProductService; | ||
|
||
private static final String LOCK_KEY_PREFIX = "salesProduct:"; | ||
private final RedisTemplate<String, String> redisTemplate; | ||
private static final String STOCK_KEY_PREFIX = "salesProduct_stock:"; | ||
private static final String CHANGE_LOG_KEY = "salesProduct_stock_change_log"; | ||
|
||
@Override | ||
public void decreaseStock(Set<OrderItem> orderItems) { | ||
for (OrderItem orderItem : orderItems) { | ||
String lockKey = LOCK_KEY_PREFIX + orderItem.getSalesProductId(); | ||
RLock lock = redissonClient.getLock(lockKey); | ||
|
||
try { | ||
boolean acquireLock = lock.tryLock(10, 1, TimeUnit.SECONDS); | ||
|
||
if (!acquireLock) { | ||
throw new RuntimeException("SalesProduct Lock 획득 실패"); | ||
} | ||
salesProductService.decreaseStock(orderItem); | ||
} catch (InterruptedException e) { | ||
Thread.currentThread().interrupt(); | ||
throw new RuntimeException("Lock 획득 중 인터럽트 발생"); | ||
} finally { | ||
if (lock.isHeldByCurrentThread()) { | ||
lock.unlock(); | ||
} | ||
String stockKey = STOCK_KEY_PREFIX + orderItem.getSalesProductId(); | ||
|
||
if (redisTemplate.opsForValue().get(stockKey) == null) { | ||
redisTemplate.opsForValue().set(stockKey, "1000000"); | ||
} | ||
|
||
Long remainStock = redisTemplate.opsForValue().decrement(stockKey, orderItem.getQuantity()); | ||
|
||
if (remainStock == null || remainStock < 0) { | ||
redisTemplate.opsForValue().increment(stockKey, orderItem.getQuantity()); | ||
throw new RuntimeException("판매 수량이 부족합니다."); | ||
} | ||
|
||
redisTemplate.opsForSet().add(CHANGE_LOG_KEY, stockKey); | ||
} | ||
} | ||
} |
44 changes: 44 additions & 0 deletions
44
src/main/java/com/mini/joymall/sale/service/SalesProductFacadeRedisson.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package com.mini.joymall.sale.service; | ||
|
||
import com.mini.joymall.order.domain.entity.OrderItem; | ||
import lombok.RequiredArgsConstructor; | ||
import org.redisson.api.RLock; | ||
import org.redisson.api.RedissonClient; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.util.Set; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
@Component("salesProductFacadeRedisson") | ||
@RequiredArgsConstructor | ||
public class SalesProductFacadeRedisson implements SalesProductFacade { | ||
|
||
private final RedissonClient redissonClient; | ||
private final SalesProductService salesProductService; | ||
|
||
private static final String LOCK_KEY_PREFIX = "salesProduct:"; | ||
|
||
@Override | ||
public void decreaseStock(Set<OrderItem> orderItems) { | ||
for (OrderItem orderItem : orderItems) { | ||
String lockKey = LOCK_KEY_PREFIX + orderItem.getSalesProductId(); | ||
RLock lock = redissonClient.getLock(lockKey); | ||
|
||
try { | ||
boolean acquireLock = lock.tryLock(10, 1, TimeUnit.SECONDS); | ||
|
||
if (!acquireLock) { | ||
throw new RuntimeException("SalesProduct Lock 획득 실패"); | ||
} | ||
salesProductService.decreaseStock(orderItem); | ||
} catch (InterruptedException e) { | ||
Thread.currentThread().interrupt(); | ||
throw new RuntimeException("Lock 획득 중 인터럽트 발생"); | ||
} finally { | ||
if (lock.isHeldByCurrentThread()) { | ||
lock.unlock(); | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.