当前位置: 首页 > news >正文

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

目录

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

1.1优化后核心代码

二、当前秒杀业务问题

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

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

2.1.2修改秒杀部分的逻辑

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

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

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

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

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

三、最后


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

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

1.1优化后核心代码

下单代码如下

@Resource
private RedisWork redisWork;@Resource
private ISeckillVoucherService seckillVoucherService;@Resource
private StringRedisTemplate stringRedisTemplate;@Resource
private RedissonClient redissonClient;private RateLimiter rateLimiter= RateLimiter.create(10);@Resource
private 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*/
@Override
public 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的发送者(生产者)

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
@Component
public class MQSender {@Resourceprivate RabbitTemplate rabbitTemplate;/*** 发送秒杀信息* @param message*/public void sendSeckillMessage(String message){log.info("发送消息:{}", message);rabbitTemplate.convertAndSend("seckill.direct", "seckill.success", message);}
}

RabbitMQ的消费者

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
@Slf4j
public class MQReceiver {@ResourceIVoucherOrderService voucherOrderService;@ResourceISeckillVoucherService 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脚本:

--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:' .. voucherIdlocal orderKey = 'seckill:order:' .. voucherIdif(tonumber(redis.call('get',stockKey)) <= 0) thenreturn 1endif(redis.call('sismember',orderKey,userId) == 1) thenreturn 2
endredis.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 延迟队列配置,用于监听订单超时:

@Configuration
public 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修改秒杀部分的逻辑

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

@Override
public Result seckillVoucher(Long voucherId) {// 现有逻辑:限流、Lua校验、创建订单...(省略)// 新增:发送延迟消息(30分钟后检查是否支付)// 消息体包含订单ID、优惠券ID、用户IDMap<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,防止并发下重复恢复库存:

-- 参数:订单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 thenreturn 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 双写)

@Service
public 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新增消费者监听超时队列,调用库存恢复方法:

@Component
public 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支付成功接口:更新订单状态

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

@Service
public 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:用户主动取消订单

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

@Service
public 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();}}}
}

三、最后

感兴趣的宝子可以关注一波,后续会更新更多有用的知识!!!

http://www.dtcms.com/a/442067.html

相关文章:

  • 嵌入式硬件——基于IMX6ULL的UART(通用异步收发传输器)
  • Spark Shuffle:分布式计算的数据重分布艺术
  • 网站能看出建设时间吗网页设计工资统计
  • Postgres数据库truncate表无有效备份恢复---惜分飞
  • 【邪修玩法】如何在WPF中开放 RESTful API 服务
  • 开源 C++ QT QML 开发(二)工程结构
  • 2025生成式AI部署避坑指南:芯片卡脖子与依赖链爆炸的实战解决方案
  • 互联网新热土视角下开源AI大模型与S2B2C商城小程序的县域市场渗透策略研究
  • 外文网站制作佛山做企业网站
  • 线上网站建设需求西安做网站 怎样备案
  • 《数据密集型应用系统设计2》--OLTP/OLAP/全文搜索的数据存储与查询
  • 【ROS2学习笔记】RViz 三维可视化
  • 如何实现理想中的人形机器人
  • 【深度学习|学习笔记】神经网络中有哪些损失函数?(一)
  • AP2协议与智能体(Intelligent Agents)和电商支付
  • Upload-labs 文件上传靶场
  • 江苏省网站备案查询系统天津做网站找津坤科技专业
  • 虚幻版Pico大空间VR入门教程 05 —— 原点坐标和项目优化技巧整理
  • AI绘画新境界:多图融合+4K直出
  • 云图书馆平台网站建设方案柴沟堡做网站公司
  • 第67篇:AI+农业:精准种植、智能养殖与病虫害识别
  • GitPuk入门到实战(5) - 如何进行标签管理
  • 特征工程中平衡高频与低频数据的权重分配试错
  • 做网站需要买企业网站icp备案
  • 兰亭妙微QT软件开发经验:跨平台桌面端界面设计的三大要点
  • 大数据工程师认证项目:汽车之家数据分析系统,Hadoop分布式存储+Spark计算引擎
  • 【AI4S】DrugChat:迈向在药物分子图上实现类似ChatGPT的功能
  • 构建基于Hexo、Butterfly、GitHub与Cloudflare的高性能个人博客
  • 自动驾驶中的传感器技术64——Navigation(1)
  • RAG技术全栈指南学习笔记------基于Datawhale all-in-rag开源项目