天天看點

Spring Boot商城應用中如何避免出現超賣情況發生?

Spring Boot商城應用中如何避免出現超賣情況發生?

所謂的超賣情況,通常是指發生在高并發場景中,當多個使用者同時購買同一個商品的時候,由于系統正處于并發請求的狀态下,因為并發請求能力不足或者是一些資料一緻性處理不當的情況下,導緻庫存被多次扣減而超過了實際庫存量的情況。

超賣情況發生的場景

例如在一些大規模的促銷活動中,會有成千上萬的使用者同時去進入線上商城,搶購商品。由于請求量激增,系統需要處理大量的使用者請求,如果并發沒有得到有效的控制,很容易就會導緻超賣情況的發生。

又比如在一些分布式系統中,多個應用執行個體可能會去同時去更新庫存,這個時候,如果沒有添加分布式鎖或者是添加其他的同步機制很容易就會導緻出現多個執行個體共同去調用庫存,然後導緻超賣情況的發生。

還有一些場景中是因為在處理資料庫事務的時候,沒有考慮到多個并發請求共同去更新庫存,導緻出現事務失效而出現庫存不一緻的情況。在一些使用了緩存的系統中,由于緩存與資料庫之間的資料不是強一緻性也會導緻超賣情況的發生。

以上的這些場景都強調了并發控制和資料一緻性的重要性。為避免超賣,需要采取多種措施,如資料庫鎖、分布式鎖、事務處理、使用消息隊列等,下面我們就來看看具體應該如何避免這些情況的發生。

資料庫鎖(Database Locking)

在實際操作中,我們可以利用資料庫鎖操作來確定每次的庫存操作都是原子性的操作,這樣來保證不會出現超賣情況,常見的資料庫鎖機制有如下幾種。

悲觀鎖(Pessimistic Locking)

悲觀鎖會在讀取資料時鎖定行,直到事務完成,確定其他事務無法同時修改資料。這可以通過 SELECT ... FOR UPDATE 語句實作。如下所示。

public void updateInventory(Long productId, int quantity) {
    // 使用悲觀鎖查詢庫存
    Product product = productRepository.findByIdForUpdate(productId);
    if (product.getStock() >= quantity) {
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);
    } else {
        throw new RuntimeException("Insufficient stock");
    }
}           

樂觀鎖(Optimistic Locking)

樂觀鎖則是通過版本号機制或者是時間戳機制對資料版本進行檢查,來保證資料在更新之前沒有被其他的事務操作所修改,可以通過 @Version 注解實作。如下所示。

@Entity
public class Product {
    @Id
    private Long id;
    private int stock;
    @Version
    private Integer version;
}

public void updateInventory(Long productId, int quantity) {
    Product product = productRepository.findById(productId).orElseThrow();
    if (product.getStock() >= quantity) {
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);
    } else {
        throw new RuntimeException("Insufficient stock");
    }
}           

分布式鎖(Distributed Locking)

在一些分布式系統中,我們需要用到分布式鎖機制來鎖定不同節點上的資料操作,常見的分布式鎖實作包括利用Redis、Zookeeper、資料庫等。

使用 Redis 實作分布式鎖

我們可以通過Redis 的 SETNX 指令來實作分布式鎖,如下所示。

public void updateInventory(Long productId, int quantity) {
    String lockKey = "lock:product:" + productId;
    boolean acquired = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
    if (!acquired) {
        throw new RuntimeException("Failed to acquire lock");
    }
    try {
        Product product = productRepository.findById(productId).orElseThrow();
        if (product.getStock() >= quantity) {
            product.setStock(product.getStock() - quantity);
            productRepository.save(product);
        } else {
            throw new RuntimeException("Insufficient stock");
        }
    } finally {
        redisTemplate.delete(lockKey);
    }
}           

資料庫事務(Database Transactions)

上面也提到了有些情況下是由于事務失效導緻的超賣,是以一定要保證資料事務操作的原子性,如果任何操作失敗,事務可以復原以確定資料一緻性。如下所示。

@Transactional
public void updateInventory(Long productId, int quantity) {
    Product product = productRepository.findById(productId).orElseThrow();
    if (product.getStock() >= quantity) {
        product.setStock(product.getStock() - quantity);
        productRepository.save(product);
    } else {
        throw new RuntimeException("Insufficient stock");
    }
}           

消息隊列(Message Queue)

這裡的消息隊列主要是指一些順序的消息隊列,也就是說滿足按照順序執行處理消息的隊列,這樣就可以確定每個訂單請求按順序處理,避免并發寫操作。進而保證不會出現超賣情況,同時也可以對系統流量進行控制,如下所示。

public void placeOrder(OrderRequest request) {
    // 将訂單請求發送到消息隊列
    messageQueue.send(request);
}

@RabbitListener(queues = "orderQueue")
public void handleOrder(OrderRequest request) {
    updateInventory(request.getProductId(), request.getQuantity());
}           

緩存與庫存預減(Cache and Pre-deduction)

還有一種情況,我們在接收到使用者訂單之後,先在緩存中對庫存進行預扣除,然後通過異步更新操作來與資料庫進行同步,這個時候如果發現庫存不夠了,或者是資料庫更新失敗了,那麼就可以對緩存中的庫存進行復原或者是更新使用者訂單操作提示,如下所示。

public void placeOrder(Long productId, int quantity) {
    String stockKey = "stock:product:" + productId;
    Integer stock = redisTemplate.opsForValue().get(stockKey);
    if (stock != null && stock >= quantity) {
        redisTemplate.opsForValue().decrement(stockKey, quantity);
        try {
            updateInventory(productId, quantity);
        } catch (Exception e) {
            // 復原緩存中的庫存
            redisTemplate.opsForValue().increment(stockKey, quantity);
            throw e;
        }
    } else {
        throw new RuntimeException("Insufficient stock");
    }
}           

總結

以上方法,是在開發中常用的能夠有效避免出現超賣情況的方案,當然在實際操作中需要結合具體的業務場景來進行選擇調整。保證在滿足業務需求情況下避免出現超賣情況的發生。

繼續閱讀