今回作成した在庫管理システムの内容
-
在庫更新処理:
ユーザーからの在庫更新リクエストに対して、在庫エンティティを取得後、在庫数が十分かどうかをチェックします。- 楽観的ロック: 通常商品など競合が少ない場合に利用し、更新時のバージョンチェックで競合を検出します。
-
悲観的ロック: 人気商品など高競合が予想される場合に利用し、データ取得時に即座にロックを取得します。
在庫数が十分な場合は更新を行い、コミットします。不足している場合はエラーを返却します。
-
決済処理:
決済リクエストを受け、決済エンティティを作成・保存します。
決済処理は再現性が求められるため、トランザクション分離レベルとしてREPEATABLE_READを採用し、正確な金額計算と整合性を保証します。
以下は、在庫管理システムの処理の流れを示したフローチャート例と、システムの内容およびこの記事の目的についての説明です。
フローチャート例
1. ドメインエンティティ
在庫エンティティ(Inventory.java)
package com.example.demo.entity;
import javax.persistence.*;
@Entity
public class Inventory {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long productId;
@Column(nullable = false)
private Integer stock;
// 楽観的ロック用のバージョンフィールド
@Version
private Long version;
// コンストラクタ、getter/setter
public Inventory() {}
public Inventory(Integer stock) {
this.stock = stock;
}
public Long getProductId() {
return productId;
}
public void setProductId(Long productId) {
this.productId = productId;
}
public Integer getStock() {
return stock;
}
public void setStock(Integer stock) {
this.stock = stock;
}
public Long getVersion() {
return version;
}
public void setVersion(Long version) {
this.version = version;
}
}
決済エンティティ(Payment.java)
package com.example.demo.entity;
import javax.persistence.*;
import java.math.BigDecimal;
@Entity
public class Payment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Long orderId;
private BigDecimal amount;
// コンストラクタ、getter/setter
public Payment() {}
public Payment(Long orderId, BigDecimal amount) {
this.orderId = orderId;
this.amount = amount;
}
public Long getId() {
return id;
}
public Long getOrderId() {
return orderId;
}
public BigDecimal getAmount() {
return amount;
}
public void setId(Long id) {
this.id = id;
}
public void setOrderId(Long orderId) {
this.orderId = orderId;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
}
2. リポジトリインターフェース
在庫リポジトリ(InventoryRepository.java)
package com.example.demo.repository;
import com.example.demo.entity.Inventory;
import org.springframework.data.jpa.repository.*;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import javax.persistence.LockModeType;
@Repository
public interface InventoryRepository extends JpaRepository<Inventory, Long> {
// 行ロック(悲観的ロック)のクエリ
@Lock(LockModeType.PESSIMISTIC_WRITE)
@Query("SELECT i FROM Inventory i WHERE i.productId = :productId")
Inventory findByProductIdForUpdate(@Param("productId") Long productId);
}
決済リポジトリ(PaymentRepository.java)
package com.example.demo.repository;
import com.example.demo.entity.Payment;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface PaymentRepository extends JpaRepository<Payment, Long> {
}
3. サービスクラス
在庫更新処理(InventoryService.java)
package com.example.demo.service;
import com.example.demo.entity.Inventory;
import com.example.demo.repository.InventoryRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.*;
@Service
public class InventoryService {
@Autowired
private InventoryRepository inventoryRepository;
/**
* 楽観的ロックを利用した在庫更新処理
* 隔離レベルはREAD_COMMITTEDを採用。
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateStockOptimistic(Long productId, int quantity) {
Inventory inventory = inventoryRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("対象商品が存在しません"));
if (inventory.getStock() < quantity) {
throw new RuntimeException("在庫不足");
}
inventory.setStock(inventory.getStock() - quantity);
// save時にバージョンチェックが実行される
inventoryRepository.save(inventory);
}
/**
* 悲観的ロックを利用した在庫更新処理
* ※ロック取得時に即時排他制御を行う
*/
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateStockPessimistic(Long productId, int quantity) {
Inventory inventory = inventoryRepository.findByProductIdForUpdate(productId);
if (inventory.getStock() < quantity) {
throw new RuntimeException("在庫不足");
}
inventory.setStock(inventory.getStock() - quantity);
inventoryRepository.save(inventory);
}
}
決済処理(PaymentService.java)
package com.example.demo.service;
import com.example.demo.entity.Payment;
import com.example.demo.repository.PaymentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.*;
import java.math.BigDecimal;
@Service
public class PaymentService {
@Autowired
private PaymentRepository paymentRepository;
/**
* 決済処理
* 隔離レベルはREPEATABLE_READを採用し、一貫性を確保する
*/
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Payment processPayment(Long orderId, BigDecimal amount) {
// 実際の決済ロジック(外部API呼び出し等)を実装可能
Payment payment = new Payment(orderId, amount);
return paymentRepository.save(payment);
}
}
4. コントローラー(Spring MVC)
在庫更新用コントローラー(InventoryController.java)
package com.example.demo.controller;
import com.example.demo.service.InventoryService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/inventory")
public class InventoryController {
@Autowired
private InventoryService inventoryService;
/**
* 楽観的ロックを利用した在庫更新API
*/
@PostMapping("/optimistic/update")
public ResponseEntity<String> updateStockOptimistic(@RequestParam Long productId,
@RequestParam int quantity) {
try {
inventoryService.updateStockOptimistic(productId, quantity);
return ResponseEntity.ok("在庫更新成功(楽観的ロック)");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("在庫更新失敗: " + e.getMessage());
}
}
/**
* 悲観的ロックを利用した在庫更新API
*/
@PostMapping("/pessimistic/update")
public ResponseEntity<String> updateStockPessimistic(@RequestParam Long productId,
@RequestParam int quantity) {
try {
inventoryService.updateStockPessimistic(productId, quantity);
return ResponseEntity.ok("在庫更新成功(悲観的ロック)");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("在庫更新失敗: " + e.getMessage());
}
}
}
決済処理用コントローラー(PaymentController.java)
package com.example.demo.controller;
import com.example.demo.entity.Payment;
import com.example.demo.service.PaymentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Autowired
private PaymentService paymentService;
/**
* 決済処理API
*/
@PostMapping("/process")
public ResponseEntity<String> processPayment(@RequestParam Long orderId,
@RequestParam BigDecimal amount) {
try {
Payment payment = paymentService.processPayment(orderId, amount);
return ResponseEntity.ok("決済処理成功: Payment ID = " + payment.getId());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body("決済処理失敗: " + e.getMessage());
}
}
}
5. テスト例
以下は、Spring Bootの統合テストで【MockMvc】を利用して各エンドポイントの動作を検証する例です。
在庫更新APIのテスト(InventoryControllerTest.java)
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class InventoryControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testOptimisticUpdate() throws Exception {
mockMvc.perform(post("/inventory/optimistic/update")
.param("productId", "1")
.param("quantity", "5")
.contentType(MediaType.APPLICATION_FORM_URLENCODED))
.andExpect(status().isOk())
.andExpect(content().string(containsString("在庫更新成功(楽観的ロック)")));
}
@Test
public void testPessimisticUpdate() throws Exception {
mockMvc.perform(post("/inventory/pessimistic/update")
.param("productId", "1")
.param("quantity", "5")
.contentType(MediaType.APPLICATION_FORM_URLENCODED))
.andExpect(status().isOk())
.andExpect(content().string(containsString("在庫更新成功(悲観的ロック)")));
}
}
決済処理APIのテスト(PaymentControllerTest.java)
package com.example.demo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class PaymentControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testProcessPayment() throws Exception {
mockMvc.perform(post("/payment/process")
.param("orderId", "100")
.param("amount", "500.00")
.contentType(MediaType.APPLICATION_FORM_URLENCODED))
.andExpect(status().isOk())
.andExpect(content().string(containsString("決済処理成功")));
}
}
補足
-
トランザクション設定:
在庫更新は高スループットを実現するためにREAD_COMMITTEDを採用し、決済処理では再現性を担保するためREPEATABLE_READを利用しています。 -
ロック制御:
悲観的ロックは、@Lock(LockModeType.PESSIMISTIC_WRITE)
によりデータ取得時に排他制御を行い、楽観的ロックは@Version
アノテーションにより更新時の競合検出を行います。 -
テスト:
各エンドポイントに対してMockMvcを用いた統合テストを実施し、実際のレスポンス内容やHTTPステータスを検証しています。
このように、Spring BootとSpring MVC(およびSpring Data JPA)を組み合わせることで、トランザクション分離レベル・排他制御を最適化した高負荷在庫管理システムの基本実装例とテスト例を実現できます。