WebSocket如何实现离线消息存储 WebSocket消息缓存处理方案【设计】

胖婷酱_2810

胖婷酱_2810

2026-06-04

782人浏览

原创

离线消息依赖服务端缓存与客户端协同,而非websocket自身:需用redis跨实例存储带msg_id和ttl的消息队列,前端localstorage暂存pending消息并支持回滚,服务端结合user+time窗口幂等校验,各环节须有降级兜底。

websocket如何实现离线消息存储 websocket消息缓存处理方案【设计】

离线消息不是靠 WebSocket 自身实现的,而是靠“服务端主动缓存 + 客户端状态协同”完成的。WebSocket 协议本身不提供存储、确认、重试能力,强行在 WebSocketHandler 里用内存 Map 存消息,重启就丢、多实例就乱、过期要手写清理——这根本不是方案,是临时补丁。

为什么不能只依赖 WebSocket 的 onMessage/onClose 做离线逻辑

很多人一上来就在 handleTextMessage 里加 if (session.isOpen()) { send() } else { cacheInMap() },结果上线就翻车:

  • 应用重启后,ConcurrentHashMap 里所有离线消息清零,用户上线再也收不到
  • 集群部署时,A 实例缓存了用户 X 的消息,但用户 X 下次连到 B 实例,B 根本查不到那条缓存
  • onClose 触发时机不可靠:页面刷新、网络抖动、浏览器崩溃都会触发,但你无法区分是“用户主动退出”还是“意外断开”
  • 没有消息幂等标识,重连后重复投递,前端展示两条一样的通知

Redis 存离线消息必须选对数据结构和 TTL

用 Redis 不是图个名字新潮,是为了解决三个硬需求:跨实例共享、自动过期、原子读写。但乱用 StringHash 会埋坑:

Yellow.ai
Yellow.ai

Yellow.ai是一款面向客户体验和员工体验自动化的企业级 Agentic AI 平台。

下载
  • 推荐用 LPUSH + BRPOPZSET(按时间戳排序)存用户离线队列,避免单 key 膨胀;不要用 String 存整个消息列表,扩容和分页都困难
  • 每条消息必须带唯一 msg_idexpireAt 字段,Redis 写入时用 EXPIREAT 指定绝对过期时间,别依赖客户端轮询清理
  • 序列化统一用 Jackson2JsonRedisSerializer,禁用 JdkSerializationRedisSerializer——后者序列化体积大、跨语言不兼容、反序列化失败直接报 ClassNotFoundException
  • 用户上线拉取离线消息后,必须用 LTRIMZREMRANGEBYRANK 清空已读部分,不能只 DEL 整个 key,否则并发拉取会丢消息

前端 pendingQueue 必须和连接状态解耦,且支持 localStorage 回滚

用户刷新页面后,内存里的 pendingQueue = [] 就没了,正在发送但没 ACK 的消息直接人间蒸发。可靠做法是:

  • 封装 sendMessage() 函数,所有调用都走它;发送前先 localStorage.setItem('pending_' + userId, JSON.stringify(msg)),再尝试 ws.send()
  • 页面加载时,检查 localStorage 是否有未 ACK 消息,有则重建队列并标记 isFromStorage: true,避免和新输入消息混淆
  • 收到服务端 ACK(比如 {msg_id: 'xxx', status: 'ok'})后,同步 localStorage.removeItem() 对应条目;失败时记录重试次数,超 3 次降级为 toast 提示“发送失败,请重试”
  • 注意 localStorage 单条上限约 5MB,但实际建议单条控制在 100KB 内;超长文本提前截断或转存 CDN,只留 ID 在本地

服务端幂等校验不能只靠 msg_id,得结合用户+时间窗口

只校验 msg_id 唯一性,遇到客户端时间回拨、ID 生成算法冲突,照样重复消费。生产环境要叠加维度:

  • Redis 中用 SETNX user:123:msg:abc123 做首次接收标记,TTL 设为 24 小时(覆盖最大业务容忍延迟)
  • 同时检查该用户近 5 分钟内是否已有同类型操作(如“支付通知”),用 ZCOUNT user:123:pay_events -inf +inf 防刷
  • ACK 响应体必须包含服务端生成的 server_seq,前端可据此判断是否为重复响应,避免 UI 二次渲染
  • 日志中固定打印 msg_id + server_seq + recv_time 三元组,出问题时能快速定位是客户端重发、还是服务端误判

真正难的不是代码怎么写,而是每个环节都要回答一个问题:“如果这一步失败了,下游还能继续吗?”——比如 Redis 挂了,离线消息存不到,至少要降级写 MySQL;localStorage 写满,就退回到内存队列;服务端幂等漏判一次,前端 UI 得靠 msg_id 去重而不是傻等服务端修复。可靠性不在某一行代码,而在所有 fallback 路径都真实跑通过。

相关专题

更多
C语言变量命名
C语言变量命名

c语言变量名规则是:1、变量名以英文字母开头;2、变量名中的字母是区分大小写的;3、变量名不能是关键字;4、变量名中不能包含空格、标点符号和类型说明符。php中文网还提供c语言变量的相关下载、相关课程等内容,供大家免费下载使用。

2023.06.20

2609

3

c语言入门自学零基础
c语言入门自学零基础

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,本专题为大家c语言入门自学零基础的相关文章,以及相关课程,感兴趣的朋友千万不要错过了。

2023.07.25

2108

9

c语言运算符的优先级顺序
c语言运算符的优先级顺序

c语言运算符的优先级顺序是括号运算符 > 一元运算符 > 算术运算符 > 移位运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符 > 逗号运算符。本专题为大家提供c语言运算符相关的各种文章、以及下载和课程。

2023.08.02

1080

5

c语言数据结构
c语言数据结构

数据结构是指将数据按照一定的方式组织和存储的方法。它是计算机科学中的重要概念,用来描述和解决实际问题中的数据组织和处理问题。数据结构可以分为线性结构和非线性结构。线性结构包括数组、链表、堆栈和队列等,而非线性结构包括树和图等。php中文网给大家带来了相关的教程以及文章,欢迎大家前来学习阅读。

2023.08.09

1018

4

c语言random函数用法
c语言random函数用法

c语言random函数用法:1、random.random,随机生成(0,1)之间的浮点数;2、random.randint,随机生成在范围之内的整数,两个参数分别表示上限和下限;3、random.randrange,在指定范围内,按指定基数递增的集合中获得一个随机数;4、random.choice,从序列中随机抽选一个数;5、random.shuffle,随机排序。

2023.09.05

1256

5

c语言const用法
c语言const用法

const是关键字,可以用于声明常量、函数参数中的const修饰符、const修饰函数返回值、const修饰指针。详细介绍:1、声明常量,const关键字可用于声明常量,常量的值在程序运行期间不可修改,常量可以是基本数据类型,如整数、浮点数、字符等,也可是自定义的数据类型;2、函数参数中的const修饰符,const关键字可用于函数的参数中,表示该参数在函数内部不可修改等等。

2023.09.20

1918

7

c语言get函数的用法
c语言get函数的用法

get函数是一个用于从输入流中获取字符的函数。可以从键盘、文件或其他输入设备中读取字符,并将其存储在指定的变量中。本文介绍了get函数的用法以及一些相关的注意事项。希望这篇文章能够帮助你更好地理解和使用get函数 。

2023.09.20

2920

8

c数组初始化的方法
c数组初始化的方法

c语言数组初始化的方法有直接赋值法、不完全初始化法、省略数组长度法和二维数组初始化法。详细介绍:1、直接赋值法,这种方法可以直接将数组的值进行初始化;2、不完全初始化法,。这种方法可以在一定程度上节省内存空间;3、省略数组长度法,这种方法可以让编译器自动计算数组的长度;4、二维数组初始化法等等。

2023.09.22

12615

6

c语言中null和NULL的区别
c语言中null和NULL的区别

c语言中null和NULL的区别是:null是C语言中的一个宏定义,通常用来表示一个空指针,可以用于初始化指针变量,或者在条件语句中判断指针是否为空;NULL是C语言中的一个预定义常量,通常用来表示一个空值,用于表示一个空的指针、空的指针数组或者空的结构体指针。

2023.09.22

509

3

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
热门推荐
/
最新课程
phpStudy极速入门视频教程
phpStudy极速入门视频教程

共6课时 | 54.6万人学习

独孤九贱(4)_PHP视频教程
独孤九贱(4)_PHP视频教程

共89课时 | 133.2万人学习