黑马点评秒杀优化和场景补充

一、对于课程中的队列优化

这里使用RabbitMQ 来取代课程中的队列,将添加订单数据至mysql改成异步的,并且使用令牌桶 算法来进行一定程度上的限流。

1.1优化后核心代码

下单代码如下

ini 复制代码
@Resourceprivate RedisWork redisWork;

@Resourceprivate ISeckillVoucherService seckillVoucherService;

@Resourceprivate StringRedisTemplate stringRedisTemplate;

@Resourceprivate RedissonClient redissonClient;

private RateLimiter rateLimiter= RateLimiter.create(10);

@Resourceprivate MQSender mqSender;

private static final DefaultRedisScript<Long> SECKILL_SCRIPT;

private IVoucherOrderService iVoucherOrderService;

static {
    SECKILL_SCRIPT = new DefaultRedisScript<>();
    SECKILL_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));
    SECKILL_SCRIPT.setResultType(Long.class);
}


/**
 * 秒杀优惠券
 * @param voucherId
 * @return
 */@Overridepublic Result seckillVoucher(Long voucherId) {
    RLock lock = redissonClient.getLock("seckill:lock");
    lock.tryLock();
    //令牌桶算法 限流if (!rateLimiter.tryAcquire(1000, TimeUnit.MILLISECONDS)){
        return Result.fail("目前网络正忙,请重试");
    }
    long orderId = redisWork.nextId("order");
    Long userId = UserHolder.getUser().getId();
    // 1 执行lua脚本int result = stringRedisTemplate.execute(
            SECKILL_SCRIPT,
            Collections.emptyList(),
            voucherId.toString(), userId.toString(),String.valueOf(orderId)
    ).intValue();
    if (result != 0) {
        return Result.fail(result == 1 ? "库存不足" : "不能重复下单");
    }
    // 2 判断是否为0// 返回订单idVoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setId(orderId);
    voucherOrder.setVoucherId(voucherId);
    voucherOrder.setUserId(userId);
    //发送消息通知向数据库中添加订单数据
    mqSender.sendSeckillMessage(JSON.toJSONString(voucherOrder));
    return Result.ok(orderId);
}

RabbitMQ的发送者(生产者)

typescript 复制代码
package com.hmdp.rabbitmq;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Slf4j@Componentpublic class MQSender {

    @Resourceprivate RabbitTemplate rabbitTemplate;

    /**
     * 发送秒杀信息
     * @param message
     */public void sendSeckillMessage(String message){
        log.info("发送消息:{}", message);
        rabbitTemplate.convertAndSend("seckill.direct", "seckill.success", message);
    }
}

RabbitMQ的消费者

less 复制代码
package com.hmdp.rabbitmq;

import com.hmdp.entity.VoucherOrder;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import lombok.extern.slf4j.Slf4j;
import com.alibaba.fastjson.JSON;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

@Component@Slf4jpublic class MQReceiver {

    @Resource
    IVoucherOrderService voucherOrderService;

    @Resource
    ISeckillVoucherService seckillVoucherService;

    /**
     * 监听秒杀消息并下单
     * @param message
     */@RabbitListener(
            bindings = @QueueBinding(
                    value = @Queue(name = "seckill.queue", durable = "true"),
                    exchange = @Exchange(name = "seckill.direct"),
                    key = "seckill.success"
    ))@Transactionalpublic void receiveSeckillVoucher(String message){
        log.info("收到秒杀消息:{}", message);
        VoucherOrder voucherOrder = JSON.parseObject(message, VoucherOrder.class);
        Long voucherId = voucherOrder.getVoucherId();
        //5.一人一单Long userId = voucherOrder.getUserId();
        //5.1查询订单int count = voucherOrderService.query().eq("user_id",userId).eq("voucher_id", voucherId).count();
        //5.2判断是否存在if(count>0){
            //用户已经购买过了
            log.error("该用户已购买过");
            return ;
        }
        log.info("扣减库存");
        //6.扣减库存boolean success = seckillVoucherService
                .update()
                .setSql("stock = stock-1")
                .eq("voucher_id", voucherId)
                .gt("stock",0)//cas乐观锁
                .update();
        if(!success){
            log.error("库存不足");
            return;
        }
        //直接保存订单try {
            voucherOrderService.save(voucherOrder);
        } catch (Exception e) {
            log.error("订单异常信息:{}", e.getMessage());
        }
    }
}

lua脚本:

lua 复制代码
--1 参数列表
--1.1 优惠卷id
local voucherId = ARGV[1]
--1.2 用户id
local userId = ARGV[2]
--1.3 订单id
local orderId = ARGV[3]

local stockKey = 'seckill:stock:' .. voucherId

local orderKey = 'seckill:order:' .. voucherId

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

二、当前秒杀业务问题

业务比较单薄,说到底只有一个秒杀部分,没有秒杀之后会发生什么,比如秒杀后用户未及时付款导致订单超时,或者用户如果生成订单后取消订单又该如何处理,这些都是需要考虑的,现在我来大概梳理一下这部分补充之后的业务流程

大概可以分为这几步:

秒杀下单、订单创建、支付处理、超时处理、主动退单

这里我们只考虑秒杀下单、订单创建、超时处理、主动退单 这四步,支付处理 这部分可以参照一下黑马商场的那个模块,这里我放张图片快速过一下

秒杀下单和订单创建的逻辑其实和上面基本一致,我们来看一下超时处理主动退单

2.1场景 1:订单超时未支付

秒杀订单创建后,用户通常有 10-30 分钟支付时间,超时后需自动恢复库存。一般设计到定时的问题都会用到xxl-job等定时任务组件,但是高并发下不推荐定时任务 轮询 (压力大、延迟高),优先用「消息中间件 延迟队列」,这里我们使用RabbitMQ。

说到消息队列的延迟,首先想到的肯定是死信队列,这里给出代码:

2.1.1配置延迟队列(处理 30 分钟未支付订单)

新增 RabbitMQ 延迟队列配置,用于监听订单超时:

kotlin 复制代码
@Configurationpublic class SeckillDelayMQConfig {
    // 延迟交换机@Beanpublic DirectExchange seckillDelayExchange() {
        return ExchangeBuilder.directExchange("seckill.delay.exchange")
                .durable(true)
                .build();
    }

    // 延迟队列(30分钟超时)@Beanpublic Queue seckillDelayQueue() {
        return QueueBuilder.durable("seckill.delay.queue")
                .withArgument("x-message-ttl", 30 * 60 * 1000) // 30分钟TTL
                .withArgument("x-dead-letter-exchange", "seckill.dlx.exchange") // 死信交换机
                .withArgument("x-dead-letter-routing-key", "seckill.timeout") // 死信路由键
                .build();
    }

    // 绑定延迟交换机和延迟队列@Beanpublic Binding delayBinding() {
        return BindingBuilder.bind(seckillDelayQueue())
                .to(seckillDelayExchange())
                .with("seckill.delay");
    }

    // 死信交换机(接收超时消息)@Beanpublic DirectExchange seckillDlxExchange() {
        return ExchangeBuilder.directExchange("seckill.dlx.exchange")
                .durable(true)
                .build();
    }

    // 超时处理队列(最终消费超时订单)@Beanpublic Queue seckillTimeoutQueue() {
        return QueueBuilder.durable("seckill.timeout.queue").build();
    }

    // 绑定死信交换机和超时处理队列@Beanpublic Binding dlxBinding() {
        return BindingBuilder.bind(seckillTimeoutQueue())
                .to(seckillDlxExchange())
                .with("seckill.timeout");
    }
}

2.1.2修改秒杀部分的逻辑

在发送创建订单消息前面先将订单放入延迟队列

typescript 复制代码
@Overridepublic Result seckillVoucher(Long voucherId) {
    // 现有逻辑:限流、Lua校验、创建订单...(省略)// 新增:发送延迟消息(30分钟后检查是否支付)// 消息体包含订单ID、优惠券ID、用户ID
    Map<String, Object> delayMsg = new HashMap<>();
    delayMsg.put("orderId", orderId);
    delayMsg.put("voucherId", voucherId);
    delayMsg.put("userId", userId);
    mqSender.sendSeckillDelayMessage(delayMsg); // 新增延迟消息发送方法// 原有:发送创建订单消息
    mqSender.sendSeckillMessage(JSON.toJSONString(voucherOrder));
    return Result.ok(orderId);
}

// 在MQSender中新增延迟消息发送方法public void sendSeckillDelayMessage(Map<String, Object> message) {
    rabbitTemplate.convertAndSend(
        "seckill.delay.exchange", 
        "seckill.delay", 
        message
    );
}

2.1.3编写库存恢复 Lua 脚本(保证 Redis 原子性)

创建**recover_stock.lua**,防止并发下重复恢复库存:

lua 复制代码
-- 参数:订单ID、优惠券ID、用户ID
local orderId = ARGV[1]
local voucherId = ARGV[2]
local userId = ARGV[3]

-- Redis键:库存键、用户下单记录键、已恢复订单记录(防重复)
local stockKey = "seckill:stock:" .. voucherId
local orderKey = "seckill:order:" .. voucherId -- 原一人一单的Set集合
local recoveredKey = "seckill:recovered:" .. voucherId -- 记录已恢复的订单

-- 1. 判断订单是否已恢复过(防重复消费)
if redis.call("sismember", recoveredKey, orderId) == 1 then
    return 0 -- 已恢复,直接返回
end

-- 2. 恢复库存(stock + 1)
redis.call("incrby", stockKey, 1)

-- 3. 从用户下单记录中移除(允许用户重新秒杀)
redis.call("srem", orderKey, userId)

-- 4. 记录已恢复的订单ID
redis.call("sadd", recoveredKey, orderId)

return 1 -- 恢复成功

2.1.4实现库存恢复方法(数据库 + Redis 双写)

scss 复制代码
@Servicepublic class VoucherOrderServiceImpl implements IVoucherOrderService {
    // 初始化Lua脚本private static final DefaultRedisScript<Long> RECOVER_SCRIPT;
    static {
        RECOVER_SCRIPT = new DefaultRedisScript<>();
        RECOVER_SCRIPT.setLocation(new ClassPathResource("recover_stock.lua"));
        RECOVER_SCRIPT.setResultType(Long.class);
    }

    /**
     * 库存恢复核心方法
     * @param orderId 订单ID
     * @param voucherId 优惠券ID
     * @param userId 用户ID
     */public void recoverStock(Long orderId, Long voucherId, Long userId) {
        // 1. 数据库层面:更新订单状态为"已取消"+恢复库存(乐观锁保证原子性)// 1.1 先更新订单状态(仅未支付订单可取消)int updateOrder = lambdaUpdate()
                .set(VoucherOrder::getStatus, 2) // 2-已取消
                .eq(VoucherOrder::getId, orderId)
                .eq(VoucherOrder::getStatus, 0) // 仅未支付订单
                .update();
        if (updateOrder == 0) {
            log.info("订单{}无需恢复(已支付或已取消)", orderId);
            return;
        }

        // 1.2 恢复数据库库存boolean recoverDb = seckillVoucherService.lambdaUpdate()
                .set(SeckillVoucher::getStock, SeckillVoucher::getStock + 1)
                .eq(SeckillVoucher::getVoucherId, voucherId)
                .update();
        if (!recoverDb) {
            log.error("订单{}数据库库存恢复失败", orderId);
            return;
        }

        // 2. Redis层面:执行Lua脚本恢复库存Long redisResult = stringRedisTemplate.execute(
                RECOVER_SCRIPT,
                Collections.emptyList(),
                orderId.toString(),
                voucherId.toString(),
                userId.toString()
        );

        if (redisResult == 1) {
            log.info("订单{}库存恢复成功(DB+Redis)", orderId);
        } else {
            log.warn("订单{}Redis库存已恢复,无需重复操作", orderId);
        }
    }
}

2.1.5新增消费者监听超时队列,调用库存恢复方法:

kotlin 复制代码
@Componentpublic class MQRecoveryReceiver {
    @Resourceprivate IVoucherOrderService voucherOrderService;

    // 处理超时未支付订单(核心优化:添加状态校验)@RabbitListener(queues = "seckill.timeout.queue")public void handleTimeoutOrder(Map<String, Object> msg, Channel channel, Message message) throws IOException {
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            Long orderId = Long.parseLong(msg.get("orderId").toString());
            Long voucherId = Long.parseLong(msg.get("voucherId").toString());
            Long userId = Long.parseLong(msg.get("userId").toString());

            // 关键步骤:查询订单当前状态VoucherOrder order = voucherOrderService.getById(orderId);
            if (order == null) {
                // 订单不存在,直接确认消息
                channel.basicAck(deliveryTag, false);
                return;
            }

            // 若订单已支付(状态1)或已取消(状态2),则不执行恢复if (order.getStatus() != 0) {
                log.info("订单{}已支付或取消,无需处理超时", orderId);
                channel.basicAck(deliveryTag, false);
                return;
            }

            // 仅未支付订单(状态0)执行库存恢复
            voucherOrderService.recoverStock(orderId, voucherId, userId);
            channel.basicAck(deliveryTag, false);

        } catch (Exception e) {
            log.error("超时订单处理失败", e);
            channel.basicNack(deliveryTag, false, false); // 拒绝消息,不重回队列
        }
    }
}

2.1.6支付成功接口:更新订单状态

在支付成功的业务逻辑中,添加订单状态更新操作(关键是将状态改为 "已支付"):

perl 复制代码
@Servicepublic class PayService {
    @Autowiredprivate IVoucherOrderService voucherOrderService;

    /**
     * 用户支付成功后调用
     * @param orderId 订单ID
     */public Result paySuccess(Long orderId) {
        // 1. 更新订单状态为"已支付"boolean success = voucherOrderService.lambdaUpdate()
                .set(VoucherOrder::getStatus, 1) // 1-已支付
                .eq(VoucherOrder::getId, orderId)
                .eq(VoucherOrder::getStatus, 0) // 仅未支付订单可更新
                .update();
        
        if (!success) {
            return Result.fail("订单状态异常,支付失败");
        }
        // 2. 其他支付后续逻辑(如生成支付凭证等)return Result.ok("支付成功");
    }
}

2.2场景2:用户主动取消订单

就是直接取消订单,然后恢复库存即可

scss 复制代码
@Servicepublic class VoucherOrderServiceImpl implements IVoucherOrderService {
    @Autowiredprivate StringRedisTemplate stringRedisTemplate;
    @Autowiredprivate SeckillVoucherServiceImpl seckillVoucherService;
    @Autowiredprivate RedissonClient redissonClient;

    // 初始化库存恢复Lua脚本(复用之前的)private static final DefaultRedisScript<Long> RECOVER_STOCK_SCRIPT;
    static {
        RECOVER_STOCK_SCRIPT = new DefaultRedisScript<>();
        RECOVER_STOCK_SCRIPT.setLocation(new ClassPathResource("recover_stock.lua"));
        RECOVER_STOCK_SCRIPT.setResultType(Long.class);
    }

    /**
     * 同步取消订单接口(用户主动取消)
     * @param orderId 订单ID
     * @return 取消结果
     */@Override@Transactional // 同步操作,用事务保证DB一致性public Result cancelOrderSync(Long orderId) {
        // 1. 获取当前用户ID(需确保登录态,从UserHolder获取)Long userId = UserHolder.getUser().getId();
        if (userId == null) {
            return Result.fail("请先登录");
        }

        // 2. 分布式锁:防止同一订单被并发取消(避免重复恢复库存)RLock lock = redissonClient.getLock("lock:cancel:order:" + orderId);
        boolean isLock = lock.tryLock(5, 10, TimeUnit.SECONDS); // 5秒等待,10秒自动释放if (!isLock) {
            return Result.fail("取消请求处理中,请稍后再试");
        }

        try {
            // 3. 核心校验:订单存在+归属正确+状态为"未支付"VoucherOrder order = getById(orderId);
            if (order == null) {
                return Result.fail("订单不存在");
            }
            if (!order.getUserId().equals(userId)) {
                return Result.fail("无权限取消此订单");
            }
            if (order.getStatus() != 0) { // 0=未支付,只有未支付订单可取消return Result.fail("订单已支付/已取消,无需重复操作");
            }

            // 4. 同步执行库存恢复(DB+Redis)// 4.1 更新订单状态为"已取消"(事务内执行,失败回滚)boolean updateOrder = lambdaUpdate()
                    .set(VoucherOrder::getStatus, 2) // 2=已取消
                    .eq(VoucherOrder::getId, orderId)
                    .eq(VoucherOrder::getStatus, 0)
                    .update();
            if (!updateOrder) {
                return Result.fail("取消失败,请重试");
            }

            // 4.2 恢复数据库库存(乐观锁,避免超恢复)boolean recoverDbStock = seckillVoucherService.lambdaUpdate()
                    .set(SeckillVoucher::getStock, SeckillVoucher::getStock + 1)
                    .eq(SeckillVoucher::getVoucherId, order.getVoucherId())
                    .update();
            if (!recoverDbStock) {
                throw new RuntimeException("库存恢复失败,取消回滚"); // 抛异常触发事务回滚
            }

            // 4.3 恢复Redis库存(Lua脚本原子执行)Long redisResult = stringRedisTemplate.execute(
                    RECOVER_STOCK_SCRIPT,
                    Collections.emptyList(),
                    orderId.toString(),
                    order.getVoucherId().toString(),
                    userId.toString()
            );
            if (redisResult != 1) {
                log.warn("订单{}Redis库存恢复重复,已忽略", orderId);
            }

            return Result.ok("订单取消成功,库存已恢复");
        } catch (Exception e) {
            log.error("同步取消订单失败", e);
            return Result.fail("取消失败,请稍后重试");
        } finally {
            // 释放分布式锁if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}
相关推荐
猎豹奕叔2 小时前
设计模式的重要设计原则,建议收藏
后端
低音钢琴3 小时前
【碎片化学习】SpringBoot中的自动配置(Auto Configuration)
spring boot·后端
canonical-entropy3 小时前
集成NopReport动态生成Word表格
后端·低代码·函数式编程·可逆计算·nop平台
IT_陈寒4 小时前
Python 3.12新特性实战:5个让你的代码提速30%的性能优化技巧
前端·人工智能·后端
禁默4 小时前
Rokid JSAR 技术开发全指南+实战演练
后端·restful·rokid·jsar
元气满满的霄霄4 小时前
Spring Boot整合缓存——Redis缓存!超详细!
java·spring boot·redis·后端·缓存·intellij-idea
小蒜学长11 小时前
springboot多功能智能手机阅读APP设计与实现(代码+数据库+LW)
java·spring boot·后端·智能手机
追逐时光者12 小时前
精选 4 款开源免费、美观实用的 MAUI UI 组件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net
你的人类朋友13 小时前
【Docker】说说卷挂载与绑定挂载
后端·docker·容器