文章目录
真实系统优化:RocketMQ消息积压问题解决
在大规模分布式系统中,消息队列(MQ)是实现解耦和异步处理的关键组件,但在实际应用中,消息积压问题常常会成为系统稳定性的瓶颈。本文将探讨如何在生产环境中解决 RocketMQ 消息积压问题,特别是在多种系统和服务交互中,如何优化消息消费效率,确保系统的高可用性和稳定性。
生产中MQ场景
在我们的系统中,RocketMQ主要用于:
- IDM身份认证系统同步档案与权限数据(消费者与发送者):和IDM身份认证系统实现数据同步,如经销商、分销商、业务员等。IDM系统较为稳定,只需要注意消费的顺序性即可,在之前的文章中已经解决了。
- 和门店运营平台同步门店数据(消费者):消息体过大,往往门店信息包含许多属性信息(门店联系人、事业部、历史版本、流程相关数据等),并且门店运营平台不太稳定,有时会出现系统问题造成消息无法及时消费
- 和数据平台同步订单信息(消费者):消息多,如果因系统不稳定,那么就会造成需要重试发送消息的数量大
- 和SAP基础信息平台同步(发送者):同步除了订单外所有档案信息、所有物料信息,消息内容不大、量不大、但是要保证实时性,SAP系统很稳定,对方基本不出错。
问题分析与总结
我们要解决的问题:
- 即使消费者平台不稳定,消息也不会被过度积压。
- 不会因为消息积压导致我们的系统崩溃或者性能问题。
- 直接通过异步、重试等机制确保消息的可靠消费
总结问题:
- 和门店运营平台同步门店数据:容易造成消息挤压
- 和数据平台同步订单信息:消息数量大,容易造成消息挤压
- 和SAP基础信息平台同步:要保证实时性与准确性。
解决方案
门店与订单方面解决方案:同步发送单个数据+定时任务重试+超过重试5次前端页面管理重试+预留批量重试接口
在实际业务中,特别是门店与订单数据同步的场景中,消费者(数据平台)不稳定时,使用传统的消息队列(MQ)方案可能会导致消息积压,影响系统稳定性。针对这个问题,提出了 异步发送同步数据 + 定时任务重试 + 前端页面管理重试 + 预留批量重试接口 的方案,来确保数据同步的可靠性、实时性,同时保证系统稳定性。
- 由于数据平台是消费者,并且他们不太稳定,那么就不适合用MQ了吧,即使使用了很好的消息重试机制,但是也会造成我方消息的积压,所以就用异步发送同步数据+定时任务重试+超过重试5次前端页面管理重试+预留批量重试接口的机制保障数据的同时也保护我们系统的稳定性
- 和门店运营平台同步门店数据商量着做数据拆分,但是不是我跟紧的,后面就先也改成异步发送同步数据+定时任务重试+超过重试5次前端页面管理重试+预留批量重试接口的方案了,要先保证业务性,因为后面可能门店运营平台会直接接入到我们的系统。
大体实现步骤如下:
1. 异步发送同步数据
利用异步编程模型,采用 @Async
注解实现异步处理,可以避免同步操作阻塞业务逻辑,并确保数据能够尽早发送。
异步发送订单数据(pushRetailOrder
方法)
@Async
public Boolean pushRetailOrder(Long orderId) {
// 记录接口消息
IntegrateRecordLine integrateRecordLine = integrateRecordHeadService.recordApi(IntegrateConstants.PUSH_RETAIL_ORDER, orderId + "", "", IntegrateConstants.INTEGRATE_OUT, null, IntegrateConstants.STR_TWO);
// 触发计算工资
sendRetailOrder(orderId);
try {
// 获取零售订单信息
BaseResponse<RetailOrderRespVM> baseResponse = retailOrderFacade.findRetailOrder(orderId);
if (baseResponse.isSuccess() && baseResponse.getContent() != null) {
RetailOrderRespVM retailOrderRespVM = baseResponse.getContent();
if (retailOrderRespVM.getItemList().size() > 0) {
String jsonReq = FastjsonUtil.toNullAsEmpty(retailOrderRespVM);
log.info("pushRetailOrder:{}", jsonReq);
// 推送数据到目标系统
boService.sendRetailOrder(FastjsonUtil.toNullAsEmpty(Arrays.asList(retailOrderRespVM)), orderId);
}
// 更新接口记录状态为成功
integrateRecordHeadService.updateRecordMsg(integrateRecordLine.getIntegrateRecordLineId(), integrateRecordLine.getIntegrateRecordHeadId(), IntegrateConstants.STATUS_SUC, "接受到零售订单推送, 请查阅其他接口推送是否成功");
} else {
// 更新接口记录状态为失败
integrateRecordHeadService.updateRecordMsg(integrateRecordLine.getIntegrateRecordLineId(), integrateRecordLine.getIntegrateRecordHeadId(), IntegrateConstants.STATUS_FAIL, baseResponse.getChnDesc());
}
} catch (Exception e) {
// 捕获异常并更新记录状态为失败
integrateRecordHeadService.updateRecordMsg(integrateRecordLine.getIntegrateRecordLineId(), integrateRecordLine.getIntegrateRecordHeadId(), IntegrateConstants.STATUS_FAIL, e.getMessage());
}
return Boolean.TRUE;
}
关键点:
- 使用
@Async
注解异步执行pushRetailOrder
方法,避免同步调用带来的阻塞。 - 通过
retailOrderFacade.findRetailOrder(orderId)
获取订单信息,并通过boService.sendRetailOrder
进行数据推送。 - 使用
integrateRecordHeadService
记录推送状态,确保推送成功后能更新记录状态,便于后续追踪。
2. 定时任务重试
为了处理因目标平台不稳定导致的消息发送失败,方案引入定时任务来进行消息重试。假设消费平台暂时不可用,系统通过定时任务进行消息重试,确保系统的高可靠性。
@Slf4j
@Component
public class RetailPushJob extends AbstractJob {
@Autowired
private ZookeeperRegistryCenter zookeeperRegistryCenter;
@Autowired
private RetailOrderService retailOrderService;
public InsurancePushJob(
@Value("${elasticJob.retailPushJob.cron}") String cron,
@Value("${elasticJob.retailPushJob.shardingTotalCount}") Integer shardingTotalCount,
@Value("${elasticJob.retailPushJob.shardingItemParameters}") String shardingItemParameters,
@Value("${elasticJob.retailPushJob.jobParameter}") String jobParameter) {
super(cron, shardingTotalCount, shardingItemParameters, jobParameter);
}
@PostConstruct
public void init() {
super.start(zookeeperRegistryCenter);
}
@Override
public void executeJob(Integer shardingTotalCount, Integer shardingItem, String itemParameter, String jobParameter) {
RetailOrderSearchDTO request=new RetailOrderSearchDTO();
request.setUpdateDate(LocalDate.now());
retailOrderService.rePushRetailOrder(request);
}
}
3. 前端管理重试
如果消息推送在系统中多次失败,可以通过前端页面提供管理界面,让用户主动进行重试操作。
前端可以提供一个 “重试” 按钮,用户点击后调用后台的重试接口。后台通过 retryFailedOrders
或专门的接口重新发送失败的消息。
@RequestMapping("/retryOrder")
public String retryFailedOrder(@RequestParam Long orderId) {
IntegrateRecordLine recordLine = integrateRecordHeadService.getRecordByOrderId(orderId);
if (recordLine != null && recordLine.getStatus().equals(IntegrateConstants.STATUS_FAIL)) {
try {
// 尝试重新推送消息
pushRetailOrder(orderId);
return "重试成功";
} catch (Exception e) {
return "重试失败:" + e.getMessage();
}
}
return "没有找到失败的订单记录";
}
关键点:
- 通过前端提供重试功能,用户可以主动触发失败记录的消息重试。
- 重试结果会直接返回到前端,用户可以根据返回的消息进行相应的操作。
4. 预留批量重试接口
为了提升效率,在某些情况下可能需要批量处理消息。可以提供批量重试接口,允许一次性重试多个失败的订单。
@RequestMapping("/batchRetryOrders")
public String batchRetryOrders(@RequestParam List<Long> orderIds) {
for (Long orderId : orderIds) {
try {
// 重新推送每个失败的订单
pushRetailOrder(orderId);
} catch (Exception e) {
return "批量重试失败,订单ID: " + orderId + ",错误:" + e.getMessage();
}
}
return "批量重试成功";
}
关键点:
- 允许批量传入订单ID,后台一次性尝试重试多个失败订单。
- 处理每个订单的推送失败时返回错误信息,保证批量操作的灵活性。
总结
通过 异步发送同步数据、定时任务重试、前端页面管理重试 和 预留批量重试接口,我们能够确保门店与订单数据同步过程的实时性与准确性。具体来说:
- 异步发送订单数据,确保不会阻塞系统的主流程。
- 通过定时任务进行消息的重试,处理消费者平台的不稳定。
- 提供前端重试功能和批量重试接口,提升系统的可靠性与用户的灵活性。
这种方案保证了数据同步的高可靠性,并且避免了消息积压,同时能够有效应对消费者系统的不稳定性。
SAP基础信息解决方案:并发消费+消费重试5次+定时任务重试+超过重试5次前端页面管理重试+预留批量重试接口
为了保障从 SAP基础信息平台 消费消息的实时性与准确性,涵盖消息消费的可靠性、实时性、准确性以及异常处理。
配置 RocketMQ 的相关参数,确保消费者能够正确连接到 RocketMQ 服务,并且开启消息跟踪功能:通过记录和追踪每条消息的流转过程,帮助开发人员分析消息的生命周期、状态变更及故障排查。
spring:
cloud:
alibaba:
rocketmq:
name-server: xxx:9876 # RocketMQ NameServer 地址
consumer:
group: SAP_CONSUMER_GROUP
# 确保消息的持久化和可靠性
enable-msg-trace: true
max-reconsume-times: 5 # 最大重试次数
retry-times-when-consume-failed: 5 # 重试次数
producer:
group: SAP_PRODUCER_GROUP
定义一个消费监听器,消费来自 SAP 基础信息平台的消息。
@Component
@MessageListener(consumerGroup = "SAP_CONSUMER_GROUP", topic = "SAP_TOPIC", selectorExpression = "*", consumeMode = ConsumeMode.ORDERLY)
public class SAPMessageListener implements RocketMQListener<MessageExt> {
// 订单同步的最大重试次数
private static final int MAX_RETRY_TIMES = 5;
@Override
@Transactional
public void onMessage(MessageExt message) {
String messageBody = new String(message.getBody());
JSONObject messageJson = JSONObject.parseObject(messageBody);
// 处理消息
processSAPMessage(messageJson);
// 如果消费失败,重试逻辑
boolean success = false;
int retryCount = 0;
while (!success && retryCount < MAX_RETRY_TIMES) {
try {
// 数据同步逻辑同步至数据库
syncToDatabase(messageJson);
success = true; // 同步成功
} catch (Exception e) {
retryCount++;
if (retryCount == MAX_RETRY_TIMES) {
// 达到最大重试次数,记录失败并发送到死信队列
sendToDeadLetterQueue(message);
}
}
}
}
private void processSAPMessage(JSONObject messageJson) {
// 根据业务需求处理消息
// 比如,解析消息中的 SAP 订单信息、档案数据等
System.out.println("处理 SAP 消息: " + messageJson);
}
private void syncToDatabase(JSONObject messageJson) {
// 将消息数据同步到数据库
// 在此执行同步操作,假设是同步订单数据到本地数据库
// 例如:orderRepository.save(order);
System.out.println("将数据同步到数据库: " + messageJson);
}
private void sendToDeadLetterQueue(MessageExt message) {
// 将无法消费的消息发送到死信队列进行人工处理
System.out.println("消息消费失败,发送到死信队列: " + message);
// 实现消息发送到死信队列的逻辑
}
}
通过配置 RocketMQ 消费者的并发消费模式,可以提高消息消费的吞吐量。
spring:
cloud:
alibaba:
rocketmq:
consumer:
consumeMode: ORDERLY # 顺序消费
# 配置并发消费
max-thread: 16
如果消息量较大,可以使用批量消费的方式来提高性能。RocketMQ 支持批量消费,可以减少消费频率,提高系统吞吐量。
@MessageListener(consumerGroup = "SAP_CONSUMER_GROUP", topic = "SAP_TOPIC", consumeMode = ConsumeMode.BROADCASTING)
public class BatchSAPMessageListener implements RocketMQListener<List<MessageExt>> {
@Override
public void onMessage(List<MessageExt> messages) {
// 批量处理消息
for (MessageExt message : messages) {
processSAPMessage(message);
}
}
}