多线程情况下长连接中的session并发问题

默认的session是 线程不安全的,最近遇到了一个需求,需要在长连接中使用异步线程池,每个线程都用到了session来向前端通信.。

1、ConcurrentWebSocketSessionDecorator

首先spring提供了一个最省心最省事的方案,那就是使用线程安全的session包装类

java 复制代码
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.TextMessage;

// 1. 在连接建立时,创建并保存装饰后的线程安全Session
public class MyHandler extends TextWebSocketHandler {
    private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        // 使用ConcurrentWebSocketSessionDecorator包装原始session
        WebSocketSession decoratedSession = new ConcurrentWebSocketSessionDecorator(
                session, 
                10 * 1000, // 发送超时时间(毫秒)
                1024 * 1024 // 最大缓冲区大小(字节)
        );
        String sessionId = session.getId();
        sessions.put(sessionId, decoratedSession); // 保存装饰后的session
    }

    // 2. 在任何线程中,都可以安全地调用发送方法
    public void broadcastMessage(String message) {
        TextMessage textMessage = new TextMessage(message);
        for (WebSocketSession session : sessions.values()) {
            try {
                // 直接发送,装饰器内部会处理并发问题
                session.sendMessage(textMessage);
            } catch (IOException e) {
                // 处理异常
            }
        }
    }
}

2、手搓

那有人就要说了,啊啊啊,我没有用spring,那我有解决办法吗?

有的,兄弟,有的!ConcurrentWebSocketSessionDecorator说白了就是搞了一个缓存的队列,然后一个单线程session一个一个的发,那我们自己手搓一套不就得了吗?好点子,说干就干

java 复制代码
import java.util.concurrent.*;

public class SessionMessageDispatcher {
    // 为每个Session ID维护一个消息队列和分发线程
    private static final Map<String, BlockingQueue<String>> sessionQueues = new ConcurrentHashMap<>();
    private static final ExecutorService executor = Executors.newCachedThreadPool();

    // 注册Session时,启动其专属的分发线程
    public static void registerSession(String sessionId, Session wsSession) {
        BlockingQueue<String> queue = new LinkedBlockingQueue<>();
        sessionQueues.put(sessionId, queue);

        executor.submit(() -> {
            while (wsSession.isOpen()) {
                try {
                    // 从队列中阻塞获取消息
                    String message = queue.poll(1, TimeUnit.SECONDS);
                    if (message != null) {
                        synchronized (wsSession) { // 此处仍需锁,但只有这一个线程竞争
                            wsSession.getBasicRemote().sendText(message);
                        }
                    }
                } catch (Exception e) {
                    // 发生异常,结束该Session的分发循环
                    break;
                }
            }
            // 连接关闭,清理资源
            sessionQueues.remove(sessionId);
        });
    }

    // 任何线程通过此方法发送消息,都是非阻塞的入队操作
    public static void sendMessageAsync(String sessionId, String message) {
        BlockingQueue<String> queue = sessionQueues.get(sessionId);
        if (queue != null) {
            queue.offer(message); // 非阻塞放入队列
        }
    }
}

那么这个时候又有人说了,那我们自己都能手搓,那不是跟spring封装那个一样了吗?那spring不过如此嘛,真的是这样吗?我们来康康区别

1. 实现复杂度与维护成本

这是两者最核心的差异。(ConcurrentWebSocketSessionDecorator)是Spring框架内置的解决方案,开箱即用。只需在连接建立时用其包装原始的WebSocketSession即可,后续所有发送操作都自动具备线程安全性,无需在业务代码中引入任何锁或队列逻辑。这极大地降低了代码复杂度和出错的概率。

而手搓版需要您自行实现一个完整的事件驱动架构。这通常包括:为每个Session维护一个独立的消息队列(如BlockingQueue)、一个专属的消费者线程、以及线程的生命周期管理(启动、停止、异常恢复)。这种自研架构不仅代码量大,还引入了额外的状态(队列、线程映射),增加了系统的复杂度和调试、维护的难度。

2. 性能与吞吐量

ConcurrentWebSocketSessionDecorator在性能和吞吐量上达到了一个非常优秀的平衡。它内部采用 "非阻塞队列 + tryLock" 的机制。当多个线程并发发送消息时,它们会快速将消息放入缓冲区队列,然后由成功获取锁的单个线程进行串行发送。未能获取锁的线程不会被阻塞,从而在高并发下仍能保持良好的整体吞吐量。

手搓版在理论上可以实现最高的吞吐量,因为它将生产(业务线程入队)和消费(单线程发送)完全解耦,生产线程几乎不会被阻塞。然而,这种优势只有在消息生产频率极高、且单次发送操作本身成为瓶颈的极端场景下才会明显体现。对于大多数业务系统,ConcurrentWebSocketSessionDecorator的性能已经足够,而手搓版本为实现极致性能所付出的架构复杂度和资源消耗(每个连接一个线程)可能并不划算。

3. 可靠性(背压保护与异常处理)

这是ConcurrentWebSocketSessionDecorator一个被低估的巨大优势。ConcurrentWebSocketSessionDecorator内置了背压(Backpressure)保护机制,通过sendTimeLimit(发送超时时间)和bufferSizeLimit(缓冲区大小限制)两个参数,可以防止因消息生产速度远大于网络发送速度而导致的消息无限制堆积和内存溢出。当超过限制时,它会将会话标记为不可靠并关闭,这是一种对系统的自我保护。

在手搓版的自研实现中,必须自行实现类似的保护逻辑,例如设置队列容量上限、监控队列积压情况并采取相应措施(如丢弃消息或关闭连接),否则很容易引发资源泄漏和系统不稳定。

4. 技术栈耦合性

ConcurrentWebSocketSessionDecorator与Spring WebSocket深度集成,是其生态的一部分,使用起来自然、顺畅。但如果项目不使用Spring框架,则无法直接采用此方案。

手搓版的优势在于其思想是通用的,可以移植到任何技术栈(如原生Java WebSocket、Netty等)。可以根据其"单线程读写+消息队列"的核心思想,在各自的技术框架内实现类似的模式。因此,在非Spring项目中,手搓版的思想是解决该问题的可行路径。

相关推荐
用户83071968408223 分钟前
Spring Boot 项目中日期处理的最佳实践
java·spring boot
JavaGuide1 小时前
Claude Opus 4.6 真的用不起了!我换成了国产 M2.5,实测真香!!
java·spring·ai·claude code
IT探险家1 小时前
Java 基本数据类型:8 种原始类型 + 数组 + 6 个新手必踩的坑
java
花花无缺1 小时前
搞懂new 关键字(构造函数)和 .builder() 模式(建造者模式)创建对象
java
用户908324602731 小时前
Spring Boot + MyBatis-Plus 多租户实战:从数据隔离到权限控制的完整方案
java·后端
桦说编程2 小时前
实战分析 ConcurrentHashMap.computeIfAbsent 的锁冲突问题
java·后端·性能优化
玹外之音3 小时前
Spring AI MCP 实战:将你的服务升级为 AI 可调用的智能工具
spring·ai编程
来一斤小鲜肉4 小时前
Spring AI入门:第一个AI应用跑起来
spring·ai编程
NE_STOP6 小时前
springMVC-常见视图组件与RESTFul编程风格
spring
大道至简Edward6 小时前
Spring Boot 2.7 + JDK 8 升级到 Spring Boot 3.x + JDK 17 完整指南
spring boot·后端