Java 高并发订单系统库存扣减并发超卖完整解决方案
大家好,今天我们来聊聊 Java 高并发订单系统中库存扣减时可能出现的并发超卖问题,以及如何完整地解决这个问题。这是一个非常常见但又至关重要的问题,处理不好会导致严重的业务损失。
一、问题背景:并发超卖
在高并发场景下,多个用户同时下单购买同一件商品,如果库存扣减逻辑没有做好并发控制,就会出现超卖现象,即卖出的商品数量超过实际库存。
举个简单的例子:
- 假设商品 A 的库存是 10 件。
- 用户 1 和 用户 2 同时下单,都购买 5 件商品 A。
-
如果库存扣减逻辑没有做好同步,可能出现以下情况:
- 用户 1 读取到库存为 10。
- 用户 2 读取到库存也为 10。
- 用户 1 扣减库存,将库存更新为 5。
- 用户 2 扣减库存,也将库存更新为 5。
这样,实际上卖出了 10 件商品,但库存却被扣减成了 5,导致超卖。
二、导致超卖的根本原因:竞态条件
超卖的根本原因在于竞态条件(Race Condition)。多个线程并发地访问和修改共享资源(库存),而最终的结果依赖于线程执行的顺序。由于线程执行顺序的不确定性,导致了数据不一致。
三、解决并发超卖的方案
解决并发超卖问题,核心在于控制对共享资源(库存)的并发访问,保证在同一时刻只有一个线程能够修改库存。 常用的方法包括:
- 悲观锁(Pessimistic Locking)
- 乐观锁(Optimistic Locking)
- 分布式锁(Distributed Locking)
- 队列(Queue)
- Redis 原子操作
- Lua 脚本
下面我们分别介绍这些方案,并给出代码示例。
1. 悲观锁
悲观锁认为并发冲突发生的概率很高,因此在访问共享资源之前,先获取锁,确保在锁释放之前,其他线程无法访问该资源。 在Java中,可以使用 synchronized 关键字或者 ReentrantLock 实现悲观锁。
-
synchronized 关键字
synchronized关键字可以修饰方法或代码块,当一个线程进入synchronized修饰的方法或代码块时,会自动获取锁,其他线程必须等待该线程释放锁才能进入。public class InventoryService { private int stock = 10; // 使用 synchronized 关键字修饰方法 public synchronized boolean decreaseStock(int quantity) { if (stock >= quantity) { stock -= quantity; System.out.println("扣减库存成功,剩余库存:" + stock); return true; } else { System.out.println("库存不足"); return false; } } }或者使用
synchronized代码块:public class InventoryService { private int stock = 10; private final Object lock = new Object(); public boolean decreaseStock(int quantity) { synchronized (lock) { if (stock >= quantity) { stock -= quantity; System.out.println("扣减库存成功,剩余库存:" + stock); return true; } else { System.out.println("库存不足"); return false; } } } } -
ReentrantLock
ReentrantLock是一个可重入的互斥锁,提供了比synchronized更强大的功能,例如可以设置公平锁、超时等待等。import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class InventoryService { private int stock = 10; private final Lock lock = new ReentrantLock(); public boolean decreaseStock(int quantity) { lock.lock(); // 获取锁 try { if (stock >= quantity) { stock -= quantity; System.out.println("扣减库存成功,剩余库存:" + stock); return true; } else { System.out.println("库存不足"); return false; } } finally { lock.unlock(); // 释放锁 } } }优点: 实现简单,保证数据一致性。
缺点: 并发度低,性能较差。所有请求都需要排队等待锁,在高并发场景下会成为性能瓶颈。
2. 乐观锁
乐观锁认为并发冲突发生的概率较低,因此不会在访问共享资源之前加锁,而是在更新数据时检查是否有其他线程修改过数据。常用的实现方式是使用版本号(Version)。
-
基于版本号的乐观锁
在数据库表中增加一个版本号字段,每次更新数据时,版本号加 1。在更新数据时,比较当前版本号和读取时的版本号是否一致,如果一致则更新成功,否则更新失败。
// 数据库表结构:商品表 (product) // id | name | stock | version // --- | ---- | ----- | ------- // 1 | 商品A | 10 | 1 public class InventoryService { // 模拟数据库操作 public boolean decreaseStock(int productId, int quantity) { // 1. 查询商品信息,包括库存和版本号 Product product = getProductById(productId); if (product == null) { System.out.println("商品不存在"); return false; } int currentStock = product.getStock(); int version = product.getVersion(); if (currentStock < quantity) { System.out.println("库存不足"); return false; } // 2. 尝试更新库存,并增加版本号 int rows = updateStock(productId, currentStock - quantity, version); // 3. 判断更新是否成功 if (rows > 0) { System.out.println("扣减库存成功,剩余库存:" + (currentStock - quantity)); return true; } else { System.out.println("扣减库存失败,可能已被其他线程修改"); return false; } } // 模拟数据库查询 private Product getProductById(int productId) { // 实际项目中从数据库查询 // 这里为了演示,直接创建对象 Product product = new Product(); product.setId(1); product.setName("商品A"); product.setStock(10); product.setVersion(1); return product; } // 模拟数据库更新 private int updateStock(int productId, int newStock, int version) { // 实际项目中使用 SQL 的 WHERE 子句进行版本号的比较 // UPDATE product SET stock = #{newStock}, version = version + 1 WHERE id = #{productId} AND version = #{version} // 这里为了演示,直接返回 1 或 0 // 模拟更新成功或失败 if (Math.random() > 0.5) { System.out.println("productId = " + productId + ", newStock = " + newStock + ", version = " + version); return 1; // 更新成功 } else { return 0; // 更新失败 } } } class Product { private int id; private String name; private int stock; private int version; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getStock() { return stock; } public void setStock(int stock) { this.stock = stock; } public int getVersion() { return version; } public void setVersion(int version) { this.version = version; } }优点: 并发度高,性能较好。
缺点: 需要重试机制,如果并发冲突频繁,重试次数会增加,影响性能。 同时也需要数据库的支持。
3. 分布式锁
当应用是分布式部署时,单机的锁(如 synchronized 和 ReentrantLock)无法保证多个应用实例之间的同步。这时需要使用分布式锁。 常用的分布式锁实现方式有:
-
基于数据库的分布式锁
通过在数据库中创建一张锁表,当需要获取锁时,向锁表中插入一条记录。释放锁时,删除该记录。
-
基于 Redis 的分布式锁
利用 Redis 的
SETNX命令实现原子性的锁操作。import redis.clients.jedis.Jedis; public class RedisLock { private static final String LOCK_KEY = "product_stock_lock"; private static final String LOCK_VALUE = "lock"; // 可以使用UUID private static final int LOCK_EXPIRE_TIME = 10; // 锁的过期时间,单位:秒 private Jedis jedis; public RedisLock(Jedis jedis) { this.jedis = jedis; } // 获取锁 public boolean tryLock() { String result = jedis.set(LOCK_KEY, LOCK_VALUE, "NX", "EX", LOCK_EXPIRE_TIME); return "OK".equals(result); } // 释放锁 public void unlock() { jedis.del(LOCK_KEY); } // 释放锁的 lua 脚本, 保证原子性 public void unlockLua() { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; Object result = jedis.eval(script, 1, LOCK_KEY); if ("1".equals(result.toString())) { System.out.println("unlock success"); } else { System.out.println("unlock fail"); } } } public class InventoryService { private int stock = 10; private RedisLock redisLock; public InventoryService(RedisLock redisLock) { this.redisLock = redisLock; } public boolean decreaseStock(int quantity) { try { // 尝试获取锁 if (redisLock.tryLock()) { if (stock >= quantity) { stock -= quantity; System.out.println("扣减库存成功,剩余库存:" + stock); return true; } else { System.out.println("库存不足"); return false; } } else { System.out.println("获取锁失败,请稍后重试"); return false; } } finally { // 释放锁 redisLock.unlockLua(); } } }优点: 适用于分布式环境,并发度较高。
缺点: 实现相对复杂,需要引入第三方组件(如 Redis)。需要考虑锁的过期时间,防止死锁。
4. 队列
将库存扣减操作放入队列中,利用队列的先进先出特性,保证库存扣减操作的顺序性。 可以使用 Kafka、RabbitMQ 等消息队列。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class InventoryService {
private int stock = 10;
private BlockingQueue<Order> orderQueue = new LinkedBlockingQueue<>();
// 接收订单,放入队列
public void placeOrder(Order order) {
try {
orderQueue.put(order);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 启动消费者线程,处理订单
public void startConsumer() {
new Thread(() -> {
while (true) {
try {
Order order = orderQueue.take();
decreaseStock(order.getProductId(), order.getQuantity());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
// 扣减库存
private boolean decreaseStock(int productId, int quantity) {
synchronized (this) {
if (stock >= quantity) {
stock -= quantity;
System.out.println("扣减库存成功,剩余库存:" + stock);
return true;
} else {
System.out.println("库存不足");
return false;
}
}
}
public static void main(String[] args) {
InventoryService inventoryService = new InventoryService();
inventoryService.startConsumer();
// 模拟多个用户下单
for (int i = 0; i < 20; i++) {
Order order = new Order(1, 1); // 商品ID为1,购买数量为1
inventoryService.placeOrder(order);
}
}
}
class Order {
private int productId;
private int quantity;
public Order(int productId, int quantity) {
this.productId = productId;
this.quantity = quantity;
}
public int getProductId() {
return productId;
}
public int getQuantity() {
return quantity;
}
}
优点: 可以实现异步处理,提高系统吞吐量。可以削峰填谷,缓解高并发压力。
缺点: 引入了消息队列,增加了系统的复杂性。需要保证消息的可靠性,防止消息丢失。
5. Redis 原子操作
Redis 提供了原子操作,可以直接对库存进行原子性的加减操作,避免并发冲突。
import redis.clients.jedis.Jedis;
public class InventoryService {
private static final String STOCK_KEY = "product_stock:1"; // 商品ID为1的库存
private Jedis jedis;
public InventoryService(Jedis jedis) {
this.jedis = jedis;
}
public boolean decreaseStock(int quantity) {
// 使用 Redis 的 DECRBY 命令原子性地减少库存
Long stock = jedis.decrBy(STOCK_KEY, quantity);
if (stock >= 0) {
System.out.println("扣减库存成功,剩余库存:" + stock);
return true;
} else {
// 库存不足,恢复库存
jedis.incrBy(STOCK_KEY, quantity);
System.out.println("库存不足");
return false;
}
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 初始化库存
jedis.set(STOCK_KEY, "10");
InventoryService inventoryService = new InventoryService(jedis);
// 模拟多个用户下单
for (int i = 0; i < 20; i++) {
inventoryService.decreaseStock(1); // 每次购买1件
}
jedis.close();
}
}
优点: 实现简单,性能高。利用 Redis 的高性能特性。
缺点: 需要引入 Redis。需要考虑 Redis 的持久化和高可用。
6. Lua 脚本
可以将多个 Redis 操作封装成一个 Lua 脚本,利用 Redis 的原子性执行 Lua 脚本,保证多个操作的原子性。
import redis.clients.jedis.Jedis;
public class InventoryService {
private static final String STOCK_KEY = "product_stock:1"; // 商品ID为1的库存
private Jedis jedis;
public InventoryService(Jedis jedis) {
this.jedis = jedis;
}
public boolean decreaseStock(int quantity) {
// Lua 脚本
String script = "local stock = tonumber(redis.call('get', KEYS[1]))n" +
"if stock >= tonumber(ARGV[1]) thenn" +
" redis.call('decrby', KEYS[1], ARGV[1])n" +
" return 1n" +
"elsen" +
" return 0n" +
"end";
// 执行 Lua 脚本
Object result = jedis.eval(script, 1, STOCK_KEY, String.valueOf(quantity));
if ("1".equals(result.toString())) {
System.out.println("扣减库存成功,剩余库存:" + jedis.get(STOCK_KEY));
return true;
} else {
System.out.println("库存不足");
return false;
}
}
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
// 初始化库存
jedis.set(STOCK_KEY, "10");
InventoryService inventoryService = new InventoryService(jedis);
// 模拟多个用户下单
for (int i = 0; i < 20; i++) {
inventoryService.decreaseStock(1); // 每次购买1件
}
jedis.close();
}
}
优点: 可以将多个 Redis 操作原子化,提高性能。
缺点: 需要编写 Lua 脚本,增加了复杂性。
四、方案选择建议
选择哪种方案取决于具体的业务场景和系统架构。
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 悲观锁 | 并发量低,数据一致性要求高 | 实现简单,保证数据一致性 | 并发度低,性能差 |
| 乐观锁 | 并发量较高,允许一定程度的重试 | 并发度高,性能好 | 需要重试机制,并发冲突频繁时性能下降,需要数据库支持 |
| 分布式锁 | 分布式环境,需要跨应用实例同步 | 适用于分布式环境,并发度较高 | 实现相对复杂,需要引入第三方组件,需要考虑锁的过期时间 |
| 队列 | 高并发,需要异步处理,削峰填谷 | 实现异步处理,提高系统吞吐量,削峰填谷,缓解高并发压力 | 引入了消息队列,增加了系统的复杂性,需要保证消息的可靠性 |
| Redis 原子操作 | 高并发,对性能要求高 | 实现简单,性能高,利用 Redis 的高性能特性 | 需要引入 Redis,需要考虑 Redis 的持久化和高可用 |
| Redis Lua 脚本 | 需要原子性地执行多个 Redis 操作 | 可以将多个 Redis 操作原子化,提高性能 | 需要编写 Lua 脚本,增加了复杂性 |
五、总结
解决 Java 高并发订单系统中库存扣减的并发超卖问题,需要根据具体的业务场景和系统架构选择合适的方案。没有一种方案是万能的,需要综合考虑并发量、数据一致性要求、性能要求、系统复杂性等因素。 在实际项目中,通常会结合多种方案,例如使用乐观锁结合重试机制,或者使用队列结合 Redis 原子操作。只有充分理解各种方案的优缺点,才能选择最适合自己的解决方案,构建稳定可靠的高并发订单系统。