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 说话,冷数据只在后台搬运——这套结构撑得起十人小队,也撑得住千人直播间。
