The so-called overselling situation usually refers to the situation that occurs in a high-concurrency scenario, when multiple users purchase the same product at the same time, because the system is in the state of concurrent requests, because the concurrent request capacity is insufficient or some data consistency is not properly handled, the inventory is deducted multiple times and exceeds the actual inventory.
Scenarios in which overselling occurs
For example, in some large-scale promotions, there will be thousands of users who enter the online mall at the same time to snap up the goods. Due to the surge in request volume, the system needs to handle a large number of user requests, and if the concurrency is not effectively controlled, it can easily lead to overselling.
For example, in some distributed systems, multiple application instances may update the inventory at the same time, and at this time, if no distributed locks are added or other synchronization mechanisms are added, it is easy to cause multiple instances to call the inventory together, and then lead to overselling.
In some cases, when processing database transactions, multiple concurrent requests are not taken into account to update the inventory, resulting in transaction failures and inventory inconsistencies. In some systems that use caching, overselling can occur because the data between the cache and the database is not strongly consistent.
All of these scenarios underscore the importance of concurrency control and data consistency. In order to avoid overselling, various measures need to be taken, such as database locks, distributed locks, transaction processing, and the use of message queues, etc., let's take a look at how to avoid these situations.
数据库锁(Database Locking)
In practice, we can use the database lock operation to ensure that each inventory operation is an atomic operation, so as to ensure that there will be no overselling, and the common database lock mechanisms are as follows.
悲观锁(Pessimistic Locking)
Pessimistic locks the rows when the data is read until the transaction completes, ensuring that other transactions can't modify the data at the same time. This can be done via SELECT ... FOR UPDATE statement. It is shown below.
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)
Optimistic locking checks the version of the data through the version number mechanism or the timestamp mechanism to ensure that the data has not been modified by other transaction operations before the update, which can be implemented through @Version annotations. It is shown below.
@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)
In some distributed systems, we need to use a distributed lock mechanism to lock data operations on different nodes, such as Redis, Zookeeper, and databases.
Implement distributed locking with Redis
We can implement distributed locks via Redis' SETNX command, as shown below.
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)
As mentioned above, there are cases where overselling is caused by transaction failure, so it is important to ensure the atomicity of data transaction operations, and if any operation fails, the transaction can be rolled back to ensure data consistency. It is shown below.
@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)
The message queue here mainly refers to some sequential message queues, that is, the queue that satisfies the processing of messages in order, so as to ensure that each order request is processed sequentially and avoid concurrent write operations. In order to ensure that there will be no overselling, the system traffic can also be controlled, as shown below.
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)
There is also a situation, after we receive the user order, first pre-deduct the inventory in the cache, and then synchronize with the database through asynchronous update operation, at this time, if we find that the inventory is not enough, or the database update fails, then we can roll back the inventory in the cache or update the user order operation prompt, as follows.
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");
}
}
summary
The above methods are commonly used in development to effectively avoid overselling, of course, in practice, you need to combine specific business scenarios to select and adjust. Ensure that overselling is avoided while meeting business needs.