Netty中实现设备消息串行处理:Semaphore + 线程池

目录

一、问题背景

[二、解决方案:Semaphore + 线程池](#二、解决方案:Semaphore + 线程池)

核心思想

完整代码实现

业务层调用

三、方案优势

四、关键技术点解析

[1. 为什么用Semaphore而不是synchronized?](#1. 为什么用Semaphore而不是synchronized?)

[2. 为什么用CachedThreadPool?](#2. 为什么用CachedThreadPool?)

[3. 资源清理为什么重要?](#3. 资源清理为什么重要?)


一、问题背景

在物联网终端设备接入服务中,我们经常遇到这样的场景:

  • 同一个设备(IMEI)会频繁发送不同类型的消息(登录、位置上报、心跳等)

  • 同类型消息必须串行处理(如连续两个登录请求,必须等第一个处理完再处理第二个)

  • 不同类型消息可以并行处理(如登录和位置上报可以同时进行)

如果用传统的单线程池方案,会导致所有消息排队,严重影响吞吐量;如果用普通线程池,又会出现并发冲突,导致数据错乱。

二、解决方案:Semaphore + 线程池

经过多次尝试,最终采用**Semaphore(信号量)**强制串行,彻底解决问题。

核心思想

  • 为每个 (IMEI, Worker类型) 分配一个Semaphore(许可证数量=1)

  • 任务执行前必须acquire() 获取许可证,执行后**release()**释放

  • 同一个Key的任务自然会排队,不同Key的任务互不影响

完整代码实现

java 复制代码
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;

@Slf4j
public class ExecutorCache {

    // IMEI+Worker类型 -> 信号量(保证串行)
    private final static ConcurrentHashMap<String, Semaphore> workerLocks = new ConcurrentHashMap<>();
    
    // IMEI+Worker类型 -> 线程池(负责执行任务)
    private final static ConcurrentHashMap<String, ExecutorService> deviceExecutors = new ConcurrentHashMap<>();

    /**
     * 执行任务,保证同一个 IMEI+Worker 串行执行
     * 
     * @param imei       设备ID
     * @param workerType Worker类型(如 LoginWorker)
     * @param task       需要执行的任务
     */
    public static void executeSerial(String imei, String workerType, Runnable task) {
        String lockKey = workerType + "-" + imei;

        // 获取信号量(最多1个许可 = 互斥锁)
        Semaphore semaphore = workerLocks.computeIfAbsent(lockKey, k -> new Semaphore(1));

        // 获取线程池(缓存线程池,因为锁已经控制了并发)
        ExecutorService executor = deviceExecutors.computeIfAbsent(lockKey, 
            k -> Executors.newCachedThreadPool(r -> new Thread(r, lockKey)));

        // 提交任务到线程池
        executor.submit(() -> {
            try {
                semaphore.acquire();  // 获取锁,如果没有许可则等待
                task.run();           // 执行业务逻辑
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("任务被中断:{}", lockKey, e);
            } finally {
                semaphore.release();  // 释放锁
            }
        });
    }

    /**
     * 清理设备的所有资源(设备断开时调用)
     */
    public static void removeDevice(String imei) {
        // 清理信号量
        workerLocks.keySet().removeIf(key -> key.endsWith("-" + imei));
        // 清理线程池
        deviceExecutors.keySet().removeIf(key -> key.endsWith("-" + imei));
    }
}

业务层调用

java 复制代码
private void protocolTerminal(String imei, Channel channel, String cmd, String data) {
    switch (cmd) {
        case ProtocolConstant.TLOGIN:
            TerminalChannelCache.putTempTerminalChannelMap(imei, channel);
            ExecutorCache.executeSerial(imei, "LoginWorker", 
                () -> loginWorker.handle(data));
            break;
        case ProtocolConstant.TPOS:
            ExecutorCache.executeSerial(imei, "locationWorker", 
                () -> locationWorker.handle(data));
            break;
        case ProtocolConstant.THEART:
            ExecutorCache.executeSerial(imei, "terHeartWorker", 
                () -> terminalHeartWorker.handle(data));
            break;
        // ... 其他命令类型
    }
}

三、方案优势

特性 传统方案 Semaphore方案
同设备同类型串行 ❌ 依赖线程池复用 ✅ 强制串行
同设备不同类型并行 ✅ 可以 ✅ 可以
资源清理 ⚠️ 容易内存泄漏 ✅ 显式清理
调试难度
代码复杂度

四、关键技术点解析

1. 为什么用Semaphore而不是synchronized?

  • synchronized 会阻塞当前线程(这里是Netty的IO线程),影响其他设备

  • Semaphore配合独立线程池,将等待放到业务线程池中,IO线程快速返回

2. 为什么用CachedThreadPool?

  • 每个 (IMEI, Worker类型) 的任务量不大

  • CachedThreadPool 空闲线程60秒自动回收,不会造成资源浪费

  • 锁机制已经保证了串行,线程池本身不需要限制并发数

3. 资源清理为什么重要?

如果不调用 removeDevice(),随着设备不断上下线:

  • workerLocksdeviceExecutors 会无限增长

  • 导致内存泄漏,最终OOM

相关推荐
分支预测失败2 分钟前
RISC-V 虚拟内存与 MMU 实战:从 Sv39 页表到 Linux 地址空间
后端
步行cgn2 分钟前
@SpringBootTest 详解:Spring Boot 测试的核心注解
后端
吴声子夜歌6 分钟前
Guava——集合(二)
java·guava
xiaoqiMikko6 分钟前
一句“为了保持同步”的注释,正好打破了同步 —— Netty 响应错配漏洞实测
java
对象存储与RustFS7 分钟前
给 RustFS 拆多租户权限:IAM 用户、组与策略的实战
后端·rust·开源
疯狂打码的少年10 分钟前
【数据库技术】SQL高级查询(分组/排序/聚合函数)
java·数据库·笔记·sql
DDXYcoder12 分钟前
万字解析动态内存从编译到运行
java·前端·算法
坐吃山猪34 分钟前
【多线程】什么时候必须/推荐使用Thread类
java·开发语言
ZGG00334 分钟前
MCP 02:MCP 的各类比对——和 Function Calling、A2A、Skill 到底什么关系
人工智能·后端·python
IT_陈寒35 分钟前
Vite热更新突然失效?可能是这个配置在捣鬼
前端·人工智能·后端