聊天系统设计
概述
聊天是游戏最活跃的系统之一:世界频道、房间对局聊天、私聊、公会频道。聊天消息量大、实时性要求高、并发集中(活动时全服刷屏)。本文设计完整的聊天系统:频道划分(世界/房间/私聊/公会/系统)、基于消息队列(Kafka/RocketMQ)的聊天架构、敏感词过滤、消息频率限制。
一、频道划分
1.1 频道类型
| 频道 | 可见范围 | 实时性 | 量级 |
|---|---|---|---|
| 世界 | 全服 | 高 | 极大 |
| 房间 | 房间成员 | 高 | 小 |
| 私聊 | 两人 | 高 | 小 |
| 公会 | 公会成员 | 中 | 中 |
| 系统 | 全服/定向 | 低 | 小 |
频道隔离:
不同频道独立路由与限流
世界频道 → 广播压力最大,重点设计
私聊 → 点对点,走直达通道1.2 频道语义
世界频道:
全体在线玩家可见(离线不补)
需要限流(防止刷屏)
可分区(按服务器/按线路)
房间频道:
对局内聊天(配合表情/快捷语)
复用房间广播通道
私聊:
点对点实时投递
离线消息(邮件/推送兜底)
公会频道:
公会成员在线可见
低频 → 复用公会广播频道开关与控制:
全局禁言(运营)
频道级禁言(世界频道关闭)
玩家级禁言(处罚)
频控在网关/聊天服务双重把关二、聊天架构
2.1 直连 vs 消息队列
小规模直连架构:
客户端 → 网关 → 聊天服务 → 直接广播
优点:简单、低延迟
缺点:全服广播阻塞单点
大规模消息队列架构:
客户端 → 网关 → 聊天服务(写入 MQ)
→ MQ 分区 → 各广播节点消费 → 推送
优点:削峰、解耦、可扩展
缺点:增加一跳延迟(可接受)2.2 MQ 聊天架构
消息流(RocketMQ / Kafka):
发送方:
客户端发消息 → 网关校验(频控/敏感词)
→ 写入聊天 Topic(带频道与分区键)
广播方:
广播节点消费 Topic → 组装 → 推送
世界频道按服务器分区消费
私聊按 receiver 分区(直达)
Topic 设计:
chat-world 世界频道(分区按服务器)
chat-room 房间频道(分区按房间)
chat-guild 公会频道(分区按公会)
chat-private 私聊(分区按接收者)分区键(顺序保证):
房间频道 → 按 roomId 分区(房间内有序)
私聊 → 按会话双方哈希(两人有序)
世界 → 按服务器分区(无顺序要求)2.3 离线与历史
离线消息:
私聊/公会 → 未读入库,上线拉取
世界/房间 → 不补(时效性内容)
历史消息:
私聊/公会 → 存库可查(聊天记录)
世界 → 一般只保留滚动窗口
存储:MySQL / MongoDB / 消息平台(按量级)三、敏感词过滤
3.1 过滤链路
过滤时机(发送时过滤,入库前):
网关/聊天服务入口 → 敏感词检测
命中 → 替换(*)或拒绝(按策略)
常用算法:
DFA(确定有限自动机):
构建敏感词树,O(N) 扫描
命中可获取"起始/结束位置"
AC 自动机:
多模式匹配,一次扫描命中所有词
适合敏感词表大、要求快的场景3.2 过滤策略
替换策略:
命中词替换为 *(保留消息长度)
例:"你好xxx" → "你好***"
体验友好,玩家可自查
拒绝策略:
直接拒绝发送(提示"消息包含违规内容")
力度大,用于高敏词(政治/诈骗)
分级过滤:
高敏词 → 拒绝 + 记录(风控)
一般词 → 替换
组合:高敏拒绝、低敏替换java
public class SensitiveWordFilter {
private final AhoCorasick automaton;
// 过滤:返回处理后的文本与命中信息
public FilterResult filter(String text) {
List<Match> matches = automaton.match(text);
// 按词表等级处理
boolean hasHighRisk = matches.stream().anyMatch(m -> m.getLevel() == HIGH);
if (hasHighRisk) {
return FilterResult.reject(matches); // 拒绝
}
return FilterResult.mask(matches); // 替换 *
}
}敏感词表管理:
词表集中配置(运营可维护)
分版本更新(灰度加载)
词表加载与命中性能监控四、消息频率限制
4.1 限频维度
玩家级:
世界频道 N 条/秒、M 条/分钟
私聊 N 条/分钟
公会 N 条/分钟
频道级:
世界频道整体流速(防止刷屏风暴)
系统公告独立通道(不受玩家限流)
场景联动:
活动期间放宽 / 收紧
禁言状态覆盖限流4.2 实现
限流实现(Redis INCR + TTL,见第 3 周):
key = chat:world:{playerId}:{minute}
INCR → 首次 EXPIRE 60s
超过阈值 → 拒绝 + 提示"发言太快"
更平滑:
令牌桶(Redis Lua 实现)
每次发消息消耗令牌
补充速率平滑(突发与长跑平衡)java
public class ChatRateLimiter {
private final RedisTemplate<String, String> redis;
public boolean allowWorldChat(long playerId) {
String key = "chat:world:" + playerId + ":" + minuteOf();
Long count = redis.opsForValue().increment(key);
if (count == 1) {
redis.expire(key, Duration.ofSeconds(60));
}
return count <= 20; // 每分钟 20 条
}
}限流策略:
普通玩家阈值 + 高等级/VIP 阈值
连续超限 → 临时禁言(10 分钟)
禁言实现:Redis 禁言标记,发送前检查五、聊天消息完整流程
一次世界聊天:
1. 客户端发消息(channel + content)
2. 网关校验:登录、禁言、频控
3. 敏感词过滤(替换/拒绝)
4. 写入 MQ(chat-world Topic)
5. 广播节点消费 → 推送给在线玩家
6. 客户端展示(带发言人信息)
审计:
关键消息/被拒消息记录日志
私聊/公会历史入库
全链路 TraceId六、常见问题
| 问题 | 处理 |
|---|---|
| 世界频道刷屏 | 玩家限流 + 频道流速控制 |
| 敏感词绕过(谐音) | 词库维护 + 拼音/变形匹配 |
| 广播延迟高 | MQ 分区消费 + 推送池 |
| 消息顺序乱 | 分区键保证频道内有序 |
| 离线消息丢失 | 私聊入库 + 上线补拉 |
七、小结
聊天系统的架构核心是"频道隔离 + MQ 削峰":频道按可见范围划分(世界/房间/私聊/公会/系统),各自独立限流与路由;消息经网关校验后写入 RocketMQ/Kafka 的频道 Topic,按分区键保证房间内与私聊顺序,广播节点消费推送;敏感词过滤用 AC 自动机做多模式匹配,高敏词拒绝、一般词替换;频率限制以 Redis INCR/令牌桶按玩家与频道双重把关。聊天看似简单,但"抗刷、过滤、扩展"三者齐备,才能在全服活动时稳如磐石。