定时任务+rocketmq实现数据同步与消息重发

一、业务层代码

public class CrmBindCustomerPushService {
    private static final String BIZ_TYPE = "bind";
    private static final String PRIVATE_KEY = "MIIEvQIBADANBgkqhkiG9w0BAQEFAA...."

    @Resource
    private CrmSyncLogMapper syncLogMapper;
    @Resource
    private CrmCustomerBindMapper bindMapper;
    @Resource
    private MessageRetryHandler messageRetryHandler;

    public String syncBindPush() throws Exception {
        // 1️ 获取同步记录
        CrmSyncLogDO logRecord = syncLogMapper.selectByBizType(BIZ_TYPE);
        if (logRecord == null) {
            log.warn("未找到同步记录,将初始化日志记录。");
            syncLogMapper.insertInit(BIZ_TYPE);
            logRecord = syncLogMapper.selectByBizType(BIZ_TYPE);
        }

        // 2️ 处理首次同步(lastSyncTime 为空的情况)
        LocalDateTime lastSyncTime = logRecord.getLastSyncTime();
        if (lastSyncTime == null) {
            lastSyncTime = LocalDateTime.of(2025, 11, 3, 9, 50, 50);
            log.info("首次执行增量同步,将从 {} 开始查询。", lastSyncTime);
        }

        // 3 查询增量数据
        List<CrmBindCustomerPush> dataList = posBindMapper.selectIncrementalData(lastSyncTime);
        if (dataList.isEmpty()) {
            log.info("没有需要推送的增量数据。");
            return "无增量数据同步";
        }

        log.info("开始增量推送,共 {} 条记录(起点时间:{})", dataList.size(), lastSyncTime);

        // 4️ 推送
        boolean result = pushIncrementalWithSign(dataList);

        // 5️ 更新同步日志
        CrmSyncLogDO update = new CrmSyncLogDO();
        update.setBizType(BIZ_TYPE);
        update.setLastSyncTime(LocalDateTime.now());
        update.setStatus(result ? "SUCCESS" : "FAIL");
        update.setRemark("增量推送");
        update.setRetryCount(logRecord.getRetryCount() + 1);
        syncLogMapper.updateStatus(update);

        log.info("增量推送结束,结果:{}", result ? "成功" : "失败");
        return "同步结果:"+ result;
    }

    /**
     * 带签名的增量推送逻辑(每条单独推送)
     */
    private boolean pushIncrementalWithSign(List<CrmBindCustomerPush> dataList) throws Exception {
        String url = "http://localhost:8080/api/getBindData";
        boolean allSuccess = true;

        for (CrmBindCustomerPush item : dataList) {
            String json = JSON.toJSONString(item);
            String sign = RsaSignVerifyDemo.sign(json, PRIVATE_KEY);

            Map<String, Object> payload = new HashMap<>();
            payload.put("data", item);
            payload.put("sign", sign);

            String jsonStr = JSON.toJSONString(payload);

            try {
                String response = HttpUtil.post(url, jsonStr);
                log.info("机构响应: {}", response);
                if (!response.contains("\"ret_code\":\"00\"")) {
                    log.warn("响应不正确,发送到重试队列: {}", json);
                    // 使用注入的 messageRetryHandler 添加待重试消息
                    String messageId = UUID.randomUUID().toString();
                    messageRetryHandler.addPendingMessage(messageId, url, jsonStr, item,"CRM_BIND_PUSH_RETRY_TOPIC");
                    allSuccess = false;
                }
            } catch (Exception e) {
                log.warn("推送异常,发送到重试队列: {}", json, e);
                // 异常情况也使用 messageRetryHandler 添加待重试消息
                String messageId = UUID.randomUUID().toString();
                messageRetryHandler.addPendingMessage(messageId, url, jsonStr, item);
                allSuccess = false;
            }
        }

        return allSuccess;
    }

}

二、消息重试处理器

@Component
@Slf4j
public class MessageRetryHandler {
    
    // 重发间隔配置(秒):1s/10s/1m/3m/5m/10m/20m/30m/1h/2h
    private static final int[] RETRY_INTERVALS = {1, 10, 60, 180, 300, 600, 1200, 1800, 3600, 7200};
    private static final int MAX_RETRY = 10;
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private CrmPushFailedMessageMapper crmPushFailedMessageMapper;
    
    /**
     * 处理消息重试
     */
    public boolean handleRetry(RetryMessage retryMsg) {
        try {
            String response = HttpUtil.post(retryMsg.getUrl(), retryMsg.getPayload());
            if (isValidResponse(response)) {
                log.info("消息重发成功,消息ID: {}", retryMsg.getMessageId());
                return true;
            }
        } catch (Exception e) {
            log.warn("消息重发失败,消息ID: {}, 重试次数: {}", 
                    retryMsg.getMessageId(), retryMsg.getRetryCount(), e);
        }
        
        retryMsg.incrementRetryCount();
        if (retryMsg.getRetryCount() >= MAX_RETRY) {
            handleMaxRetriesExceeded(retryMsg);
            return true;
        }
        
        // 发送到 RocketMQ 延迟队列进行重试
        // 发送到延迟队列,使用消息中指定的topic
        try {
            rocketMQTemplate.syncSend(retryMsg.getTopic(),
                    MessageBuilder.withPayload(JSON.toJSONString(retryMsg)).build(),
                    3000,
                    getDelayLevel(retryMsg.getRetryCount()));
            log.info("消息发送到延迟队列,消息ID: {}, Topic: {}, 延迟级别: {}", retryMsg.getMessageId(), retryMsg.getTopic(), getDelayLevel(retryMsg.getRetryCount()));
        } catch (Exception e) {
            log.error("发送消息到延迟队列失败,消息ID: {}, Topic: {}", retryMsg.getMessageId(), retryMsg.getTopic(), e);
            // 如果发送到MQ失败,记录错误但继续重试
        }
        return false;
    }
    
    /**
     * 根据重试次数获取 RocketMQ 延迟级别1s/10s/1m/3m/5m/10m/20m/30m/1h/2h
     * RocketMQ 延迟级别: 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
     */
    private int getDelayLevel(int retryCount) {
        // 映射到 RocketMQ 的延迟级别,对应 1s/10s/1m/3m/5m/10m/20m/30m/1h/2h
        switch (retryCount) {
            case 0: return 1;   // 1s
            case 1: return 3;   // 10s
            case 2: return 5;   // 1m
            case 3: return 7;   // 3m
            case 4: return 9;   // 5m
            case 5: return 14;  // 10m
            case 6: return 15;  // 20m
            case 7: return 16;  // 30m
            case 8: return 17;  // 1h
            default: return 18; // 2h (默认最大)
        }
    }
    
    /**
     * 验证响应是否有效
     */
    private boolean isValidResponse(String response) {
        return response != null && response.contains("\"ret_code\":\"00\"");
    }
    
    /**
     * 处理达到最大重试次数的消息
     */
    private void handleMaxRetriesExceeded(RetryMessage retryMsg) {
        log.error("消息重发达到最大次数{},停止重试。消息ID: {}, 最终状态: 失败",
                MAX_RETRY, retryMsg.getMessageId());

        // 保存到数据库
        try {
            CrmPushFailedMessageDO failedMessage = new CrmPushFailedMessageDO();
            failedMessage.setMessageId(retryMsg.getMessageId());
            failedMessage.setUrl(retryMsg.getUrl());
            failedMessage.setPayload(retryMsg.getPayload());
            failedMessage.setOriginalData(JSON.toJSONString(retryMsg.getOriginalData()));
            failedMessage.setRetryCount(retryMsg.getRetryCount());
            failedMessage.setFailReason("达到最大重试次数");

            crmPushFailedMessageMapper.insert(failedMessage);
            log.info("失败消息已记录到数据库,消息ID: {}", retryMsg.getMessageId());
        } catch (Exception e) {
            log.error("记录失败消息到数据库失败,消息ID: {}", retryMsg.getMessageId(), e);
        }
    }

    /**
     * 添加待重试消息(指定topic)并立即发送到延迟队列
     */
    public void addPendingMessage(String messageId, String url, String payload, Object originalData, String topic) {
        RetryMessage retryMsg = new RetryMessage(messageId, url, payload, 0, 
                LocalDateTime.now().plusSeconds(RETRY_INTERVALS[0]), originalData, topic);
        
        // 直接发送到 RocketMQ 延迟队列,不保存在本地内存中
        try {
            rocketMQTemplate.syncSend(topic,
                    MessageBuilder.withPayload(JSON.toJSONString(retryMsg)).build(),
                    3000,
                    getDelayLevel(0)); // 第一次重试使用级别1(1s)
            log.info("======未正确响应=====消息首次发送到延迟队列,消息ID: {}, Topic: {}, 延迟级别: {}", messageId, topic, getDelayLevel(0));
        } catch (Exception e) {
            log.error("======未正确响应=====首次发送消息到延迟队列失败,消息ID: {}, Topic: {}", messageId, topic, e);
        }
        log.info("添加待重试消息,消息ID: {}, Topic: {}", messageId, topic);
    }

    /**
     * 添加待重试消息(使用默认topic)
     */
    public void addPendingMessage(String messageId, String url, String payload, Object originalData) {
        addPendingMessage(messageId, url, payload, originalData, "CRM_PUSH_EXCEPTION_RETRY_TOPIC");
    }
}

三、消费者处理

@Component
@RocketMQMessageListener(topic = "CRM_BIND_PUSH_RETRY_TOPIC", consumerGroup = "crm-bind-retry-consumer-group")
@Slf4j
public class CrmBindRetryConsumer implements RocketMQListener<String> {

    @Autowired
    private MessageRetryHandler messageRetryHandler;

    @Override
    public void onMessage(String message) {
        try {
            RetryMessage retryMsg = JSON.parseObject(message, RetryMessage.class);
            // 使用统一的重试处理器处理消息
            messageRetryHandler.handleRetry(retryMsg);
        } catch (Exception e) {
            log.error("处理绑定消息失败: {}", message, e);
        }
    }
}

四、定时任务推送

@Component
@Slf4j
public class CrmBindCustomerJob implements JobHandler {

    @Resource
    private CrmBindCustomerPushService crmBindCustomerPushService;

    @Override
    @TenantJob
    public String execute(String param) throws Exception {
        log.info("开始执行绑定数据推送定时任务");
        try {
            String result = crmPosBindCustomerPushService.syncPosBindPush();
            log.info("绑定数据推送定时任务执行完成,结果: {}", result);
            return result;
        } catch (Exception e) {
            log.error("绑定数据推送定时任务执行失败", e);
            throw e;
        }
    }
}

五、相关日志记录类

@TableName("crm_push_failed_message")
@Data
@EqualsAndHashCode(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CrmPushFailedMessageDO extends BaseDO {
    
    /**
     * 主键
     */
    private Long id;
    
    /**
     * 消息ID
     */
    private String messageId;
    
    /**
     * 推送URL
     */
    private String url;
    
    /**
     * 推送载荷
     */
    private String payload;
    
    /**
     * 原始数据
     */
    private String originalData;
    
    /**
     * 重试次数
     */
    private Integer retryCount;
    
    /**
     * 失败原因
     */
    private String failReason;
}
@Data
public class CrmSyncLogDO {
    private Long id;
    private String bizType;
    private LocalDateTime lastSyncTime;
    private Integer retryCount;
    private String status;
    private String remark;
    private LocalDateTime createdTime;
    private LocalDateTime updatedTime;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RetryMessage {
    private String messageId;
    private String url;
    private String payload;
    private int retryCount = 0;
    private LocalDateTime nextRetryTime;
    private Object originalData;
    private String topic;
    
    public void incrementRetryCount() {
        this.retryCount++;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值