在上一篇博客中,我们介绍了如何利用 Redis 和 Lua 脚本来高效处理秒杀活动中的高并发请求,保证用户体验。本文将进一步优化秒杀系统,通过引入阻塞队列实现异步下单,从而提高系统的整体性能和稳定性。
引言
秒杀活动往往伴随着极高的并发请求,对系统的性能和稳定性提出了巨大挑战。同步处理订单请求可能导致数据库压力过大,影响系统响应时间。为了缓解这一问题,我们可以采用异步下单的方式,将订单请求先放入阻塞队列,由后台线程逐一处理,从而降低数据库的瞬时压力。
方案设计
基本思路
- 用户发起秒杀请求,先通过 Redis Lua 脚本进行资格判断。
- 通过 Lua 脚本判断用户是否有购买资格,并扣减库存。
- 将订单信息放入阻塞队列中,由后台线程异步处理订单创建和数据库操作。
- 返回订单 ID 给用户。
具体实现
Lua 脚本
Lua 脚本的逻辑保持不变,继续用于判断秒杀资格和扣减库存。
Java 代码
在 Java 代码中,我们通过阻塞队列实现异步下单,并利用 Redisson 分布式锁来确保订单操作的线程安全。
java
@Service
@Slf4j
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
@Resource
private ISeckillVoucherService seckillVoucherService;
@Resource
private RedisIdWorker redisIdWorker;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Resource
private RedissonClient redissonClient;
private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
static {
SECKILL_SCRIPT = new DefaultRedisScript<>();
SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
SECKILL_SCRIPT.setResultType(Long.class);
}
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();
@PostConstruct
private void init() {
SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
private class VoucherOrderHandler implements Runnable {
@Override
public void run() {
while (true) {
try {
VoucherOrder voucherOrder = orderTasks.take();
handleVoucherOrder(voucherOrder);
} catch (Exception e) {
log.error("创建订单失败", e);
}
}
}
}
private void handleVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
RLock lock = redissonClient.getLock("lock:order:" + userId);
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("不允许重复下单");
return;
}
try {
proxy.crateVoucherOrder(voucherOrder);
} finally {
lock.unlock();
}
}
private IVoucherOrderService proxy;
@Override
public Result seckillVoucher(Long voucherId) {
Long userId = UserHolder.getUser().getId();
Long result = stringRedisTemplate.execute(
SECKILL_SCRIPT,
Collections.emptyList(),
voucherId.toString(), userId.toString()
);
int r = result.intValue();
if (r != 0) {
return Result.fail(r == 1 ? "库存不足" : "不能重复下单");
}
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(userId);
voucherOrder.setVoucherId(voucherId);
orderTasks.add(voucherOrder);
proxy = (IVoucherOrderService) AopContext.currentProxy();
return Result.ok(orderId);
}
@Override
@Transactional
public void crateVoucherOrder(VoucherOrder voucherOrder) {
Long userId = voucherOrder.getUserId();
int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();
if (count > 0) {
log.error("不允许重复下单!");
return;
}
boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id",
voucherOrder.getVoucherId())
.gt("stock", 0).update();
if (!success) {
log.error("库存不足!");
return;
}
save(voucherOrder);
}
}
代码详解
-
初始化阻塞队列和线程池:
- 使用
BlockingQueue
和ExecutorService
实现一个单线程的订单处理机制,在服务初始化时启动订单处理线程。
- 使用
-
秒杀请求处理:
- 用户发起秒杀请求时,首先通过 Lua 脚本判断秒杀资格和扣减库存。
- 如果有购买资格,将订单信息放入阻塞队列中。
-
订单处理线程:
- 订单处理线程从阻塞队列中取出订单,并在获取到用户锁后创建订单,防止同一用户重复下单。
-
事务处理:
- 在订单处理方法中使用事务管理,确保订单创建和库存扣减的原子性。
结论
通过引入阻塞队列实现异步下单,我们有效地减少了数据库的瞬时压力,提高了系统的整体性能和稳定性。这种方法不仅适用于秒杀活动,还可以推广到其他高并发场景,如抢购、促销活动等。希望本文对您理解和实现高并发系统有所帮助。
可能出现的问题
我在一次批量用一千个线程去抢优惠卷的时候发现,优惠卷没有抢完,初步判断是阻塞队列的大小过小,内存的限制问题。