分布式微服务系统架构第119集:WebSocket监控服务内部原理和执行流程

加群联系作者vx:xiaoda0423

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

1024bat.cn/

👉 Kafka Producer 端(生产者)配置

👉 Kafka Consumer 端(消费者)配置

区分了不同的消费模式 (批量消费、单条消费),并且生产者也是分别独立管理。

🛠️ 流程步骤概览

阶段 内容
配置阶段 通过 @Configuration 注解注册生产者、消费者配置。
属性注入 通过 @Value 注解注入 application.ymlapplication.properties 中的 Kafka 配置。
Bean 定义 生产者定义 KafkaTemplate,消费者定义 KafkaListenerContainerFactory(分批量和单条消费两种)。
使用阶段 在业务代码中注入对应的 KafkaTemplate 发送消息,注解 @KafkaListener 配合不同的 ContainerFactory 接收消息。

📈 数据结构变化过程

阶段 数据结构 变化说明
生产者发送前 Java对象/String Java对象通常先转为 String(通过序列化器)
传输到 Kafka byte[] Kafka 内部只认识字节数组
消费者接收 byte[] → String 消费者接收到字节流,反序列化回 String
消费逻辑处理 Java对象/String 再根据业务场景反序列化成 Java 对象处理

📚 示例操作流程(完整链路)

1. 启动阶段

  • Spring Boot 扫描到 @Configuration,注册 KafkaTemplate 和 KafkaListenerContainerFactory。

2. 发送消息(生产者)

less 复制代码
@Autowired
@Qualifier("kafkaTemplate")
private KafkaTemplate<String, String> kafkaTemplate;

public void sendMessage(String topic, String message) {
    kafkaTemplate.send(topic, message);
}

3. 消费消息(消费者)

typescript 复制代码
@KafkaListener(topics = "test-topic", containerFactory = "kafkaBRConsumerFactory")
public void listenBatch(List<ConsumerRecord<String, String>> records) {
    for (ConsumerRecord<String, String> record : records) {
        System.out.println("Received message: " + record.value());
    }
}

// 或者单条消费
@KafkaListener(topics = "test-topic", containerFactory = "kafkaOBORConsumerFactory")
public void listenSingle(ConsumerRecord<String, String> record) {
    System.out.println("Received single message: " + record.value());
}
  • 生产者和消费者配置完全隔离,职责清晰。
  • 支持批量消费 (性能高)和逐条消费(处理更精细)两种模式。
  • KafkaTemplate 统一封装消息发送,内部使用的是 ProducerFactory
  • KafkaListenerContainerFactory 统一封装消息监听,内部使用的是 ConsumerFactory
  • 生产消费流程:Java对象 → Kafka字节流传输 → Java对象。

📄 1. application.yml Kafka配置模板

yaml 复制代码
kafka:
  producer:
    servers: 127.0.0.1:9092
    retries: 3              # 发送失败重试次数
    batch:
      size: 16384           # 批量发送最大字节数
    linger: 1               # 等待时间(ms)
    buffer:
      memory: 33554432      # 32MB缓冲区大小
  consumer:
    servers: 127.0.0.1:9092
    enable:
      auto:
        commit: false       # 是否自动提交offset
    session:
      timeout: 30000        # session超时时间
    auto:
      commit:
        interval: 1000      # 自动提交offset时间间隔
    group:
      id: my-consumer-group
    auto:
      offset:
        reset: latest       # 最新位置消费(可选 earliest)
    concurrency: 3          # 并发线程数

注意

  • producer.serversconsumer.servers 通常是一样的(Kafka 集群地址)。

  • auto.offset.reset

    • latest:只消费启动后新的消息
    • earliest:从最早可用的消息开始消费(适合首次启动的消费者)

🚀 2. 消费者端:加上消息重试机制

在你的消费者容器工厂上,加个 SeekToCurrentErrorHandler,遇到异常能自动重试N次(否则Kafka默认抛出异常整个消费线程就挂掉了!)

dart 复制代码
private KafkaListenerContainerFactory<?> getConsumerFactory(boolean batchListener) {
    ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setConcurrency(concurrency);
    factory.setBatchListener(batchListener);

    // 消息处理异常时重试配置(重要)
    factory.setErrorHandler(new SeekToCurrentErrorHandler(
            new FixedBackOff(5000L, 3) // 5秒重试一次,总共重试3次
    ));

    return factory;
}

细节说明

  • SeekToCurrentErrorHandler 会把这条消息重新投到消费队列中,不会丢失。
  • FixedBackOff(5000L, 3):表示每隔5秒重试一次重试3次后如果还失败就记录error日志。

如果你想更极致一点,比如:重试 N 次还失败就发到 死信队列(DLQ) ,也可以配!

⚡ 3. 生产者端:Kafka发送消息异步带回调

别直接 kafkaTemplate.send()完就不管了,加一个Callback回调,拿到真正的发送成功 or 失败,做链路监控。

补充一下发送方法:

less 复制代码
@Autowired
@Qualifier("kafkaTemplate")
private KafkaTemplate<String, String> kafkaTemplate;

public void sendMessageWithCallback(String topic, String message) {
    kafkaTemplate.send(topic, message).addCallback(
        success -> {
            if (success != null) {
                System.out.println("发送成功!topic:" + success.getRecordMetadata().topic() + 
                                   " partition:" + success.getRecordMetadata().partition() + 
                                   " offset:" + success.getRecordMetadata().offset());
            }
        },
        failure -> {
            System.err.println("发送失败!原因:" + failure.getMessage());
        }
    );
}

说明

  • success 可以拿到:topic、partition、offset,链路追踪好用!
  • failure 可以拿到异常信息,比如超时、网络问题、队列满等。

✅ 总结(目前你的Kafka模块)

组件 细节
Producer 配置 ProducerFactory、KafkaTemplate,支持异步Callback
Consumer 配置 ConsumerFactory、ListenerContainerFactory,支持批量、逐条消费、异常重试
配置文件 application.yml统一管理
错误处理 加了 SeekToCurrentErrorHandler 自动重试
发送保障 生产者发送加回调,失败报警

🚀 你可以直接拿来实战用,真正可以扛流量的 Kafka 消费生产链路!

🧩 1. 死信队列处理机制整体流程

✅ 正常消费 →

❌ 消费失败 + 重试多次 →

🔁 还是失败 →

➡️ 将原消息投递到【死信Topic】(Dead Letter Topic)

➡️ 后续人工修复 or 自动补偿

🛠️ 2. 代码实现分三步

(1)修改你的 getConsumerFactory 方法,加 DeadLetterPublishingRecoverer

我们要替换掉原来的 SeekToCurrentErrorHandler,换成支持死信队列的异常处理器:

less 复制代码
@Autowired
@Qualifier("kafkaTemplate") // 用来发到死信topic
private KafkaTemplate<String, String> kafkaTemplate;

private KafkaListenerContainerFactory<?> getConsumerFactory(boolean batchListener) {
    ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(consumerFactory());
    factory.setConcurrency(concurrency);
    factory.setBatchListener(batchListener);

    // 配置死信队列处理器
    DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(kafkaTemplate,
        (r, e) -> new TopicPartition(r.topic() + ".DLT", r.partition()) 
        // 失败后投递到原Topic后缀加.DLT(Dead Letter Topic)
    );

    // 失败后重试3次,每次间隔5秒,重试完仍失败则交给recoverer处理
    factory.setErrorHandler(new SeekToCurrentErrorHandler(recoverer, new FixedBackOff(5000L, 3)));

    return factory;
}

🔵 解释一下:

  • DeadLetterPublishingRecoverer:失败后自动把消息重新生产到 .DLT 结尾的新Topic,比如消费 order-event 失败了,就投到 order-event.DLT
  • FixedBackOff(5000, 3):5秒重试1次,总共重试3次
  • 如果3次都失败,就触发 recoverer,把消息扔到死信队列里。

(2)application.yml 配置补充(DLQ Topic创建)

Kafka死信队列是一个普通的Topic,只不过你最好提前建好(也可以用自动创建)。

比如:

vbnet 复制代码
kafka:
  topics:
    - name: order-event
    - name: order-event.DLT   # 死信队列

如果你用的 Kafka Server 开了自动建Topic(auto.create.topics.enable=true),也可以不手动建。

(3)写个死信队列监听器(DLT Consumer)

比如你可以新建一个 DLQ 处理器,专门监听死信消息:

typescript 复制代码
@Component
public class DeadLetterConsumer {

    @KafkaListener(topics = "order-event.DLT", groupId = "dead-letter-group")
    public void consumeDeadLetter(String message) {
        System.err.println("【死信消息处理】收到消息:" + message);
        // TODO: 这里可以发告警、落库、人工介入、补偿逻辑等
    }
}

注意:这里可以记录日志、发告警(钉钉/飞书/邮件),或者存到DB里,方便后续人工修复。

🎯 3. DLQ机制总结图

rust 复制代码
正常消费成功 --> 直接ack
正常消费失败 --> 重试 --> 成功ack
重试N次失败 --> DeadLetterPublishingRecoverer --> 投递到 死信Topic
死信消费者监听 --> 告警 or 补偿处理

🔥 总结

步骤 内容
1 给 KafkaListenerContainerFactory 加上 DeadLetterPublishingRecoverer
2 配好死信Topic(一般就是正常Topic后加 .DLT
3 写一个专门的死信消费者
4 死信消息可以告警、存库、人工补偿

这样配置下来,你的 Kafka 消费链路就非常稳定了:不会因为单条脏数据影响整体消费系统健康。

  • 死信消息里最好加上:
    • 原始消息体
    • 消费失败原因(异常栈)
    • 时间戳
  • 死信消费可以打到链路追踪系统,比如接入 Skywalking/Zipkin
  • 对重要消息类型可以设置死信消息补偿重试机制

🚀 每个Bean的作用详细解释

Bean 作用
ServerEndpointExporter 核心 !Spring Boot 启动时会扫描 @ServerEndpoint 注解的类,自动注册成 WebSocket 端点。没有它的话,@ServerEndpoint 注解不会生效!
BaseEndpointConfigure 扩展配置,通常用来自定义 WebSocket 握手过程,比如:校验token、设置自定义属性、统一Session管理、拦截器(handshake拦截)等。

🔥 WebSocket启动内部执行流程

  1. Spring Boot 容器启动 →
  2. 扫描到 @Configuration 注解的 WebSocketConfig
  3. 创建 ServerEndpointExporter 实例,注册到 Spring 容器中 →
  4. ServerEndpointExporter 开始扫描项目中所有 @ServerEndpoint 注解的类 →
  5. 找到后,自动将这些类注册成 WebSocket端点(endpoint) →
  6. 创建 BaseEndpointConfigure Bean,供你后续扩展(比如你可以写自己的 modifyHandshake

写一个 @ServerEndpoint 示例(最简版)

比如:

typescript 复制代码
import javax.websocket.OnMessage;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import javax.websocket.OnOpen;
import javax.websocket.OnClose;

@ServerEndpoint("/ws/echo")
public class EchoWebSocket {

    @OnOpen
    public void onOpen(Session session) {
        System.out.println("WebSocket连接成功: " + session.getId());
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("收到消息: " + message);
        try {
            session.getBasicRemote().sendText("服务端返回: " + message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @OnClose
    public void onClose(Session session) {
        System.out.println("WebSocket连接关闭: " + session.getId());
    }
}

解释一下

  • @ServerEndpoint("/ws/echo"):对外暴露的连接地址,例如:ws://localhost:8080/ws/echo
  • @OnOpen:连接建立时触发
  • @OnMessage:收到消息时触发
  • @OnClose:连接关闭时触发
场景 优化建议
Session管理 统一保存在ConcurrentHashMap<String, Session>,方便广播发消息
心跳检测 定时推送PING消息,避免连接断了还以为连着
安全认证 modifyHandshake 里校验token / header
异常捕获 包一下 @OnError 防止异常断开
线程模型 注意,WebSocket回调是在Tomcat nio线程池内,不要阻塞!可以扔到自己的业务线程池处理

🔥 带Token鉴权的WebSocket全流程设计

✅ 客户端连接 ws://yourserver/ws/xxx?token=xxx

✅ 服务器在握手时解析token

✅ token合法:允许连接

❌ token非法:拒绝连接,关闭通道

🛠️ 正式上代码

(1)改造你的 BaseEndpointConfigure

typescript 复制代码
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;

public class BaseEndpointConfigure extends ServerEndpointConfig.Configurator {

    /**
     * 修改握手逻辑,增加鉴权处理
     */
    @Override
    public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
        // 从url参数中拿token
        String token = getTokenFromRequest(request);

        if (!validateToken(token)) {
            throw new RuntimeException("无效的token,禁止连接WebSocket!");
        }

        // 可以把用户信息保存到属性中,供后续OnOpen等使用
        sec.getUserProperties().put("token", token);
    }

    private String getTokenFromRequest(HandshakeRequest request) {
        String query = request.getQueryString(); // ?token=xxx
        if (query != null && query.startsWith("token=")) {
            return query.substring(6);
        }
        return null;
    }

    private boolean validateToken(String token) {
        // TODO: 这里可以接入真实的token校验,比如JWT解析、Redis验证之类的
        return token != null && token.length() > 5;
    }
}

🔵 解释一下

  • modifyHandshake:在WebSocket握手阶段触发,可以用来做参数检查、认证鉴权
  • getTokenFromRequest:从WebSocket连接的url参数里提取token。
  • validateToken:校验token是否合法(这里简单演示了长度判断,你可以换成解析JWT、查Redis等更复杂的逻辑)。

(2)你的 @ServerEndpoint 类里怎么拿token?

比如你的 EchoWebSocket

typescript 复制代码
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;

@ServerEndpoint(value = "/ws/echo", configurator = BaseEndpointConfigure.class)
public class EchoWebSocket {

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        String token = (String) config.getUserProperties().get("token");
        System.out.println("WebSocket连接建立,token=" + token);
    }

    @OnMessage
    public void onMessage(String message, Session session) {
        try {
            session.getBasicRemote().sendText("服务端返回:" + message);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @OnClose
    public void onClose(Session session) {
        System.out.println("WebSocket连接关闭");
    }

    @OnError
    public void onError(Session session, Throwable error) {
        System.err.println("WebSocket发生错误:" + error.getMessage());
    }
}

注意!

这里 @ServerEndpoint 要加上:

ini 复制代码
configurator = BaseEndpointConfigure.class

不然不会执行自定义握手逻辑!

(3)客户端连接示例

连接时 URL 应该像这样带上 token

bash 复制代码
ws://localhost:8080/ws/echo?token=abcdefg12345

如果 token 不对(比如为空或者非法),服务器在握手阶段就直接拒绝连接了!

🧩 总流程复盘

  1. 客户端连接携带token
  2. SpringBoot 启动 WebSocketConfig,注册 ServerEndpoint
  3. 握手时触发 BaseEndpointConfigure.modifyHandshake
  4. 校验token是否合法
  5. 合法 -> 保存到UserProperties -> 正常连接
  6. 非法 -> 抛异常 -> 连接失败

✅ 总结一下

内容
支持WebSocket连接带token校验
校验不通过,连接直接拒绝
校验通过,token信息可在后续OnOpen里获取
整个流程干净利落,兼容你现在的配置体系
相关推荐
bubiyoushang8882 小时前
解决 Git 访问 GitHub 时的 SSL 错误
git·github·ssl
tonngw3 小时前
【Mac 从 0 到 1 保姆级配置教程 16】- Docker 快速安装配置、常用命令以及实际项目演示
macos·docker·容器·开源·github·docker desktop·orbstack
海码0076 小时前
【版本控制】Git 和 GitHub 入门教程
git·github
烛阴6 小时前
bignumber.js深度解析:驾驭任意精度计算的终极武器
前端·javascript·后端
服务端技术栈6 小时前
电商营销系统中的幂等性设计:从抽奖积分发放谈起
后端
你的人类朋友7 小时前
✍️Node.js CMS框架概述:Directus与Strapi详解
javascript·后端·node.js
面朝大海,春不暖,花不开7 小时前
自定义Spring Boot Starter的全面指南
java·spring boot·后端
乄夜7 小时前
嵌入式面试高频(5)!!!C++语言(嵌入式八股文,嵌入式面经)
c语言·c++·单片机·嵌入式硬件·物联网·面试·职场和发展
钡铼技术ARM工业边缘计算机7 小时前
【成本降40%·性能翻倍】RK3588边缘控制器在安防联动系统的升级路径
后端
CryptoPP8 小时前
使用WebSocket实时获取印度股票数据源(无调用次数限制)实战
后端·python·websocket·网络协议·区块链