工业级实时聊天系统完整落地

前言

市面上大部分实时聊天教程仅单机 Demo,存在无法集群部署、断线丢失消息、无离线消息、跨服务用户无法互通、无心跳保活、消息重复乱序等致命问题,不能直接上线。 本文提供一套中小企业通用成熟 IM 架构,基于SpringBoot + Socket.IO + Redis Pub/Sub + RabbitMQ + MySQL + Nginx,完整实现私聊、群聊、在线状态、离线消息、已读回执、断线重连、集群高可用、消息可靠投递,所有配置、表结构、核心代码均可直接复制用于项目开发,覆盖开发、测试、云服务器部署全流程。

一、技术栈选型与选型理由

整体技术栈

  1. 前端:Vue3 + Vite + socket.io-client
  2. 后端网关:SpringBoot 2.7 + socket.io-spring-boot-starter
  3. 消息分发中间件:Redis(Pub/Sub 跨节点广播、在线状态缓存、心跳管理)
  4. 异步削峰中间件:RabbitMQ(消息持久化、异步入库、死信兜底)
  5. 持久化存储:MySQL 8.0(聊天记录、好友、群组、用户基础数据)
  6. 接入层:Nginx(负载均衡、WebSocket 协议升级、HTTPS、限流)
  7. 部署环境:CentOS7/8 云服务器、Docker 一键部署

各组件选型原因

  1. Socket.IO 替代原生 WebSocket 原生 WebSocket 无自动断线重连、无心跳检测、无房间分组、网络弱网无降级方案,自研容错逻辑成本极高;Socket.IO 内置心跳、重连、房间、ACK 回执,前端后端 API 统一,大幅降低开发成本。
  2. Redis Pub/Sub 解决集群互通核心痛点 多台 IM 服务实例维护独立长连接,不同服务下用户无法互相推送消息;Redis 发布订阅实现全服务消息广播,任意节点消息同步至全部实例。
  3. RabbitMQ 保证消息不丢失 聊天消息先投递 MQ 异步入库,避免同步写 MySQL 阻塞长连接;支持消息持久化、重试、死信队列,防止数据库宕机丢失聊天记录。
  4. 分层存储设计 MySQL 永久存储全量聊天记录;Redis 缓存在线状态、未读消息、临时会话,高频查询走内存,降低 DB 压力。
  5. Nginx 负载均衡 统一域名入口,完成 WebSocket 协议升级,分发长连接至多台 IM 服务,单机上限 3-5 万在线用户,集群可横向扩容支撑十万级在线。

不推荐方案对比

  1. 轮询 / 长轮询:延迟高、服务器带宽消耗大,淘汰;
  2. SSE:仅服务端单向推送,无法双向聊天;
  3. MQTT:多用于物联网设备,缺少 IM 房间、已读、离线消息配套能力;
  4. 第三方 IM SDK(环信 / 融云):私有化部署成本高,业务逻辑无法自定义,数据存在第三方风险。

二、系统整体分层架构

四层架构(生产标准分层)

  1. 接入层 Nginx
  • SSL 证书统一处理,80 端口跳转 HTTPS
  • WebSocket 协议升级配置,支持长连接转发
  • 负载均衡分发用户连接至多 IM 服务节点
  • IP 限流、防盗链、恶意请求拦截
  1. 网关层 IM Socket 服务集群(多实例)
  • 维护客户端长连接,心跳检测、断线重连处理
  • 房间管理、私聊 / 群聊消息接收、参数校验、敏感词过滤
  • Redis 读写:在线状态、未读消息、Pub/Sub 消息广播订阅
  • 消息投递至 RabbitMQ 异步队列
  1. 异步消息层 RabbitMQ
  • 消息持久化,削峰填谷,高并发聊天不阻塞网关服务
  • 消费消息写入 MySQL,处理离线消息存储
  • 死信队列存储处理失败消息,定时重试
  1. 数据持久层 Redis + MySQL
  • Redis:在线用户映射、心跳过期键、群成员缓存、未读消息列表
  • MySQL:用户、好友关系、群组、群成员、全量聊天记录

消息流转流程(一对一私聊)

  1. 用户 A 前端建立 Socket 连接,携带 token 登录认证;
  2. 后端校验 token,Redis 写入在线记录user:online:{userId} = 当前服务实例ID,设置 30s 过期;
  3. A 发送私聊消息,携带唯一 requestId、接收人 userId、消息内容;
  4. 后端校验消息长度、敏感词、限流规则;
  5. 封装完整消息体,投递 RabbitMQ 消息队列做持久化兜底;
  6. 消息通过 Redis Pub/Sub 发布至全局聊天频道;
  7. 所有 IM 服务实例订阅频道,匹配接收人在线状态;
  8. 若接收人 B 连接在当前实例,直接 Socket 推送消息至前端;
  9. B 前端收到消息,返回ack回执给服务端;
  10. 服务端收到回执,更新 MySQL 消息为已读;
  11. 若 B 离线,消息存入 Redis 离线未读列表,用户上线一次性拉取。

群聊消息流转流程

  1. 用户发送群消息,携带 groupId;
  2. 查询该群全部成员 userId 列表;
  3. 消息投递 RabbitMQ,Redis Pub/Sub 广播;
  4. 各服务实例遍历本地在线用户,匹配群成员推送;
  5. 离线群成员存入离线消息池,上线统一拉取。

三、数据库表完整设计(可直接执行建表语句)

1. 用户聊天消息表 chat_message(核心表)

存储所有私聊、群聊消息,区分单聊 / 群聊,记录已读状态

sql

复制代码
CREATE TABLE `chat_message` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键消息ID',
  `request_id` varchar(64) NOT NULL COMMENT '客户端唯一请求ID,幂等防重复',
  `from_user_id` bigint NOT NULL COMMENT '发送人ID',
  `to_user_id` bigint DEFAULT NULL COMMENT '私聊接收人,群聊为null',
  `group_id` bigint DEFAULT NULL COMMENT '群聊ID,私聊为null',
  `msg_type` tinyint NOT NULL DEFAULT 1 COMMENT '1文本 2图片 3文件 4语音',
  `content` text NOT NULL COMMENT '消息内容',
  `is_read` tinyint NOT NULL DEFAULT 0 COMMENT '0未读 1已读',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发送时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY uk_request_id (`request_id`),
  INDEX idx_from_user (`from_user_id`),
  INDEX idx_to_user (`to_user_id`),
  INDEX idx_group (`group_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '聊天消息记录表';

2. 好友关系表 user_friend

sql

复制代码
CREATE TABLE `user_friend` (
  `id` bigint AUTO_INCREMENT PRIMARY KEY,
  `user_id` bigint NOT NULL COMMENT '用户ID',
  `friend_id` bigint NOT NULL COMMENT '好友ID',
  `remark` varchar(50) DEFAULT '' COMMENT '好友备注',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_user_friend (`user_id`,`friend_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

3. 群组表 chat_group

sql

复制代码
CREATE TABLE `chat_group` (
  `id` bigint AUTO_INCREMENT PRIMARY KEY,
  `group_name` varchar(100) NOT NULL COMMENT '群名称',
  `owner_id` bigint NOT NULL COMMENT '群主ID',
  `avatar` varchar(255) DEFAULT '' COMMENT '群头像',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

4. 群成员表 group_member

sql

复制代码
CREATE TABLE `group_member` (
  `id` bigint AUTO_INCREMENT PRIMARY KEY,
  `group_id` bigint NOT NULL,
  `user_id` bigint NOT NULL,
  `member_remark` varchar(50) DEFAULT '' COMMENT '群内昵称',
  `join_time` datetime DEFAULT CURRENT_TIMESTAMP,
  UNIQUE KEY uk_group_user (`group_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

四、Redis Key 设计规范(缓存核心逻辑)

  1. 在线用户状态 key: user:online:{userId} value: 当前 IM 服务实例 ID 过期时间 30s,客户端 25s 发送心跳自动续期,超时自动判定离线。
  2. 用户离线未读消息集合 key: user:offline:msg:{userId} Set 集合,存储 messageId 用户上线批量查询,拉取完整消息后清空集合。
  3. 群成员在线缓存 key: group:online:{groupId} Set,缓存群内在线用户 ID,减少 DB 查询。
  4. 全局消息广播频道 频道名:chat_global_channel,所有 IM 服务统一订阅。
  5. 限流计数器 key: limit:msg:{userId} 计数器,限制单用户每秒发送消息上限。

五、核心功能实现细节与代码片段

5.1 前端 Socket 连接、断线重连封装(Vue3)

安装依赖

shell

复制代码
npm install socket.io-client

封装 socket 工具类

javascript

运行

复制代码
import { io } from 'socket.io-client'

let socket = null
const SOCKET_URL = import.meta.env.VITE_SOCKET_URL

export function initSocket(token) {
  // 自动重连、重连间隔、最大重试次数
  socket = io(SOCKET_URL, {
    auth: { token },
    autoConnect: false,
    reconnection: true,
    reconnectionAttempts: 10,
    reconnectionDelay: 2000,
    transports: ['websocket']
  })

  // 手动连接
  socket.connect()

  // 连接成功
  socket.on('connect', () => {
    console.log('聊天连接成功')
  })

  // 断线重连
  socket.on('disconnect', (reason) => {
    console.log('连接断开', reason)
  })

  // 接收私聊消息
  socket.on('private_msg', (msg) => {
    // 渲染聊天列表,存储本地
  })

  // 接收群消息
  socket.on('group_msg', (msg) => {
    
  })

  // 上线拉取离线消息
  socket.on('offline_msg_list', (list) => {
    
  })

  // 心跳响应
  socket.on('pong', () => {})

  return socket
}

// 发送私聊消息
export function sendPrivateMsg(toUserId, content, requestId) {
  socket.emit('send_private', {
    toUserId,
    content,
    requestId,
    msgType: 1
  }, (ack) => {
    // 后端回执,确认发送成功
  })
}

// 心跳保活,25s一次
export function startHeartBeat() {
  setInterval(() => {
    socket.emit('ping')
  }, 25000)
}

5.2 SpringBoot 后端 Socket.IO 核心配置

引入 Maven 依赖

xml

复制代码
<dependency>
    <groupId>com.corundumstudio.socketio</groupId>
    <artifactId>netty-socketio</artifactId>
    <version>1.7.7</version>
</dependency>

Socket 服务配置类

java

运行

复制代码
@Configuration
public class SocketIoConfig {
    @Bean
    public SocketIOServer socketIOServer() {
        com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
        config.setHostname("0.0.0.0");
        config.setPort(9090);
        // 允许跨域
        config.setAllowCustomRequestsOrigin(true);
        config.setOrigin("*");
        SocketIOServer server = new SocketIOServer(config);
        return server;
    }
}

5.3 登录认证、在线状态、心跳逻辑

java

运行

复制代码
@Component
public class SocketAuthHandler {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private SocketIOServer socketIOServer;

    // 客户端连接认证
    @PostConstruct
    public void initAuth() {
        socketIOServer.addConnectListener(client -> {
            String token = client.getHandshakeData().getAuthToken().get("token").toString();
            // 校验token,获取userId
            Long userId = getUserIdByToken(token);
            if(userId == null) {
                client.disconnect();
                return;
            }
            // 绑定用户与客户端连接
            client.set("userId", userId);
            // 写入在线缓存,30秒过期
            String onlineKey = "user:online:" + userId;
            redisTemplate.opsForValue().set(onlineKey, ServerUtil.getServerId(), 30, TimeUnit.SECONDS);
        });

        // 心跳ping事件
        socketIOServer.addEventListener("ping", Void.class, (client, data, ackSender) -> {
            Long userId = client.get("userId");
            String onlineKey = "user:online:" + userId;
            // 续期30秒
            redisTemplate.expire(onlineKey, 30, TimeUnit.SECONDS);
            client.sendEvent("pong");
        });

        // 客户端断开连接
        socketIOServer.addDisconnectListener(client -> {
            Long userId = client.get("userId");
            if(userId != null) {
                redisTemplate.delete("user:online:" + userId);
            }
        });
    }
}

5.4 Redis Pub/Sub 跨节点消息广播实现

发布消息工具类

java

运行

复制代码
@Service
public class ChatPubService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    public void publishChatMsg(ChatMsgDTO dto) {
        redisTemplate.convertAndSend("chat_global_channel", dto);
    }
}

全局消息订阅监听

java

运行

复制代码
@Component
public class ChatSubListener {
    @Autowired
    private SocketIOServer socketIOServer;
    @Autowired
    private OfflineMsgService offlineMsgService;

    @RedisListener(channel = "chat_global_channel")
    public void receiveMsg(ChatMsgDTO dto) {
        // 私聊消息处理
        if(dto.getToUserId() != null) {
            boolean online = redisTemplate.hasKey("user:online:" + dto.getToUserId());
            if(online) {
                // 推送在线用户
                socketIOServer.getClientOperations().sendEvent("private_msg", dto.getToUserId().toString(), dto);
            } else {
                // 存入离线消息
                offlineMsgService.saveOfflineMsg(dto.getToUserId(), dto.getId());
            }
        }
        // 群聊逻辑省略
    }
}

六、Nginx 完整配置(WebSocket 负载均衡 + HTTPS)

6.1 http 基础配置(nginx.conf http 块)

nginx

复制代码
http {
    server_tokens off;
    # 长连接缓存
    upstream chat_server_cluster {
        server 127.0.0.1:9090 weight=1;
        server 127.0.0.1:9091 weight=1;
        ip_hash; # 同一用户固定分发同一服务,避免频繁切换连接
    }

    # 全局开启gzip
    gzip on;
    gzip_min_length 1k;
    gzip_types text/plain application/json application/javascript;
}

6.2 HTTPS 站点配置 conf.d/chat.conf

nginx

复制代码
# http 80跳转https
server {
    listen 80;
    server_name chat.xxx.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name chat.xxx.com;
    ssl_certificate /etc/nginx/ssl/chat.pem;
    ssl_certificate_key /etc/nginx/ssl/chat.key;
    ssl_protocols TLSv1.2 TLSv1.3;

    # WebSocket升级核心配置
    location /socket.io/ {
        proxy_pass http://chat_server_cluster;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_connect_timeout 300s;
        proxy_read_timeout 300s; # 长连接超时时间,必须大于心跳周期
    }
}

关键说明:proxy_read_timeout 设置 300 秒,避免无数据传输时 Nginx 主动断开长连接。

七、RabbitMQ 消息队列配置(可靠投递)

交换机与队列规划

  1. 普通聊天交换机:chat_exchange
  2. 消息持久化队列:chat_msg_queue
  3. 死信队列:chat_msg_dlq(处理失败消息重试)

消息生产消费核心规则

  1. 消息投递开启持久化,服务重启不丢失;
  2. 消费者手动 ACK,处理完成才确认删除消息;
  3. 入库失败消息转发死信队列,定时任务重试;
  4. 高并发峰值自动削峰,不会阻塞 Socket 长连接网关。

八、生产环境核心问题解决方案

8.1 跨服务用户无法互通

解决方案:Redis Pub/Sub 全局广播,所有服务实例统一订阅频道,消息全集群同步。

8.2 断线丢失消息

  1. 客户端发送消息等待后端 ACK 回执,无回执自动重发;
  2. 消息先存入 MQ 持久化,再推送用户;
  3. 离线消息存入 Redis,上线批量拉取。

8.3 消息重复推送、重复入库

每条消息携带唯一requestId,数据库设置唯一索引,重复插入直接忽略;消费 MQ 时判断消息是否已处理。

8.4 在线状态不准确、假在线

心跳 25s 上报,Redis30s 过期,客户端异常断开、服务宕机自动清除在线 key,无残留脏数据。

8.5 单服务在线连接上限

单机支撑 3-5 万在线用户,新增服务实例加入 Nginx 集群,横向扩容无上限。

8.6 前端刷新页面丢失会话

前端本地缓存 token,页面刷新自动重连 Socket,重连成功自动拉取离线消息、恢复房间。

8.7 高并发刷屏打垮数据库

  1. Redis 限流,单用户每秒最多 3 条消息;
  2. 消息异步 MQ 入库,削峰;
  3. 聊天记录分表,按月分表降低单表数据量。

九、云服务器完整部署流程

  1. 云服务器安全组放行端口:22 (ssh)、80、443、9090;
  2. 安装 Docker、Redis、RabbitMQ、MySQL;
  3. 前端打包 dist 部署 Nginx 静态页面;
  4. 后端 SpringBoot 打包 Jar,多端口启动多实例(9090、9091);
  5. 配置 Nginx 负载均衡与 WebSocket 转发;
  6. 域名解析至服务器公网 IP,上传 SSL 证书;
  7. 校验 Nginx 配置 nginx -t,重启 Nginx;
  8. 启动 IM 服务集群,测试私聊、群聊、断线重连、离线消息。

十、项目扩展优化方向

  1. 文件 / 图片上传:对接 OSS 对象存储,消息仅存储文件 URL;
  2. 音视频通话:集成 WebRTC,基于现有 Socket 做信令传输;
  3. 消息撤回、已读、多端同步;
  4. 敏感词过滤:接入词库,发送消息前拦截违规内容;
  5. 消息归档:定时任务迁移历史消息至归档分表,优化主表查询速度;
  6. 监控告警:接入 Prometheus 监控在线人数、消息 TPS、MQ 堆积、服务宕机告警。

结语

本套架构是中小企业 IM 项目标准落地方案,完全规避单机 Demo 各类缺陷,具备高并发、高可用、消息可靠、集群扩容能力。开发顺序建议:先完成 Socket 连接与心跳在线状态 → 实现 Redis Pub/Sub 跨节点通信 → MQ 异步消息持久化 → 离线消息处理 → Nginx 集群部署。所有代码、SQL、Nginx 配置可直接复制投入项目开发,适配私有化部署、小程序、Web 后台多端聊天场景。