1. 项目概述从零构建轻量级即时通讯系统去年接手公司内部通讯工具改造时我意识到一个轻量级聊天系统的核心价值——它不仅是信息传递的工具更是团队协作的神经中枢。这个用Spring BootWebSocket实现的简易聊天系统包含完整前后端源码特别适合需要快速搭建内部通讯平台的中小团队。整套系统可在2小时内完成部署日均承载万级消息无压力。系统采用分层架构设计前端使用Vue3Element Plus构建响应式界面后端基于Spring Boot 2.7提供RESTful API通过WebSocket实现实时消息推送。数据库选用MySQL 8.0存储结构化数据Redis 6.2缓存活跃会话信息。这种技术组合既保证了开发效率又能满足中小规模场景下的性能需求。关键特性速览单聊/群聊消息收发支持文字/图片/文件在线状态实时显示消息已读回执历史消息查询跨设备会话同步2. 核心技术方案解析2.1 通信协议选型对比面对HTTP轮询、长轮询、SSE和WebSocket等方案我们最终选择全双工通信的WebSocket协议。实测数据显示在100并发用户场景下WebSocket的服务器资源消耗仅为长轮询的1/5。以下是关键参数对比协议类型延迟(ms)CPU占用(%)内存消耗(MB)HTTP轮询300-50045120长轮询150-3003895WebSocket50-1001265实现时需要注意WebSocket的自动重连机制。我们在前端封装了如下重连逻辑const socket new WebSocket(url); let reconnectTimer null; socket.onclose () { if(!reconnectTimer) { reconnectTimer setTimeout(() { initWebSocket(); // 重新初始化连接 reconnectTimer null; }, 3000); // 3秒指数退避重试 } };2.2 消息存储设计采用写扩散模式存储消息核心表结构如下messages表CREATE TABLE messages ( id BIGINT PRIMARY KEY AUTO_INCREMENT, conversation_id VARCHAR(64) NOT NULL COMMENT 会话ID, sender_id BIGINT NOT NULL, content_type TINYINT DEFAULT 1 COMMENT 1文本 2图片 3文件, content TEXT, file_url VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_conversation (conversation_id), INDEX idx_sender (sender_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;conversations表CREATE TABLE conversations ( id VARCHAR(64) PRIMARY KEY, type TINYINT DEFAULT 1 COMMENT 1单聊 2群聊, name VARCHAR(64), avatar VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;存储优化技巧大文本内容使用COMPRESS()压缩存储实测可节省40%空间频繁访问的会话信息用Redis缓存采用LRU淘汰策略历史消息按月分表通过Spring动态数据源切换访问3. 关键功能实现细节3.1 消息实时推送流程后端WebSocket核心处理逻辑ServerEndpoint(/chat/{userId}) public class ChatEndpoint { private static final ConcurrentHashMapLong, Session sessions new ConcurrentHashMap(); OnOpen public void onOpen(Session session, PathParam(userId) Long userId) { sessions.put(userId, session); broadcastOnlineStatus(userId, true); } OnMessage public void onMessage(String message, Session session) { MessageDTO msg JSON.parseObject(message, MessageDTO.class); if(msg.getType() MessageType.TEXT) { saveMessage(msg); // 持久化存储 Session target sessions.get(msg.getToId()); if(target ! null) { target.getAsyncRemote().sendText(JSON.toJSONString(msg)); } } } OnClose public void onClose(PathParam(userId) Long userId) { sessions.remove(userId); broadcastOnlineStatus(userId, false); } }前端消息处理核心代码// 建立WebSocket连接 const socket new WebSocket(wss://example.com/chat/${userId}); // 接收消息处理 socket.onmessage (event) { const msg JSON.parse(event.data); if(msg.type TEXT) { store.commit(addMessage, msg); // Vuex存储 playNotificationSound(); // 播放提示音 } else if(msg.type STATUS) { updateUserStatus(msg.userId, msg.online); } }; // 发送消息方法 function sendMessage(content, toId) { const msg { type: TEXT, content, fromId: currentUser.id, toId, timestamp: Date.now() }; socket.send(JSON.stringify(msg)); }3.2 未读消息计数优化采用Redis的HyperLogLog实现高效计数误差率控制在0.8%以内public class UnreadCounter { private final RedisTemplateString, String redisTemplate; // 增加未读数 public void increment(Long conversationId, Long userId) { String key unread: conversationId : userId; redisTemplate.opsForHyperLogLog().add(key, UUID.randomUUID().toString()); } // 获取未读数 public long getCount(Long conversationId, Long userId) { String key unread: conversationId : userId; return redisTemplate.opsForHyperLogLog().size(key); } // 清空未读 public void reset(Long conversationId, Long userId) { String key unread: conversationId : userId; redisTemplate.delete(key); } }4. 部署与性能调优4.1 容器化部署方案使用Docker Compose编排服务docker-compose.yml关键配置version: 3 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: ${DB_PASSWORD} volumes: - ./mysql-data:/var/lib/mysql ports: - 3306:3306 redis: image: redis:6.2-alpine ports: - 6379:6379 app: build: . ports: - 8080:8080 depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod启动命令docker-compose up -d --build4.2 性能压测数据使用JMeter进行1000并发测试服务器配置4核8G CentOS 7.9测试场景吞吐量(msg/s)平均延迟(ms)错误率(%)纯文本消息2850620带附件消息9201850.2混合消息流15301280.1优化措施开启MySQL查询缓存query_cache_size 64M调整WebSocket线程池server.tomcat.max-threads200 server.tomcat.min-spare-threads20Redis管道批处理ListObject results redisTemplate.executePipelined((RedisCallbackObject) connection - { for(Message msg : messages) { connection.stringCommands().set( (msg:msg.getId()).getBytes(), JSON.toJSONString(msg).getBytes() ); } return null; });5. 常见问题排查指南5.1 WebSocket连接异常症状前端频繁出现Connection closed提示排查步骤检查Nginx配置location /chat { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 86400; }验证防火墙规则iptables -L | grep 8080检查心跳设置前端每30秒发送PING帧5.2 消息乱序问题现象移动端切换网络后消息顺序错乱解决方案前端增加序列号校验let lastSeq 0; socket.onmessage (event) { const msg JSON.parse(event.data); if(msg.seq lastSeq) return; lastSeq msg.seq; // 处理消息... };后端添加单调递增序列号Column(name sequence_id) GeneratedValue(strategy GenerationType.SEQUENCE) private Long sequenceId;5.3 大群组性能下降表现超过200人的群组消息延迟明显优化方案采用读扩散写扩散混合模式实现分级推送策略if(recipients.size() 50) { // 大群走异步队列 kafkaTemplate.send(group-message, message); } else { // 小群直接推送 sendImmediately(message); }开启消息合并每100ms批量发送一次6. 扩展功能实现建议6.1 端到端加密方案基于libsodium实现消息加密// 前端加密 const publicKey await sodium.crypto_box_keypair(); const encrypted await sodium.crypto_box_easy( message, nonce, recipientPublicKey, senderPrivateKey ); // 后端仅作转发无法解密内容6.2 消息撤回功能实现逻辑前端标记消息状态为recalled后端同步更新数据库UPDATE messages SET status 2 WHERE id ? AND sender_id ?;推送撤回事件通知{ type: RECALL, messageId: 123456, operator: 1001 }6.3 消息搜索优化使用Elasticsearch构建全文索引Document(indexName messages) public class MessageIndex { Id private Long id; Field(type FieldType.Text, analyzer ik_max_word) private String content; // 其他字段... } // 搜索示例 NativeSearchQuery query new NativeSearchQueryBuilder() .withQuery(QueryBuilders.matchQuery(content, 关键词)) .build();这套系统在实际部署时我建议先用内网小范围测试核心功能特别是WebSocket连接的稳定性。遇到过最棘手的问题是Nginx默认会超时关闭长连接需要在配置中显式设置proxy_read_timeout。另外MySQL连接池参数要根据实际并发量调整初期建议设置spring.datasource.hikari.maximum-pool-size20避免连接耗尽导致服务不可用。