当下单扣库存接口加了分布式锁后,吞吐量太低,这个性能显然还有很大的提升空间。下面介绍几种优化思路:
- 减少锁竞争:锁竞争是性能瓶颈之一,可以通过以下方式来减少锁竞争:
- 尽量减少锁的范围:只锁定必要的代码块。
- 采用细粒度锁:将锁划分为更小的粒度,以减少锁竞争。
- 使用乐观锁:在不加锁的情况下进行并发操作,如果发现数据冲突,则进行回滚或重试。
- 使用缓存:将热门商品库存缓存在内存中,减少数据库的访问,提高吞吐量。可以采用Redis等缓存中间件,通过缓存预热等方式来提高缓存命中率。
- 异步处理:将库存扣减的操作异步化,将请求放入消息队列中,异步处理库存扣减操作。这样可以将请求处理时间和库存扣减操作解耦,提高系统的并发能力。
- 分库分表:将商品库存数据拆分到多个库或表中,减少单个库或表的数据量,提高并发能力。在采用分库分表的方案时,需要注意数据一致性问题。
- 负载均衡:采用负载均衡技术,将请求分发到多个服务器上处理,提高系统的并发能力。可以采用Nginx、HAProxy等负载均衡软件。
综上所述,优化下单扣库存接口的吞吐量,可以从锁竞争、缓存、异步处理、分库分表、负载均衡等方面入手,综合采用多种优化手段来提高系统的性能。需要根据实际情况和具体业务场景进行选择和实现。
基于减少锁竞争的方案,可以采用细粒度锁的方式,将锁划分为更小的粒度,以减少锁竞争。具体实现可以考虑以下步骤:
- 将商品库存数据拆分到多个表中,每个表对应一个分段库存。
- 对于每个分段库存,采用单独的锁进行控制,这样每个库存分段之间就可以并发进行操作。
- 对于库存扣减的操作,首先通过商品ID和库存ID计算出对应的分段库存,然后在该分段库存上进行锁定和操作。
下面是一个简单的示例代码,实现了基于分段锁的库存扣减:
javaCopy code// 商品库存分段数量
private static final int PARTITION_NUM = 4;
// 商品库存分段锁
private static final ReentrantLock[] locks = new ReentrantLock[PARTITION_NUM];
// 商品库存数据
private static final Map<String, Integer>[] stockMap = new Map[PARTITION_NUM];
// 初始化商品库存分段锁和数据
static {
for (int i = 0; i < PARTITION_NUM; i++) {
locks[i] = new ReentrantLock();
stockMap[i] = new HashMap<>();
}
}
/**
* 扣减库存
*
* @param productId 商品ID
* @param stockId 库存ID
* @param num 扣减数量
* @return true-扣减成功,false-扣减失败
*/
public boolean deductStock(String productId, String stockId, int num) {
int partition = getPartition(productId, stockId); // 计算分段库存
locks[partition].lock(); // 获取分段锁
try {
Map<String, Integer> stock = stockMap[partition];
Integer currentStock = stock.get(stockId);
if (currentStock == null || currentStock < num) {
return false; // 库存不足,扣减失败
}
stock.put(stockId, currentStock - num); // 扣减库存
return true;
} finally {
locks[partition].unlock(); // 释放分段锁
}
}
/**
* 计算商品库存所在的分段
*
* @param productId 商品ID
* @param stockId 库存ID
* @return 分段编号
*/
private int getPartition(String productId, String stockId) {
int hash = (productId + "_" + stockId).hashCode();
return Math.abs(hash % PARTITION_NUM);
}
在这个示例代码中,我们将商品库存数据拆分到4个Map中,每个Map对应一个分段库存。对于每个分段库存,我们采用一个单独的锁进行控制,这样不同分段库存之间就可以并发进行操作。在进行库存扣减操作时,首先计算出对应的分段库存,然后在该分段库存上进行锁定和操作,操作完成后释放锁。
在进行库存扣减操作时,还需要考虑到并发修改的情况,因此可以在扣减库存的方法中加入CAS机制。这样可以避免多个线程同时修改同一个库存数据导致的数据不一致问题。
以下是一个修改后的代码示例:
javaCopy code// 商品库存分段数量
private static final int PARTITION_NUM = 4;
// 商品库存分段锁
private static final ReentrantLock[] locks = new ReentrantLock[PARTITION_NUM];
// 商品库存数据
private static final Map<String, AtomicInteger>[] stockMap = new Map[PARTITION_NUM];
// 初始化商品库存分段锁和数据
static {
for (int i = 0; i < PARTITION_NUM; i++) {
locks[i] = new ReentrantLock();
stockMap[i] = new HashMap<>();
}
}
/**
* 扣减库存
*
* @param productId 商品ID
* @param stockId 库存ID
* @param num 扣减数量
* @return true-扣减成功,false-扣减失败
*/
public boolean deductStock(String productId, String stockId, int num) {
int partition = getPartition(productId, stockId); // 计算分段库存
locks[partition].lock(); // 获取分段锁
try {
AtomicInteger currentStock = stockMap[partition].get(stockId);
if (currentStock == null || currentStock.get() < num) {
return false; // 库存不足,扣减失败
}
int updatedStock = currentStock.addAndGet(-num);
if (updatedStock < 0) {
currentStock.addAndGet(num); // 还原库存
return false; // 库存不足,扣减失败
}
return true; // 扣减成功
} finally {
locks[partition].unlock(); // 释放分段锁
}
}
/**
* 计算商品库存所在的分段
*
* @param productId 商品ID
* @param stockId 库存ID
* @return 分段编号
*/
private int getPartition(String productId, String stockId) {
int hash = (productId + "_" + stockId).hashCode();
return Math.abs(hash % PARTITION_NUM);
}
在这个修改后的代码中,我们使用了AtomicInteger代替了Integer类型的库存数据,这样可以通过AtomicInteger的原子性操作避免多个线程同时修改同一个库存数据导致的数据不一致问题。在扣减库存时,我们首先尝试使用CAS操作扣减库存,如果CAS操作失败,则说明当前库存已被其他线程修改,需要重新获取最新库存数据进行扣减操作。如果库存不足,则将库存还原并返回扣减失败的结果。在CAS操作失败的情况下,我们并没有使用锁,而是直接返回扣减失败的结果,这样可以减少锁竞争的次数,提高并发性能。
在这个示例代码中,由于使用了jvm锁进行并发控制,因此在单台机器上运行时不会出现超卖的情况。因为在同一时刻,只有一个线程可以获取到该商品库存的锁,并且在执行扣减操作时,其他线程会被阻塞,直到当前线程释放锁为止。因此,同一时刻只有一个线程可以修改库存数据,不会出现超卖的情况。
但是,如果多台机器上同时运行该代码,并且使用的是不同的库存数据源,就有可能出现超卖的情况。因为在多台机器上运行时,每台机器上都会有一个独立的库存数据源,并且每台机器上运行的代码都是独立的。因此,在同一时刻可能有多个线程同时对不同的库存数据源进行扣减操作,这样就有可能出现超卖的情况。
为了解决这个问题,可以使用分布式锁或者分布式事务来保证多台机器上的数据一致性。分布式锁可以保证同一时刻只有一个线程可以对库存数据源进行操作,从而避免出现超卖的情况。而分布式事务可以保证多个库存数据源之间的数据一致性,从而避免出现数据不一致的情况。
下面是优化后的代码实现,使用Redis作为分布式锁的存储介质,同时支持可重入和锁失效续期功能:
javaCopy code
public class DistributedLockStockServiceImpl implements StockService {
private static final String LOCK_PREFIX = "stock_lock_";
private static final long LOCK_EXPIRE_TIME = 3000L; //锁失效时间
private static final long LOCK_RETRY_INTERVAL = 100L; //重试间隔时间
private final Map<String, Integer> stock = new ConcurrentHashMap<>();
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public void init() {
stock.put("item_001", 1000);
stock.put("item_002", 2000);
stock.put("item_003", 3000);
stock.put("item_004", 4000);
}
@Override
public boolean reduceStock(String itemId, int quantity) {
String lockKey = LOCK_PREFIX + itemId;
String requestId = UUID.randomUUID().toString();
//加锁
while (!tryLock(lockKey, requestId, LOCK_EXPIRE_TIME)) {
try {
Thread.sleep(LOCK_RETRY_INTERVAL);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
try {
int stockQuantity = stock.get(itemId);
if (stockQuantity < quantity) {
return false;
}
stock.put(itemId, stockQuantity - quantity);
return true;
} finally {
//解锁
unlock(lockKey, requestId);
}
}
private boolean tryLock(String lockKey, String requestId, long expireTime) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, TimeUnit.MILLISECONDS);
if (result != null && result) {
return true;
}
//获取锁失败,尝试续期
String lockValue = redisTemplate.opsForValue().get(lockKey);
if (lockValue != null && lockValue.equals(requestId)) {
redisTemplate.expire(lockKey, expireTime, TimeUnit.MILLISECONDS);
return true;
}
return false;
}
private void unlock(String lockKey, String requestId) {
String lockValue = redisTemplate.opsForValue().get(lockKey);
if (lockValue != null && lockValue.equals(requestId)) {
redisTemplate.delete(lockKey);
}
}
}
在这个优化后的代码中,我们使用了Redis作为分布式锁的存储介质,并且添加了可重入和锁失效续期的功能。在进行库存扣减操作时,首先尝试获取分布式锁,如果获取成功则进行扣减操作,操作完成后释放锁。如果获取失败,则重试一定次数,直到获取到锁为止。在获取锁时,如果当前线程已经持有该锁,则直接返回成功,否则尝试在Redis中创建一个新的锁并设置超时时间。在释放锁时,只有持有该锁的线程才能释放锁,否则会返回失败。如果当前线程持有该锁,那么在释放锁之前还会检查锁是否已经失效,如果没有失效则将锁的超时时间进行续期。
支持可重入和锁失效可续期的分布式锁:
javaCopy code
public class DistributedLock {
private static final String LOCK_PREFIX = "lock:";
private static final long DEFAULT_EXPIRE_TIME = 5000; // 默认锁的过期时间为5秒
private static final long DEFAULT_RETRY_INTERVAL = 100; // 默认重试间隔时间为100毫秒
private static final int DEFAULT_RETRY_TIMES = 10; // 默认重试次数为10次
private final RedisTemplate<String, String> redisTemplate;
private final String lockKey;
private final String lockValue;
private final long expireTime;
private final long retryInterval;
private final int retryTimes;
private final ThreadLocal<Long> lockCount;
public DistributedLock(RedisTemplate<String, String> redisTemplate, String lockKey) {
this(redisTemplate, lockKey, UUID.randomUUID().toString(), DEFAULT_EXPIRE_TIME, DEFAULT_RETRY_INTERVAL, DEFAULT_RETRY_TIMES);
}
public DistributedLock(RedisTemplate<String, String> redisTemplate, String lockKey, long expireTime) {
this(redisTemplate, lockKey, UUID.randomUUID().toString(), expireTime, DEFAULT_RETRY_INTERVAL, DEFAULT_RETRY_TIMES);
}
public DistributedLock(RedisTemplate<String, String> redisTemplate, String lockKey, long expireTime, long retryInterval, int retryTimes) {
this(redisTemplate, lockKey, UUID.randomUUID().toString(), expireTime, retryInterval, retryTimes);
}
public DistributedLock(RedisTemplate<String, String> redisTemplate, String lockKey, String lockValue, long expireTime, long retryInterval, int retryTimes) {
this.redisTemplate = redisTemplate;
this.lockKey = LOCK_PREFIX + lockKey;
this.lockValue = lockValue;
this.expireTime = expireTime;
this.retryInterval = retryInterval;
this.retryTimes = retryTimes;
this.lockCount = ThreadLocal.withInitial(() -> 0L);
}
public boolean tryLock() {
long now = System.currentTimeMillis();
for (int i = 0; i < retryTimes; i++) {
if (redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue)) {
redisTemplate.expire(lockKey, expireTime, TimeUnit.MILLISECONDS);
lockCount.set(lockCount.get() + 1);
return true;
}
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (currentValue != null && Long.parseLong(currentValue) < now) {
String oldValue = redisTemplate.opsForValue().getAndSet(lockKey, lockValue);
if (oldValue != null && oldValue.equals(currentValue)) {
redisTemplate.expire(lockKey, expireTime, TimeUnit.MILLISECONDS);
lockCount.set(lockCount.get() + 1);
return true;
}
}
try {
Thread.sleep(retryInterval);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return false;
}
public boolean unlock() {
if (lockCount.get() <= 0) {
throw new IllegalStateException("Lock count has been corrupted");
}
if (lockCount.get() > 1) {
lockCount.set(lockCount.get() - 1);
return false;
}
if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
lockCount.set(0L);
return true;
}
return false;
}
总结
我们刚刚讨论了如何优化高并发下的库存扣减问题。在最开始的方案中,我们使用了悲观锁来解决并发问题,但是由于锁的粒度太大,导致系统的性能无法满足高并发的需求。为了解决这个问题,我们引入了分段锁的概念,将锁的粒度降低到了每个分段库存上,从而增加了并发性。
进一步优化时,我们引入了分布式锁的概念,将锁的控制从单机扩展到了分布式环境下。在这个方案中,我们使用了Redis来作为分布式锁的实现,将锁的获取和释放都通过Redis的原子操作来实现。同时,我们还增加了可重入性和锁失效可续期的功能,保证了系统的稳定性和可用性。
总的来说,优化高并发下的库存扣减问题需要从多个方面考虑,包括锁的粒度、锁的控制方式、分布式环境下的锁实现等。在实际开发中,需要根据实际情况综合考虑这些因素,才能设计出性能高、可靠稳定的系统。