Skip to content

Commit

Permalink
Merge pull request #134 from f-lab-edu/feature/#133
Browse files Browse the repository at this point in the history
[Dev] 재고 감소 로직 Redis로 전환
  • Loading branch information
pak0426 authored Jul 21, 2024
2 parents 1889f0c + 8c27497 commit 66e165a
Show file tree
Hide file tree
Showing 11 changed files with 215 additions and 93 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'mysql:mysql-connector-java:8.0.33'
implementation 'org.redisson:redisson-spring-boot-starter:3.31.0'
implementation 'org.springframework.kafka:spring-kafka'
// implementation 'org.springframework.kafka:spring-kafka'
implementation 'com.google.code.gson:gson'

compileOnly 'org.projectlombok:lombok'
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/mini/joymall/JoyMallApplication.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.mini.joymall;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableScheduling
@SpringBootApplication
@ConfigurationPropertiesScan
public class JoyMallApplication {
Expand Down
20 changes: 10 additions & 10 deletions src/main/java/com/mini/joymall/commons/config/KafkaTopicConfig.java
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();
// }
}
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);
}
}
}
}
}
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import com.mini.joymall.order.dto.request.CreateOrderRequest;
import com.mini.joymall.order.dto.response.CreateOrderResponse;
import com.mini.joymall.sale.service.SalesProductFacade;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Service;
Expand All @@ -28,7 +27,7 @@ public class OrderService {
public OrderService(OrderValidator orderValidator,
OrderRepository orderRepository,
CustomerAddressRepository addressRepository,
@Qualifier("salesProductFacadeKafka") SalesProductFacade salesProductFacade) {
@Qualifier("salesProductFacadeRedis") SalesProductFacade salesProductFacade) {
this.orderValidator = orderValidator;
this.orderRepository = orderRepository;
this.addressRepository = addressRepository;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.mini.joymall.order.domain.entity.OrderItem;
import com.mini.joymall.order.dto.OrderItemDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.kafka.core.KafkaTemplate;
//import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;

import java.util.Set;
Expand All @@ -14,14 +14,14 @@
@RequiredArgsConstructor
public class SalesProductFacadeKafka implements SalesProductFacade {

private final KafkaTemplate<String, String> kafkaTemplate;
// private final KafkaTemplate<String, String> kafkaTemplate;
private static final String TOPIC = "new-stock-decrease";
private final Gson gson = new Gson();

@Override
public void decreaseStock(Set<OrderItem> orderItems) {
Set<OrderItemDTO> items = orderItems.stream().map(OrderItemDTO::from).collect(Collectors.toSet());
String json = gson.toJson(items);
kafkaTemplate.send(TOPIC, json);
// kafkaTemplate.send(TOPIC, json);
}
}
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);
}
}
}
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();
}
}
}
}
}
Loading

0 comments on commit 66e165a

Please sign in to comment.