分布式微服务系统架构第94集:Kafka 消费监听处理类,redisson延时队列

加群联系作者vx:xiaoda0423

仓库地址:webvueblog.github.io/JavaPlusDoc...

1024bat.cn/

typescript 复制代码
public void handleAccessEvent(ConsumerRecord<?, ?> recordMsg, Acknowledgment ack) {
        long start = System.currentTimeMillis();
        List<ConsumerRecord<?, ?>> records = Collections.singletonList(recordMsg);
        String timeStr = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
        handleBiz(records, start, timeStr);
        ack.acknowledge(); // 手动提交偏移量,确保消息处理完毕
        log.info("==>设备数据-柜子事件处理耗时: {}ms", System.currentTimeMillis() - start);
    }
    
    public void handleBiz(List<ConsumerRecord<?, ?>> records, long countTime, String timeStr) {
        for (ConsumerRecord<?, ?> record : records) {
            try {
                String value = record.value().toString();
                
优化点 说明
幂等性处理 若多次消费同一订单,确保不会重复写入(订单状态比对)
异步执行更新 bizOrderService.setRenewEndOrder() 可使用线程池异步提高性能
延迟队列支持 若逾期处理需延迟精确执行,可用 Kafka 延迟消息或 Redis 延时任务
Prometheus监控指标 上报处理成功数、失败数、处理耗时等,用于高可用监控
消费线程隔离 可将不同事件类型拆分不同消费者容器,提升隔离性与扩展性
ini 复制代码
/**
 * Kafka 消费监听 - 处理订单逾期事件
 */
@KafkaListener(
    topics = KafkaConstant.ORDER_OVERDUE_TOPIC,
    containerFactory = "bizBRConsumerFactory",
    properties = {"max.poll.records = 100"} // 每次最多拉取 100 条,提升吞吐量
)
public void handle(List<ConsumerRecord<String, String>> records, Acknowledgment ack) {
    if (CollectionUtils.isEmpty(records)) {
        return;
    }

    String dateTime = LocalDateTime.now().format(inputFormatter); // 当前时间格式化
    List<String> failedOrders = new ArrayList<>(); // 记录失败订单 ID 以便后续排查

    for (ConsumerRecord<String, String> record : records) {
        // 空数据直接跳过
        if (StringUtils.isBlank(record.value())) {
            continue;
        }

        try {
            log.info("[Kafka-订单逾期] 接收到原始消息:{}", record.value());

            // 反序列化消息为事件对象
            OrderOverdueEvent overdueEvent = GsonUtils.getObjectFromJson(record.value(), OrderOverdueEvent.class);
            if (overdueEvent == null || StringUtils.isBlank(overdueEvent.getOrderId())) {
                log.warn("[Kafka-订单逾期] 消息格式异常:{}", record.value());
                continue;
            }

            BExchSvcOrder order = bExchSvcOrderService.selectById(overdueEvent.getOrderId());
            if (!updatePreCheck(order)) {
                continue;
            }

            // 标记逾期延迟(内部逻辑需要)
            order.setOverdueDelay(true);

            // 首先尝试通过续费接口完结
            BExchSvcOrder result = bizOrderService.setRenewEndOrder(order, dateTime);
            if (result == null) {
                // 若续费接口返回为空,说明未续费成功,则直接设置为逾期状态
                log.info("[Kafka-订单逾期] 订单逾期,未续费完结,准备状态更新,订单:{}", JacksonUtils.toJson(order));

                BExchSvcOrderBO updateBO = BExchSvcOrderBO.builder()
                        .orderId(order.getOrderId())
                        .oStatus(OrderConstant.OVERDUE) // 设置订单状态为 OVERDUE
                        .overdueDelay(true)
                        .updType(OrderConstant.CHANGEBAT)
                        .build();

                // 更新数据库状态
                bExchSvcOrderService.update(updateBO);

            }

        } catch (Exception ex) {
            // 单条处理失败,继续处理下一条,不影响整体消费
            String failedId = record.value();
            log.error("[Kafka-订单逾期] 处理失败,订单原始内容:{}", failedId, ex);
            failedOrders.add(failedId);
        }
    }

    // 所有消息处理完成后统一提交 offset
    ack.acknowledge();

    if (!failedOrders.isEmpty()) {
        log.warn("[Kafka-订单逾期] 以下订单处理失败需排查:{}", failedOrders);
    }
}
  1. 配置优化

    properties

    ini 复制代码
    # 消费者配置
    spring.kafka.consumer.max-poll-records=500
    spring.kafka.listener.concurrency=3
    spring.kafka.listener.poll-timeout=5000
    
    # 线程池配置
    io.executor.corePoolSize=10
    io.executor.maxPoolSize=20
    io.executor.queueCapacity=1000
  2. 监控增强

    • 添加Metrics指标(处理速率、耗时分布等)
    • 实现Circuit Breaker模式(如Resilience4j)
    • 关键操作添加TraceID实现全链路追踪
  3. 数据一致性

    • 实现幂等处理(通过eventId去重)
    • 使用事务消息保证最终一致性
    • 添加死信队列处理无法消费的消息
  4. 压力测试

    • 使用JMeter进行不同批量大小的性能测试
    • 调整线程池参数找到最优配置
    • 测试故障恢复能力(如ES宕机时的重试机制)
  • ✅ 使用线程池替代单线程消费,提高并发能力;

  • ✅ 增强可配置性和灵活性(比如线程数量、阻塞时间);

  • ✅ 异常处理更加细粒度;

  • ✅ 避免重复初始化;

  • ✅ 增强日志和可追踪性;

kotlin 复制代码
/**
 * 抽象延迟消息处理器,支持 Redisson 延迟队列消费
 * 适用于高并发、可扩展的异步延迟消息处理场景
 */
public abstract class xxx<T> implements xxx<T> {

    private static final Logger log = LoggerFactory.getLogger(AbstractDelayMessageHandler.class);

    private static final int DEFAULT_POLL_SLEEP_TIME = 5000; // 默认阻塞队列轮询等待时间(毫秒)

    @Autowired
    private RedissonClient redissonClient; // 注入 Redisson 客户端,用于操作延迟队列

    private RBlockingDeque<T> blockingDeque; // Redisson 阻塞队列
    private RDelayedQueue<T> delayedQueue;   // Redisson 延迟队列
    
    // 消费标志位,控制线程是否运行
    // 是否启用消费者,配置优先级高
    
    @Value("${redisson.delay.consumer.enable:false}")
    protected boolean enable; // 是否启用消费者,配置优先级高

    @Value("${redisson.delay.consumer.threads:2}")
    private int consumerThreads; // 消费者线程数,默认2个,可通过配置覆盖

    @Value("${redisson.delay.consumer.pollTimeout:5000}")
    private long pollTimeoutMs; // 轮询延迟消息的超时时间,默认5000ms

    private ExecutorService consumerExecutor; // 消费线程池
    
    // 阻塞拉取延迟消息(支持设置超时时间)
    message = blockingDeque.poll(pollTimeoutMs, TimeUnit.MILLISECONDS);

    if (Objects.isNull(message)) {
        continue;
    }

    // 过滤空消息
    if ((message instanceof String) && StringUtils.isBlank((String) message)) {
        continue;
    }

    log.info("消费延迟队列:{},接收消息:{}", queueName(), message);

    // 处理延迟消息(交由子类实现)
    this.handle(message);
    
    
    
   

✅ 延迟场景 1:优惠券即将过期提醒(提前1小时提醒)

实体类 CouponExpireReminderMessage.java

less 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CouponExpireReminderMessage implements Serializable {
    private static final long serialVersionUID = 1L;

    private String userId;
    private String couponId;
    private String couponName;
    private String expireTime; // 格式:yyyy-MM-dd HH:mm:ss
}

延迟队列处理器 CouponExpireReminderHandler.java

scala 复制代码
@Component
public class CouponExpireReminderHandler extends AbstractDelayMessageHandler<CouponExpireReminderMessage> {

    private static final Logger log = LoggerFactory.getLogger(CouponExpireReminderHandler.class);

    @Override
    protected String queueName() {
        return "coupon:expire:remind:delay:queue";
    }

    @Override
    protected void handle(CouponExpireReminderMessage message) {
        try {
            log.info("【优惠券过期提醒】用户:{},券:{}({}),过期时间:{}",
                    message.getUserId(), message.getCouponId(), message.getCouponName(), message.getExpireTime());

            // 示例:通过站内信或推送系统通知用户
            // notificationService.sendExpireReminder(message.getUserId(), message.getCouponName());

        } catch (Exception e) {
            log.error("处理优惠券过期提醒异常:{}", message, e);
        }
    }
}

加入延迟队列

java 复制代码
CouponExpireReminderMessage msg = new CouponExpireReminderMessage(
    "user123", "coupon456", "满50减20", "2025-04-10 23:59:59"
);
redissonClient.getDelayedQueue(redissonClient.getBlockingDeque("coupon:expire:remind:delay:queue", new JsonJacksonCodec()))
    .offer(msg, 1, TimeUnit.HOURS); // 提前一小时提醒

✅ 延迟场景 2:用户未读通知提醒(24小时后仍未读则再次提醒)

实体类 UnreadNotificationReminderMessage.java

less 复制代码
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UnreadNotificationReminderMessage implements Serializable {
    private static final long serialVersionUID = 1L;

    private String userId;
    private String noticeId;
    private String title;
}

处理器类 UnreadNotificationReminderHandler.java

scala 复制代码
@Component
public class UnreadNotificationReminderHandler extends AbstractDelayMessageHandler<UnreadNotificationReminderMessage> {

    private static final Logger log = LoggerFactory.getLogger(UnreadNotificationReminderHandler.class);

    @Override
    protected String queueName() {
        return "notice:unread:remind:delay:queue";
    }

    @Override
    protected void handle(UnreadNotificationReminderMessage message) {
        try {
            // 模拟判断是否未读(例如通过DB检查状态)
            // boolean unread = noticeService.isUnread(message.getUserId(), message.getNoticeId());
            boolean unread = true; // 模拟

            if (unread) {
                log.info("【未读通知提醒】用户:{},通知标题:{}", message.getUserId(), message.getTitle());
                // 推送提醒
                // pushService.sendUnreadNotification(message.getUserId(), message.getTitle());
            }

        } catch (Exception e) {
            log.error("未读通知提醒处理失败:{}", message, e);
        }
    }
}
高可用
Redisson 自带重连机制;建议集群部署支持备份

Redisson / RabbitMQ / Kafka 的延迟队列功能

无需关心底层使用哪种消息系统

RabbitMQ 延迟队列实现

RabbitMQ 本身不支持真正的延迟队列,但可使用插件或 TTL + DLX(死信交换机) 实现

(使用延迟插件)

typescript 复制代码
@Bean
public CustomExchange delayExchange() {
    Map<String, Object> args = new HashMap<>();
    args.put("x-delayed-type", "direct");
    return new CustomExchange("delay.exchange", "x-delayed-message", true, false, args);
}

实现类

多队列统一管理调度

支持多个不同延迟任务(如:订单取消、支付过期、短信发送等)统一注册和调度,提高可扩展性。

结构如下

arduino 复制代码
com.xxx.delay
├── AbstractDelayMessageHandler<T>      // 抽象类:每种业务继承它
├── DelayQueueManager                   // 统一注册&调度
├── ThreadPoolExecutorFactory           // 统一线程池管理
├── impl
│   └── OrderCancelDelayHandler         // 实现类:订单取消
├── model
│   └── OrderCancelMessage              // 消息体

功能清单

功能 说明
🔁 失败重试机制 支持自定义最大重试次数和退避重试
消费超时检测 如果处理时间超出阈值,记录慢消费日志
📊 Prometheus监控支持 可用于 Grafana 展示消费速率/失败率/重试次数等

失败重试机制(自动重新入队)

🎯 实现点

  • 每条消息增加 retryCount 字段
  • 重试时延迟投递到 Redisson 延迟队列
  • 超过最大次数后记录错误日志,不再消费
相关推荐
PPIO派欧云10 分钟前
PPIO X OWL:一键开启任务自动化的高效革命
运维·人工智能·自动化·github·api·教程·ppio派欧云
考虑考虑1 小时前
go使用gorilla/websocket实现websocket
后端·程序员·go
一纸忘忧1 小时前
成立一周年!开源的本土化中文文档知识库
前端·javascript·github
李少兄1 小时前
解决Spring Boot多模块自动配置失效问题
java·spring boot·后端
Piper蛋窝2 小时前
Go 1.19 相比 Go 1.18 有哪些值得注意的改动?
后端
码农BookSea2 小时前
不用Mockito写单元测试?你可能在浪费一半时间
后端·单元测试
帽儿山的枪手2 小时前
socket套接字你搞清楚了吗
网络协议·面试
吾日三省吾码2 小时前
GitHub Copilot (Gen-AI) 很有用,但不是很好
人工智能·github·copilot
codingandsleeping3 小时前
Express入门
javascript·后端·node.js
ss2733 小时前
基于Springboot + vue + 爬虫实现的高考志愿智能推荐系统
spring boot·后端·高考