- 解决 NettyServerHandler.java 的 Git 合并冲突 - 添加消息处理耗时监控到 channelRead 方法 - 更新 improve_record.md 优化记录文档
8.4 KiB
充电桩频繁登录问题优化记录
问题背景
发生时间: 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
修改内容:
// 修改前
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
修改内容:
- 新增反向映射
channelIdToPileSnMap getPileSnByChannelId()从 O(n) 优化到 O(1)- 同步维护反向映射(checkChannel、removeByPileSn、removeByChannelId)
- 新增
removeByPileSnAndChannelId()安全删除方法
效果: 当连接数多时,查询性能大幅提升
优化 3: 修改线程池拒绝策略(优先级:P1)✅ 已完成
文件: jsowell-framework/src/main/java/com/jsowell/framework/config/ThreadPoolConfig.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
修改内容:
// 修改前: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
修改内容:
@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
修改内容:
// 修改前
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
修改内容:
public static final AttributeKey<Integer> IDLE_COUNT = AttributeKey.valueOf("ykc_idle_count");
public static final AttributeKey<String> LAST_FRAME_TYPE = AttributeKey.valueOf("ykc_last_frame_type");
public static final AttributeKey<String> LAST_RECEIVE_AT = AttributeKey.valueOf("ykc_last_receive_at");
public static final AttributeKey<String> LAST_SERIAL_NUMBER = AttributeKey.valueOf("ykc_last_serial_number");
public static final AttributeKey<String> 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. 编译打包
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 |
| 提交建议 | 建议创建新分支提交,便于回滚 |
| 状态 | ✅ 全部优化已完成 |