多线程情况下长连接中的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项目中,手搓版的思想是解决该问题的可行路径。

相关推荐
毅炼2 小时前
Java 基础常见问题总结(1)
开发语言·python
无名-CODING2 小时前
Spring事务管理完全指南:从零到精通(上)
java·数据库·spring
fengxin_rou2 小时前
【黑马点评实战篇|第一篇:基于Redis实现登录】
java·开发语言·数据库·redis·缓存
数智工坊2 小时前
【数据结构-栈】3.1栈的顺序存储-链式存储
java·开发语言·数据结构
短剑重铸之日2 小时前
《设计模式》第七篇:适配器模式
java·后端·设计模式·适配器模式
R-G-B2 小时前
python 验证每次操作图片处理的顺序是否一致,按序号打上标签,图片重命名
开发语言·python·图片重命名·按序号打上标签·验证图片处理的顺序
小二·2 小时前
Go 语言系统编程与云原生开发实战(第10篇)性能调优实战:Profiling × 内存优化 × 高并发压测(万级 QPS 实录)
开发语言·云原生·golang
DFT计算杂谈2 小时前
VASP+Wannier90 计算位移电流和二次谐波SHG
java·服务器·前端·python·算法
多多*2 小时前
2月3日面试题整理 字节跳动后端开发相关
android·java·开发语言·网络·jvm·adb·c#