「黑马点评」六、秒杀优化

2025 年 4 月 23 日 星期三
24

「黑马点评」六、秒杀优化

异步秒杀优化

正常的执行需要执行六步,且涉及数据库,非常耗时,如果将判断库存和校验一人一单抽离出来,并将相关订单放到一个阻塞队列中,异步处理下单操作,可以优化更快的订单返回,以及下单速度

image.png|500

image.png|500

也就是说当前只需要确认能否执行,真正的执行可以慢慢做

将耗时的判断库存和校验一人一单的流程放入 Redis 中操作,并利用 Lua 脚本确保原子性

库存判断可以使用 String,一人一单直接使用 Set 即可

image.png|500

image.png|500

以上优化将同步变为异步且秒杀下单资格流程运行在 Redis 上,提高了并发能力

秒杀资格判断

需求:

  1. 新增秒杀优惠券的同时,将优惠券信息保存到 Redis 中
  2. 基于 Lua 脚本,判断秒杀库存、一人一单,决定用户是否抢购成功
  3. 如果抢购成功,将优惠券 id 和用户 id 封装后存入阻塞队列
  4. 开启线程任务,不断从阻塞队列中获取信息,实现异步下单功能

示例

  1. 将优惠券信息保存到Redis中

stringRedisTemplate.opsForValue().set(
    RedisConstants.SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());
  1. 基于 Lua 脚本,检验库存以及一人一单
-- 1.参数列表
local voucherId = ARGV[1]
local userId = ARGV[2]
local orderId = ARGV[3]
 
-- 2.数据key
local stockKey = 'seckill:stock:' .. voucherId
local orderKey = 'seckill:order:' .. voucherId
 
-- 3.脚本业务
if(tonumber(redis.call('get', stockKey)) <= 0) then
    return 1
end
if(redis.call('sismember', orderKey, userId) == 1) then
    return 2
end
redis.call('incrby', stockKey, -1)
redis.call('sadd', orderKey, userId)
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

使用

@Override
public Result<Long> seckillVoucher(Long voucherId) {
    // 1. 查询优惠券
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    // 2. 判断秒杀时间
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        return Result.error("秒杀尚未开始");
    }
    if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
        return Result.error("秒杀已结束");
    }
    // 3. 校验库存以及一人一单
    Long result = stringRedisTemplate.execute(
        SECKILL_SCRIPT,
        Collections.emptyList(),
        voucherId.toString(),UserHolder.getUser().getId().toString()
    );
    if (result != 0) {
        return Result.error(result == 1 ? "库存不足" : "不能重复下单");
    }
    // 4. 创建订单号
    Long orderId = redisIdWorker.nextId("order");
    // TODO 将订单信息推送到阻塞队列,等待异步创建订单
    return Result.success(orderId);
}
  1. 阻塞队列
private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024 * 1024);

VoucherOrder voucherOrder = orderTasks.take();
  1. 线程任务,异步创建订单
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();

@PostConstruct
private void init() {
    SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}

示例

在 seckillVoucher 中利用 Redis 完成库存余量、一人一单的判断,完成抢单

在 handleVoucherOrder 中将下单业务放入阻塞队列,利用独立线程异步下单

提交一个循环处理下单业务的任务类到线程池,利用 createVoucherOrder 在数据库中完成下单

@Slf4j
@Service
public class VoucherOrderServiceImpl implements IVoucherOrderService {

    @Resource
    private RedisIdWorker redisIdWorker;

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    
    @Resource
    private VoucherOrderMapper voucherOrderMapper;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private RedissonClient redissonClient;

    private IVoucherOrderService proxy;

    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();

    /**
     * 秒杀优惠券的主入口方法
     */
    @Override
    public Result<Long> seckillVoucher(Long voucherId) {
        // 1. 查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        // 2. 判断秒杀时间
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.error("秒杀尚未开始");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.error("秒杀已结束");
        }
        // 3. 校验库存以及一人一单
        Long result = stringRedisTemplate.execute(
            SECKILL_SCRIPT,
            Collections.emptyList(),
            voucherId.toString(),UserHolder.getUser().getId().toString()
        );
        if (result != 0) {
            return Result.error(result == 1 ? "库存不足" : "不能重复下单");
        }
        // 4. 创建订单号
        VoucherOrder voucherOrder = new VoucherOrder();
        voucherOrder.setId(redisIdWorker.nextId("order"));
        voucherOrder.setUserId(UserHolder.getUser().getId());
        voucherOrder.setVoucherId(voucherId);
        voucherOrder.setPayType(1); // 设置支付方式,1:余额支付
        voucherOrder.setStatus(1);  // 设置订单状态,1:未支付
        // 5. 将订单信息放入阻塞队列
        orderTasks.add(voucherOrder);
        // 6. 获取代理对象
        proxy = (IVoucherOrderService) AopContext.currentProxy();
        // 7. 返回订单id
        return Result.success(voucherOrder.getId());
    }

    /**
     * 创建订单的事务方法
     */
    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder) {
        // 1. 一人一单判断
        long count = voucherOrderMapper.selectCount(voucherOrder.getUserId(), voucherOrder.getVoucherId());
        if (count > 0) {
            log.error("用户{}重复下单", voucherOrder.getUserId());
            return;
        }
        // 2. 扣减库存
        boolean isSuccess = seckillVoucherService.update()
            .setSql("stock = stock - 1")
            .eq("voucher_id", voucherOrder.getVoucherId())
            .gt("stock", 0)  // 使用大于0的条件替代等于当前库存的条件
            .update();
        if (!isSuccess) {
            log.error("库存不足");
            return;
        }
        // 3. 创建订单
        voucherOrderMapper.insert(voucherOrder);
    }

    /**
     * 处理订单的方法
     */
    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();
        // 获取分布式锁
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        boolean isLock = lock.tryLock();
        if (!isLock) {
            log.error("用户{}重复下单", userId);
            return;
        }
        try {
            // 创建订单
            proxy.createVoucherOrder(voucherOrder);
        } finally {
            // 释放锁
            lock.unlock();
        }
    }

    /**
     * 异步处理订单的线程任务类
     */
    private class VoucherOrderHandler implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    // 1. 获取队列中的订单信息
                    VoucherOrder voucherOrder = orderTasks.take();
                    // 2. 处理订单
                    handleVoucherOrder(voucherOrder);
                } catch (InterruptedException e) {
                    log.error("秒杀订单处理线程异常", e);
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    /**
     * 初始化方法,启动处理线程
     */
    @PostConstruct
    private void init() {
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }
}

问题:

基于内存的阻塞队列,受到 JVM 内存限制

数据不持久,安全性问题

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...