0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SpringBootで作成した高負荷在庫管理システムの基本実装例

Posted at

今回作成した在庫管理システムの内容

  • 在庫更新処理:
    ユーザーからの在庫更新リクエストに対して、在庫エンティティを取得後、在庫数が十分かどうかをチェックします。

    • 楽観的ロック: 通常商品など競合が少ない場合に利用し、更新時のバージョンチェックで競合を検出します。
    • 悲観的ロック: 人気商品など高競合が予想される場合に利用し、データ取得時に即座にロックを取得します。
      在庫数が十分な場合は更新を行い、コミットします。不足している場合はエラーを返却します。
  • 決済処理:
    決済リクエストを受け、決済エンティティを作成・保存します。
    決済処理は再現性が求められるため、トランザクション分離レベルとして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)を組み合わせることで、トランザクション分離レベル・排他制御を最適化した高負荷在庫管理システムの基本実装例とテスト例を実現できます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?