4.1 白板持久化:Redis 与 COS 分工

持久化第一刀:明确「谁存什么」。白板数据分热、冷两层,Redis 管热(在线房间状态),COS 管冷(快照、导出、日志归档)。本篇定下这条分工,避免「全塞数据库」把在线服务拖垮。

三类数据存哪

数据 特性 存哪 原因
在线房间状态 热、读多 Redis 快、支持集合
事件日志 高频写、需回溯 热在 Redis 流 / 冷在 COS 分级
快照 / 导出 低频写、读多 COS 大对象、可 CDN
房间元信息 低频 数据库 事务、查询

核心:热数据离在线服务近,冷数据下沉对象存储。

Redis 存什么、怎么存

用途 结构 说明
在线成员 Set(SADD/SREM) 进出房、计数
房间最新态 Hash 新成员进房补齐
跨节点广播 Pub/Sub 多节点房间共享
热日志 List / Stream 近期增量

Redis 是「在线协同」的共享大脑,多节点一致性靠它。

快照与日志归档为什么走 COS

白板画三天可能上万笔,全在数据库里查回放都会被撑爆;把「某时刻整板」渲染成图片放 COS,「近期日志」定时合并归档到 COS: | 数据 | 冷存动作 | 收益 | |---|---|---| | 快照 | 渲染图 → COS | 快速恢复、做封面 | | 历史日志 | 合并归档 → COS | 数据库减压 | | 导出资源 | 图 / 视频 → COS | 走 CDN 加速 |

COS 是「大对象 + 读多」的天然归宿,把重活从在线链路摘出去。

热冷切换的边界

阶段 数据在 触发切换
在线协作中 Redis 热 房间关闭 / 超时
刚结束 Redis 转冷存 定期合并
长期 COS 归档 按保留期

关键是「热冷有边界、边界自动切」:在线靠 Redis,收尾转 COS,绝不让人手动搬。

踩坑提示

最坑是「在线服务直接查 COS / 数据库取最新态」:快照在 COS,但「此刻房间最新画面」要在 Redis——混了就会让每个请求都打对象存储,延迟爆炸。定死「在线读 Redis、冷读 COS」,恢复时先 COS 快照再加 Redis 增量,路径清晰不串。

小结

持久化分工 = Redis 存热(房间状态 / 成员 / 广播 / 热日志)、COS 存冷(快照 / 归档 / 导出)、数据库存元信息。热冷有界、自动切换,白板才存得住、也才扩得动。

为什么热数据不直接进数据库

新人最容易问的问题:MySQL 也能存房间状态,为什么不直接写库?三个理由: | 理由 | 说明 | |---|---| | 写入频率 | 一笔线拆成几十个点,千人房间每秒上万次写 | | 广播语义 | 数据库没有 Pub/Sub,通知还得另搭一层 | | 恢复语义 | 在线态本来就可以丢——真相在快照里 |

「可以丢」不等于「随便丢」:热数据丢失的前提是冷侧有快照能接上,这正是「快照 + 增量」配套的用意。

房间状态怎么落地:键设计示例

场景:千人协作白板,成员频繁进出,每个人进房第一眼都要看到「谁在线、板画到哪」。每次都查数据库,在线链路必然被拖垮;把热状态放进 Redis,读写都在毫秒级。键的设计围绕「房间」这一主体展开: | 键 | 类型 | 存什么 | 生命周期 | |---|---|---|---| | room:members:<roomId> | Set | 在线成员 id | 关房时清 | | room:state:<roomId> | Hash | 画布最新摘要 | 快照后瘦身 | | room:seq:<roomId> | String | 当前事件序号 | 单调递增 | | room:log:<roomId> | Stream | 近期事件增量 | 归档后裁剪 |

服务端操作可以精简为两步:进房登记、先存后广播:

// 进房:登记成员并补齐最新态
await redis.sadd(`room:members:${roomId}`, userId);
const state = await redis.hgetall(`room:state:${roomId}`);
socket.emit('sync', { seq: await redis.get(`room:seq:${roomId}`), state });

// 广播:先写事件流再发布,保证先存后到
await redis.xadd(`room:log:${roomId}`, '*', 'op', JSON.stringify(event));
redis.publish(`room:pub:${roomId}`, JSON.stringify(event));

要点是「先写流再发布」:先落 Stream,再走 Pub/Sub 广播。迟来的成员可以从流里补增量,而不是到处问人「刚才画了啥」。

冷热切换什么时候做

切换不是手动搬,而是「定时任务 + 事件钩子」双管齐下: | 触发 | 动作 | 谁来做 | |---|---|---| | 空房超时 | 渲染快照写 COS、清 Redis 热 key | 定时扫描器 | | 主动关房 | 先打里程碑快照再归档 | 关房钩子 | | 每日凌晨 | 合并历史日志、裁剪过期增量 | 归档任务 | | 快照生成后 | 更新数据库元信息指向新快照 | 快照服务 |

边界参数写进配置,别硬编码在逻辑深处: | 参数 | 建议值 | 说明 | |---|---|---| | 空房超时 | 30 分钟 | 超时触发转冷 | | 快照保留 | 最近 30 天 | 旧的降级 | | 日志保留 | 快照之间增量 | 锚点外可裁 |

多节点下的状态共享

服务扩到多机后,「同一房间的两人连在不同机器」是常态。此时 Redis 是唯一的共享层:Set 存全房成员、Pub/Sub 做跨机广播,每台机器只订阅自己房间所在频道。数据库没有「订阅」语义,对象存储又太慢,所以分工里还有第三条隐含规则:跨节点一致性,只交给 Redis。

监控盯着哪几个指标

热冷两层各盯一个核心数: | 层 | 指标 | 告警建议 | |---|---|---| | Redis | 房间 key 内存、Pub/Sub 积压 | 内存超 70% | | 归档任务 | 快照生成耗时、失败率 | 失败率 > 2% | | COS | 快照读取延迟、404 率 | 命中率 < 90% |

再补一个坑:Redis 会重启

把 Redis 当永久存储是最隐蔽的坑——重启后房间状态全丢。Redis 在这里是缓存,不是真相:真相是 COS 快照加数据库元信息。故障后房间状态应能靠「快照 + 增量日志」重建,这条退路一旦想通,整个分工就不会反人类。

存快照和归档日志都离不开对象存储,新项目落地可以趁 腾讯云云产品合集特惠 囤一波 COS 存储包,冷数据按量付费很省。

事件日志的热冷分级

事件日志是最容易两头不靠的数据:全放 Redis,内存吃不消;全直接写 COS,在线回放又太慢。分级做法是: | 层 | 内容 | 写入方式 | |---|---|---| | 热层(Redis Stream) | 最近 N 分钟或最近 M 条 | 逐条写,支撑在线回放 | | 冷层(COS) | 合并后的历史段 | 定时打包上传 | | 淘汰 | 超出热层窗口自动裁剪 | 只动热层,不碰冷侧 |

热层窗口按「快照间隔」反推:至少要盖住两张快照之间的增量,恢复链路才不会断档。

上线前自检清单

分工做得对不对,上线前照着过一遍: | 检查项 | 通过标准 | |---|---| | 在线读路径 | 全程不碰 COS 和数据库 | | 恢复路径 | 5 秒内出快照 + 增量 | | 关房链路 | 自动转冷,无人工介入 | | 快照可重建 | 删掉 Redis 重启一次,房间仍能恢复 | | 账单可控 | 快照降级和日志裁剪规则生效 |

清单里最容易漏的是第四条:不亲手重启一次 Redis,你永远不知道恢复链路能不能跑通。

一句话回顾分工

Redis 管「此刻」,COS 管「曾经」,数据库管「名录」。三者各司其职,在线链路只和 Redis 说话,冷数据只在后台搬运——这套结构撑得起十人小队,也撑得住千人直播间。

延伸阅读