目录
在电商系统中,秒杀活动是一个非常典型的高并发场景。在这种场景下,如何保证库存数量的正确性,防止超卖(Overselling)是实现的难点之一。今天,我将介绍如何使用乐观锁和 CAS(Compare-And-Swap)技术来解决超卖问题,并通过代码示例进行详细说明。希望对大家在处理高并发库存扣减时有所帮助。
一、背景介绍
1.1 超卖问题的产生
在秒杀或抢购活动中,由于用户并发请求过多,多个线程可能同时读取到相同的库存数据,导致库存数量被多个线程同时修改。这样就容易出现“库存变成负数”的情况,即产生了超卖问题。
超卖问题的典型场景:
- 当多个用户几乎同时提交秒杀请求时,如果没有合适的库存锁机制,所有线程会读取到同样的库存数,并尝试同时进行扣减操作。
- 结果就是库存被重复扣减,导致卖出的数量超过了实际库存,造成库存混乱。
1.2 解决超卖的常见方法
为了解决超卖问题,常见的方法有以下几种:
-
悲观锁:
- 使用数据库的行锁或分布式锁来防止多个线程同时修改库存。
- 缺点:在高并发环境下,容易导致锁等待和阻塞,影响系统性能。
-
乐观锁:
- 通过数据库的版本号(Version)或 CAS 技术(Compare-And-Swap)来实现数据的原子性更新,避免并发写操作冲突。
- 优点:在高并发场景下,不会阻塞线程,能够提高系统的吞吐量。
二、乐观锁与 CAS 技术的实现方案
乐观锁(Optimistic Locking)认为并发操作不会经常发生,因此在每次修改数据时,检查数据是否被其他线程修改过。如果数据没有被修改过,则执行更新操作;如果数据已经被其他线程修改,则放弃本次操作或进行重试。
在数据库操作中,乐观锁通常是通过版本号机制或 CAS 技术来实现的。
-
版本号机制:
- 在数据表中添加一个
version
字段,每次更新数据时,检查version
是否与读取时的一致,如果一致,则更新,并将version
加 1。
- 在数据表中添加一个
-
CAS(Compare-And-Swap):
- 使用 SQL 中的条件更新语句,如
WHERE stock > 0
,在扣减库存时,只有当库存量满足条件时,才进行更新操作,从而实现库存的原子性扣减。
- 使用 SQL 中的条件更新语句,如
三、代码实现:使用乐观锁与 CAS 解决库存超卖问题
以下是使用乐观锁与 CAS 技术实现库存扣减的代码示例:
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
// 1.查询优惠券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
// 2.判断秒杀是否开始
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
// 尚未开始
return Result.fail("秒杀尚未开始!");
}
// 3.判断秒杀是否已经结束
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
// 已经结束
return Result.fail("秒杀已经结束!");
}
// 4.判断库存是否充足
if (voucher.getStock() < 1) {
// 库存不足
return Result.fail("库存不足!");
}
// 5.扣减库存 - 使用乐观锁和CAS
boolean success = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0) // 防止超卖
.update();
if (!success) {
// 扣减库存失败
return Result.fail("库存不足!");
}
// 6.创建订单
VoucherOrder voucherOrder = new VoucherOrder();
// 6.1.设置订单id
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
// 6.2.设置用户id
Long id = UserHolder.getUser().getId();
voucherOrder.setUserId(id);
// 6.3.设置优惠券id
voucherOrder.setVoucherId(voucherId);
save(voucherOrder);
// 7.返回订单id
return Result.ok(orderId);
}
}
四、代码解析
-
查询优惠券信息:
- 通过
seckillVoucherService.getById(voucherId)
查询秒杀优惠券的详细信息,如开始时间、结束时间和库存量。
- 通过
-
判断秒杀活动是否在有效期内:
- 判断秒杀是否尚未开始(
beginTime.isAfter(LocalDateTime.now())
)或已结束(endTime.isBefore(LocalDateTime.now())
)。
- 判断秒杀是否尚未开始(
-
库存判断:
- 在进行库存扣减前,先判断库存是否大于 0,如果小于 1,则直接返回“库存不足”的提示。
-
使用乐观锁与 CAS 扣减库存:
- 通过
update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0)
的条件更新语句进行扣减库存操作。 - 这里使用了
gt("stock", 0)
来确保只有在库存大于 0 时,才执行扣减操作,从而实现库存的原子性更新。
- 通过
-
创建订单:
- 在扣减库存成功后,为用户生成一个新的订单,并保存到数据库中。
-
返回订单 ID:
- 返回创建的订单 ID 作为秒杀成功的结果。
五、总结
通过使用乐观锁与 CAS 技术,可以在高并发场景下有效防止库存超卖问题,保证库存操作的原子性和一致性。该方案相比悲观锁在高并发场景下具有更好的性能,同时避免了锁竞争和阻塞。