Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/distribute lock #11

Open
wants to merge 30 commits into
base: feature/order
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bfe706e
📝 Docs : 문서 수정
haxr369 Apr 17, 2024
11fb85e
✨ Feat : 클라이언트 주문 요청 API 작성
haxr369 Apr 18, 2024
b910033
✨ Feat : ProductService 재고 확인 기능 추가
haxr369 Apr 18, 2024
8fd7f7e
✨ Feat : 새로운 기능
haxr369 Apr 18, 2024
168ea41
✨ Feat : 주문 생성 및 유형 제품과 연결 시도
haxr369 Apr 19, 2024
b200e12
🐛 Fix : 데이터 생성 문제 해결
haxr369 Apr 19, 2024
158732b
✨ Feat : 핵심제품에 재고량 확인 가능하도록 수정
haxr369 Apr 20, 2024
ae1b0b0
✅ Test : 핵심제품 재고 업데이트 테스트 작성
haxr369 Apr 20, 2024
b474fa9
✅ Test : 여러 transaction에서 동시에 한 레코드를 수정할 때 동시성 문제 발생시키기
haxr369 Apr 21, 2024
f2ab0da
✅ Test : 비관적락 낙관적락 전략 적용
haxr369 Apr 22, 2024
59997e0
✨ Feat : 낙관적 락의 예외를 처리할 수 있도록 메서드 추가
haxr369 Apr 22, 2024
9ffdf64
🤖 Refactor : 응답을 ResponseEntity를 상속해서 헤더를 사용할 수 있게함. 추후 리다이렉트 사용할 때 용…
haxr369 Apr 23, 2024
b6cdd0c
🤖 Refactor : ResponseEntity를 주로 사용하도록 응답 수정
haxr369 Apr 23, 2024
bd4ecad
✨ Feat : 결제 서버 연결할 준비
haxr369 Apr 23, 2024
9b5d1f3
✨ Feat : 결제 요청 기능 구현
haxr369 Apr 23, 2024
a35bf01
✨ Feat : 결제 결과 요청하도록 confirm 수정
haxr369 Apr 24, 2024
f7f8a90
✨ Feat : 결제 결과 받아서 주문 확정 시작
haxr369 Apr 26, 2024
367a68f
✨ Feat : 새로운 기능
haxr369 Apr 27, 2024
ab157a0
✅ Test : 분산락 이용해서 재고 감소를 락을 가진 쓰레드만 할 수 있게함
haxr369 Apr 28, 2024
62816e2
✅ Test : 분산락에 대한 테스트 구현
haxr369 Apr 28, 2024
bf35b61
✨ Feat : 롤백 로직 추가 완료
haxr369 Apr 28, 2024
1168d6c
✨ Feat : 수량을 일정 개수 미만으로 주문하면 정상적으로 롤백 할 수 있도록 로직 작성
haxr369 Apr 29, 2024
e849c9b
🐛 Fix : 분산락 이용해서 서로 다른 세션에서 동시성 문제 해결
haxr369 Apr 29, 2024
966828c
🐛 Fix : 결제 시 결과를 반환 과정에서 주문을 찾지 못하는 문제 해결
haxr369 May 2, 2024
8c7420a
✨ Feat : 클라이언트의 요청마다 다른 락 id를 사용해서 주문 간 격리를 높이고자 한다.
haxr369 May 4, 2024
8caa6c2
버그
haxr369 May 4, 2024
f14ff52
Fix : 롤백하면서 재고량이 증가하지 않고, uncommited read 문제가 발생하는 상황 해결
haxr369 May 7, 2024
b050c70
🤖 Refactor : 주문 서비스와 상품 서비스를 분리 추후 상품 서버를 새롭게 만들기 쉽도록 주문과 상품 도메인 간 의존…
haxr369 May 8, 2024
ae1620d
Readme에 시퀀스 다이어그램 추가
haxr369 May 9, 2024
6879cac
🤖 Refactor : 코드 리팩토링
haxr369 May 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
.env.local
.gitignore
README.md
.gitmessage
.gitmessage.txt
LICENSE
SunStyle_edited.xml
codecov.yml
Expand Down
File renamed without changes.
93 changes: 91 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,95 @@
![Codecov](https://img.shields.io/badge/Codecov-F01F7A?logo=codecov&logoColor=white)
![GitHub Actions](https://img.shields.io/badge/GitHub%20Actions-2088FF?logo=githubactions&logoColor=white)

# 관심사
# 주문 서버 관심사
### 주문 및 결제 과정

# 정보
1. 클라이언트는 주문 서버에 주문 요청
2. 주문 서버는 클라이언트 요청의 유효성을 검사
1. `PENDING_ORDER` 상태의 유형제품을 선택
2. 유형제품의 상태를 `PROCESSING`로 변경
3. 주문에 유형 제품을 추가
4. 만약 주문 요청한 수량만큼 `PENDING_ORDER` 상태의 유형제품이 없다면, `재고가 부족합니다. ` 문구를 클라이언트에게 응답
3. 주문 서버가 결제 서버에 결제 요청
4. 결제 서버가 결제 요청 받았다고 응답
5. 주문 서버는 결제 요청이 들어간 것을 클라이언트에게 전달
6. 결제 서버가 결제 결과를 주문 서버에 전달
1. 결제가 정상적으로 완료되면,
1. 유형제품의 상태를 `SHIPPING`으로 변경
2. 핵심제품의 재고를 차감
2. 결제가 실패되면,
1. 재고 유지
2. 주문의 유형제품 상태를 `PENDING_ORDER`로 변경
7. 주문 서버는 받는 결과를 클라이언트에게 전달

### 클라이언트 주문 요청 형태

```json
{
"core_products" : {
"1" : "30",
"2" : "10"
},
"client_type" : "InexperiencedCustomer",
"payment_method" : "CREDIT_CARD"
}
```

### 결제 요청 형태
```json
{
"buyer": {
"name": "John Doe",
"email": "[email protected]"
},
"seller": {
"name": "Jane Doe",
"email": "[email protected]"
},
"payment": "CREDIT_CARD",
"price": 100,
"redirect": "http://localhost:8080/payment/confirm"
}
```

### 상품의 상태에 대한 설명

`PENDING_ORDER` : 유형제품이 주문에 포함되지 않는 기본적인 상태를 의미. 여러 주문에서 동시에 접근하면 한 주문에만 들어간다.

`PROCESSING` : 유형제품이 주문에 포함되며, 결제 결과를 기다리는 상태를 의미.

`SHIPPING` : 결제가 정상적으로 종료되고, 해당 유형제품이 온전히 고객의 소유가 되는 상태.

`DELIVERED` : 상품이 고객에게 배송 완료된 상태. 현재 프로젝트에서는 배송까지는 관심사가 아니기 때문에 사용하지 않는다.

### 클라이언트 요청 유효성 검사 리스트

- [ ] 핵심제품 id가 존재하는가?
- [ ] 핵심제품의 재고가 충분한가?
- [ ] `PENDING_ORDER` 상태의 유형제품이 충분한가?
- [ ] 클라이언트가 상품 구매 권한이 있는가?
- [ ] 결제 방식이 유효한 방식인가?

## 시퀀스 다이어그램

![count-sequence.png](./count-sequence.png)

**Transaction 1**

- 사용자 권한 확인
- 재고 확인
- 주문 생성
- 상품을 결제 중으로 상태 변환

**Transaction 2**

- 결제 결과가 성공일 때,
- 상품 상태를 SHIPMENT 로 변경
- 주문의 상태를 FINISH 로 변경
- 에러가 발생하거나, 결제가 실패했을 때,
- 상품의 상태를 PENDING_ORDER 으로 변경
- 상품 재고를 원복
- 주문의 상태를 FAIL 로 변경

**Transaction 3**
- 리디렉션 정보에 따라 주문을 읽기
10 changes: 10 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ repositories {
}

@Suppress("SpellCheckingInspection") dependencies {
// spring-distribute-lock
implementation("org.springframework.integration:spring-integration-jdbc")
implementation("org.springframework.boot:spring-boot-starter-jdbc")
implementation("org.springframework.boot:spring-boot-starter-integration")

// spring-web-payment
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.5.0")
runtimeOnly("io.netty:netty-resolver-dns-native-macos:4.1.94.Final:osx-aarch_64")

// spring-web-jpa-concurrency
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
Expand Down
Binary file added count-sequence.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 12 additions & 12 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,22 @@ services:
depends_on:
mysql:
condition: service_healthy
networks:
- private-subnet
# networks:
# - private-subnet

mysql:
<<: *mysql-template
hostname: dev
container_name: mysql-dev
ports:
- "3306:3306"
networks:
- private-subnet

networks:
private-subnet: # private: 172.19.0.x
driver: bridge
ipam:
driver: default
config:
- subnet: 172.19.0.0/24
# networks:
# - private-subnet
#
#networks:
# private-subnet: # private: 172.19.0.x
# driver: bridge
# ipam:
# driver: default
# config:
# - subnet: 172.19.0.0/24
14 changes: 14 additions & 0 deletions src/main/java/com/concurrency/config/EnumMappingConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.concurrency.config;

import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class EnumMappingConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
ApplicationConversionService.configure(registry);
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/concurrency/config/JDBCConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.concurrency.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.jdbc.lock.DefaultLockRepository;
import org.springframework.integration.jdbc.lock.JdbcLockRegistry;
import org.springframework.integration.jdbc.lock.LockRepository;

import javax.sql.DataSource;

@Configuration
public class JDBCConfig {
@Bean
public DefaultLockRepository DefaultLockRepository(DataSource dataSource){
return new DefaultLockRepository(dataSource);
}
@Bean
public JdbcLockRegistry jdbcLockRegistry(LockRepository lockRepository){
return new JdbcLockRegistry(lockRepository);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,8 +1,29 @@
package com.concurrency.jpa.customer.Product;

import com.concurrency.jpa.customer.Product.entity.ActualProduct;
import com.concurrency.jpa.customer.Product.enums.ActualStatus;
import jakarta.persistence.LockModeType;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface ActualProductRepository extends JpaRepository<ActualProduct, Long>{
@Query(value = "SELECT count(a.id) FROM ActualProduct a " +
"WHERE a.coreProduct.id = :coreId AND a.actualStatus = :actualStatus")
Long countByCoreProductIdANDActualStatus(@Param("coreId") Long k, @Param("actualStatus") ActualStatus actualStatus);

@Lock(LockModeType.PESSIMISTIC_READ)
@Query(value = "SELECT a FROM ActualProduct a " +
"WHERE a.coreProduct.id = :coreProductId AND a.actualStatus = :reqStatus")
List<ActualProduct> findByCoreProduct_IdAndActualStatus(@Param("coreProductId") Long coreProductId, @Param("reqStatus")ActualStatus reqStatus, Pageable pageable);
List<ActualProduct> findByOrder_Id(Long orderId);

@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT a FROM ActualProduct a join fetch a.coreProduct c WHERE a.order.id = :orderId")
List<ActualProduct> findByOrder_IdPessimistic(@Param("orderId") Long orderId);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
package com.concurrency.jpa.customer.Product;

import com.concurrency.jpa.customer.Product.entity.CoreProduct;
import jakarta.persistence.LockModeType;
import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Lock;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.QueryHints;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface CoreProductRepository extends JpaRepository<CoreProduct, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("select c from CoreProduct c where c.id = :id" )
Optional<CoreProduct> findByIdPessimistic(@Param("id") Long id);
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.concurrency.jpa.customer.Product;

import com.concurrency.jpa.customer.Product.entity.ActualProduct;
import com.concurrency.jpa.customer.Product.enums.ActualStatus;

import java.util.List;
import java.util.Map;

public interface ProductService {
List<ActualProduct> findActualProductsByOrder(Long orderId);
List<ActualProduct> findActualProducts(Long coreProductId, ActualStatus actualStatus, Long stock);

List<ActualProduct> concatActualProductList(Map<Long, Long> coreProducts);
void updateCoreProductsStock(Map<Long, Long> requireProducts);
long subtractCoreProductStock(Long coreProductId, Long reqStock);
long subtractCoreProductStockPessimistic(Long coreProductId, Long reqStock);
long subtractCoreProductStockOptimistic(Long coreProductId, Long reqStock);
}
Loading
Loading