业务背景
思考这样一个问题:如果说优惠券模板有效期结束了之后,我们的模板状态依然是生效中。基于这个功能点考虑,我们需要在定时任务和定时消息中进行选择,以此来满足精准关闭优惠券模板功能。
调度器实现
- 只需配置定时任务逻辑,无需依赖其他基础设施。
- 实时性较差:定时任务的运行频率决定了更新的时效性(例如,每分钟扫描一次可能会延迟最多一分钟)。
- 资源浪费:即使没有过期的优惠券,任务依然会运行,占用资源。
RocketMQ 延迟消息实现
- 延迟消息:在优惠券模板创建时,发送一条延迟消息,延迟时间等于模板的有效期。
消息队列介绍
1. 什么是消息队列?
消息队列是一种用于异步通信的机制,允许不同的系统组件或服务之间交换信息。它的主要作用是将消息从发送者传递到接收者,同时解耦这两个组件的直接依赖。
2. 什么是 RocketMQ?
-
高吞吐量和低延迟:RocketMQ 设计用于处理大量的消息,并提供低延迟的消息传递服务,适合需要高性能的场景。
-
分布式架构:RocketMQ 使用分布式架构来支持大规模的消息传递。它可以水平扩展,以处理更大的数据量和更高的并发需求。
-
消息可靠性:RocketMQ 支持消息持久化和多副本机制,确保在系统故障时不会丢失消息。这使得消息的可靠性和一致性得到了保障。
-
高可用性和容错:RocketMQ 提供了高可用性的解决方案,包括多主多从等架构方案,确保系统的稳定性和连续性。
消息队列都有哪些作用?
异步解耦
最常见的一个场景是用户注册后,需要发送注册邮件和短信通知,以告知用户注册成功。假设每个任务串行执行,每个任务耗时分别为 50ms,则用户需要在注册页面等待总共 150ms 才能登录。
并行形式:对于用户来说,注册功能实际只需要注册系统存储用户的账户信息后,该用户便可以登录,后续的注册短信和邮件不是即时需要关注的步骤。其他的操作放入对应的 RocketMQ 中然后马上返回用户结果,由 RocketMQ 异步地进行这些操作。
削峰填谷
在秒杀或团队抢购活动中,由于用户请求量较大,导致流量暴增,秒杀的应用在处理如此大量的访问流量后,下游的通知系统无法承载海量的调用量,甚至会导致系统崩溃等问题而发生漏通知的情况。为解决这些问题,可在应用和下游通知系统之间加入 RocketMQ。
RocketMQ5.x延时消息设置优惠券结束状态
生产者核心代码
@Override
public void createCouponTemplate(CouponTemplateSaveReqDTO requestParam) {
// ......
// 使用 RocketMQ5.x 发送任意时间延时消息
// 定义 Topic
String couponTemplateDelayCloseTopic = "one-coupon_merchant-admin-service_coupon-template-delay_topic${unique-name:}";
// 通过 Spring 上下文解析占位符,也就是把咱们 VM 参数里的 unique-name 替换到字符串中
couponTemplateDelayCloseTopic = configurableEnvironment.resolvePlaceholders(couponTemplateDelayCloseTopic);
// 定义消息体
JSONObject messageBody = new JSONObject();
messageBody.put("couponTemplateId", couponTemplateDO.getId());
messageBody.put("shopNumber", UserContext.getShopNumber());
// 设置消息的送达时间,毫秒级 Unix 时间戳
Long deliverTimeStamp = couponTemplateDO.getValidEndTime().getTime();
// 构建消息体
String messageKeys = UUID.randomUUID().toString();
Message<JSONObject> message = MessageBuilder
.withPayload(messageBody)
.setHeader(MessageConst.PROPERTY_KEYS, messageKeys)
.build();
// 执行 RocketMQ5.x 消息队列发送&异常处理逻辑
SendResult sendResult;
try {
sendResult = rocketMQTemplate.syncSendDeliverTimeMills(couponTemplateDelayCloseTopic, message, deliverTimeStamp);
log.info("[生产者] 优惠券模板延时关闭 - 发送结果:{},消息ID:{},消息Keys:{}", sendResult.getSendStatus(), sendResult.getMsgId(), messageKeys);
} catch (Exception ex) {
log.error("[生产者] 优惠券模板延时关闭 - 消息发送失败,消息体:{}", couponTemplateDO.getId(), ex);
}
}
-
设置执行的Topic
-
定义消息体,优惠券ID和店铺编号
-
设置Unix时间戳(也就是目标时间)通过getValidEndTime()获取传入的结束字段
-
构建消息体并使用rocket发送延迟消息
消费者核心代码
@Override
public void onMessage(JSONObject message) {
// 开头打印日志,平常可 Debug 看任务参数,线上可报平安(比如消息是否消费,重新投递时获取参数等)
log.info("[消费者] 优惠券模板定时执行@变更模板表状态 - 执行消费逻辑,消息体:{}", message.toString());
// 修改指定优惠券模板状态为已结束
LambdaUpdateWrapper<CouponTemplateDO> updateWrapper = Wrappers.lambdaUpdate(CouponTemplateDO.class)
.eq(CouponTemplateDO::getShopNumber, message.getLong("shopNumber"))
.eq(CouponTemplateDO::getId, message.getLong("couponTemplateId"))
.set(CouponTemplateDO::getStatus, CouponTemplateStatusEnum.ENDED.getStatus());
couponTemplateService.update(updateWrapper);
}
-
添加
@RocketMQMessageListener
注解,其中加上 Topic 和消费者组定义。 -
实现
RocketMQListener
消息监听接口,泛型的类型是我们生产者发送消息的类定义。 -
修改指定优惠券模板状态为已结束
最终的日志输入类似于:
2024-08-22T19:23:17.456+08:00 INFO 78983 --- [cg-mading0924_2] CouponTemplateDelayExecuteStatusConsumer : [消费者] 优惠券模板定时执行@变更模板表状态 - 执行消费逻辑,消息体:{"couponTemplateId":1826580899668439042,"shopNumber":1810714735922956666}
本章逻辑流程图
完结散花,这里只是简单实现了rocket延迟消息,很多细节还没有掌握,继续学习!
部分图片和内容引用知识星球《拿个offer》牛券项目-https://nageoffer.com/onecoupon/