分布式微服务系统架构第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里获取
整个流程干净利落,兼容你现在的配置体系
相关推荐
雷渊几秒前
如何设计一个订单号生成服务?
后端
心走2 分钟前
八股文中TCP三次握手怎么具象理解?
前端·面试
雷渊5 分钟前
设计秒杀系统需要考虑哪些因素?
后端
无妄无望13 分钟前
Git,本地上传项目到github
git·github
小华同学ai13 分钟前
90.9K star!Open WebUI一键部署AI聊天界面,这个开源项目让大模型交互更简单!
github
super凹凸曼14 分钟前
分享一个把你的API快速升级为MCP规范的方案,可在线体验
java·后端·开源
离线请留言16 分钟前
本地密码管理器-Vaultwarden
后端
IT杨秀才17 分钟前
LangChain框架入门系列(3):数据连接
人工智能·后端·langchain
IT杨秀才18 分钟前
LangChain框架入门系列(2):Hello LangChain
人工智能·后端·langchain
七月丶21 分钟前
🧼 为什么我开始在项目里禁用 CSS 文件?
前端·javascript·后端