# 充电桩频繁登录问题优化记录 ## 问题背景 **发生时间**: 2026-03-21 06:00 左右 **问题描述**: 充电桩出现频繁登录的情况 **初步原因**: 项目没有及时回复桩端的心跳消息,未及时回复超过 3 次后桩端会发起重连 --- ## 问题分析 ### 问题 1: Pipeline 配置存在潜在阻塞点(最关键) **文件**: `jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerChannelInitializer.java:41` **问题描述**: - `nettyServerHandler` 绑定到 `businessGroup` 线程池(32 线程) - 但 `echoServerHandler` **没有绑定到任何 EventExecutorGroup**,它会在 **IO 线程**(Worker 线程)中执行 - 如果 IO 线程被其他任务阻塞,心跳回复就无法及时发送 **影响**: 这是导致心跳回复不及时的最主要原因! --- ### 问题 2: 心跳处理中存在阻塞的数据库操作 **文件**: `jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/HeartbeatRequestHandler.java:54` **问题描述**: - `saveLastTimeAndCheckChannel()` 是**同步**执行的,包含 Redis 写操作 - 如果 Redis 响应慢,会阻塞心跳响应 --- ### 问题 3: `PileChannelEntity.checkChannel()` 使用同步阻塞遍历 **文件**: `jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileChannelEntity.java:108-115` **问题描述**: - `getPileSnByChannelId()` 使用线性遍历整个 `manager.entrySet()` - 当连接数很多时,这个遍历操作会很慢,并且会阻塞执行线程 --- ### 问题 4: 线程池配置可能导致任务堆积 **文件**: `jsowell-framework/src/main/java/com/jsowell/framework/config/ThreadPoolConfig.java:63` **问题描述**: - 使用 `CallerRunsPolicy` 拒绝策略,当队列满时,任务会在调用者线程中执行 - 如果 Netty 的 IO 线程或 businessGroup 线程作为调用者,会**阻塞这些关键线程**! --- ## 优化方案 ### 优化 1: 修复 Pipeline 配置(优先级:P0)✅ 已完成 **文件**: `jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerChannelInitializer.java:41` **修改内容**: ```java // 修改前 pipeline.addLast(businessGroup, nettyServerHandler); pipeline.addLast(echoServerHandler); // 修改后 pipeline.addLast(businessGroup, nettyServerHandler); pipeline.addLast(businessGroup, echoServerHandler); // 回复Handler也绑定到业务线程池 ``` **效果**: 避免心跳回复被 IO 线程阻塞 --- ### 优化 2: 优化 PileChannelEntity 使用双向映射(优先级:P0)✅ 已完成 **文件**: `jsowell-common/src/main/java/com/jsowell/common/enums/ykc/PileChannelEntity.java` **修改内容**: 1. 新增反向映射 `channelIdToPileSnMap` 2. `getPileSnByChannelId()` 从 O(n) 优化到 O(1) 3. 同步维护反向映射(checkChannel、removeByPileSn、removeByChannelId) 4. 新增 `removeByPileSnAndChannelId()` 安全删除方法 **效果**: 当连接数多时,查询性能大幅提升 --- ### 优化 3: 修改线程池拒绝策略(优先级:P1)✅ 已完成 **文件**: `jsowell-framework/src/main/java/com/jsowell/framework/config/ThreadPoolConfig.java` **修改内容**: ```java // 修改前 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); private final int queueCapacity = 2000; // 修改后 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardOldestPolicy()); private final int queueCapacity = 5000; // 增大队列容量 ``` **效果**: 避免阻塞关键线程(Netty IO 线程、业务线程池) --- ### 优化 4: 心跳处理完全异步化(优先级:P1)✅ 已完成 **文件**: `jsowell-netty/src/main/java/com/jsowell/netty/handler/yunkuaichong/HeartbeatRequestHandler.java:67-85` **修改内容**: ```java // 修改前:saveLastTimeAndCheckChannel 同步执行 saveLastTimeAndCheckChannel(pileSn, channel); CompletableFuture.runAsync(() -> updateStatus(...), executor); // 修改后:先返回心跳应答,再异步处理 byte[] response = getResult(ykcDataProtocol, messageBody); CompletableFuture.runAsync(() -> { saveLastTimeAndCheckChannel(pileSn, channel); pileBasicInfoService.updateStatus(...); }, executor); return response; ``` **效果**: 心跳回复不受 Redis/数据库操作影响 --- ### 优化 5: 增加心跳处理耗时监控(优先级:P2)✅ 已完成 **文件**: `jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerHandler.java:81-127` **修改内容**: ```java @Override public void channelRead(ChannelHandlerContext ctx, Object message) throws Exception { long startTime = System.currentTimeMillis(); try { // ... 原有代码 ... } finally { ReferenceCountUtil.release(message); // 性能监控:记录消息处理耗时 long elapsed = System.currentTimeMillis() - startTime; String frameType = ctx.channel().attr(LAST_FRAME_TYPE).get(); // 心跳帧(0x03)处理超过50ms警告,其他帧超过200ms警告 int warnThreshold = "0x03".equals(frameType) ? 50 : 200; if (elapsed > warnThreshold) { log.warn("【性能警告】消息处理耗时: {}ms, 帧类型: {}, channelId: {}", elapsed, frameType, ctx.channel().id().asLongText()); } } } ``` **效果**: 便于后续排查性能问题 --- ## 额外优化(团队完成) ### 优化 6: IdleStateHandler 超时调整 **文件**: `jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerChannelInitializer.java:38` **修改内容**: ```java // 修改前 pipeline.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS)); // 修改后 pipeline.addLast(new IdleStateHandler(15, 0, 0, TimeUnit.SECONDS)); // 读空闲tick设置为15s,连续3次空闲后再由handler执行关闭 ``` **效果**: 更快检测到心跳超时,及时处理连接异常 --- ### 优化 7: 新增连接管理 AttributeKey **文件**: `jsowell-netty/src/main/java/com/jsowell/netty/server/yunkuaichong/NettyServerHandler.java:40-48` **修改内容**: ```java public static final AttributeKey IDLE_COUNT = AttributeKey.valueOf("ykc_idle_count"); public static final AttributeKey LAST_FRAME_TYPE = AttributeKey.valueOf("ykc_last_frame_type"); public static final AttributeKey LAST_RECEIVE_AT = AttributeKey.valueOf("ykc_last_receive_at"); public static final AttributeKey LAST_SERIAL_NUMBER = AttributeKey.valueOf("ykc_last_serial_number"); public static final AttributeKey DISCONNECT_REASON = AttributeKey.valueOf("ykc_disconnect_reason"); ``` **效果**: 更精细的连接管理和断开原因追踪 --- ## 修改文件清单 | 序号 | 文件路径 | 修改内容 | 状态 | |------|----------|----------|------| | 1 | `jsowell-netty/.../NettyServerChannelInitializer.java` | EchoServerHandler 绑定到业务线程池
IdleStateHandler 从 30s 调整为 15s | ✅ 完成 | | 2 | `jsowell-common/.../PileChannelEntity.java` | 新增反向映射,优化查询性能
新增 removeByPileSnAndChannelId 方法 | ✅ 完成 | | 3 | `jsowell-framework/.../ThreadPoolConfig.java` | 修改拒绝策略,增大队列容量 | ✅ 完成 | | 4 | `jsowell-netty/.../HeartbeatRequestHandler.java` | 心跳处理完全异步化 | ✅ 完成 | | 5 | `jsowell-netty/.../NettyServerHandler.java` | 增加消息处理耗时监控
新增 AttributeKey 用于连接管理 | ✅ 完成 | --- ## 测试建议 ### 1. 编译打包 ```bash mvn clean package -DskipTests ``` ### 2. 部署后观察日志 关注以下指标: - 是否还有桩频繁登录的情况 - 心跳处理耗时是否降低 - 是否出现 `【性能警告】` 日志 ### 3. 监控 Redis 和数据库性能 - Redis 响应时间应 < 5ms - 数据库连接池应充足 - 检查 Redis 服务性能 ### 4. 持续监控 部署后持续观察 24 小时,确认问题是否彻底解决 --- ## 预期效果 | 指标 | 优化前 | 优化后 | |------|--------|--------| | 心跳回复延迟 | 可能被阻塞 | < 10ms | | 桩频繁登录 | 存在 | 消除 | | 连接查询性能 | O(n) | O(1) | | 线程阻塞风险 | 高 | 低 | | 超时检测响应 | 30s | 15s(更快)| --- ## 相关文档 - `CLAUDE.md` - 项目架构文档 - `jsowell-netty/CLAUDE.md` - Netty 模块文档 - `jsowell-common/CLAUDE.md` - 通用模块文档 --- ## 记录信息 | 项目 | 内容 | |------|------| | 记录人 | Claude Code + 开发团队 | | 记录时间 | 2026-03-21 | | Git 分支 | dev | | 提交建议 | 建议创建新分支提交,便于回滚 | | 状态 | ✅ 全部优化已完成 |