1051 字
3 分钟
中后台系统重构实践:IndexedDB 数据版本门禁缓存

这篇文章聚焦一件事:
在中后台实时场景里,如何把 IndexedDB 从“简单本地存储”升级成“带版本门禁的缓存层”。

核心结论先给出来:

  • id + key + list 建立版本门禁缓存协议
  • 前端先比版本,命中读本地,未命中回源并更新
  • 通过该机制把高频字典请求拦截在本地,提升首屏与切页体验

背景:为什么要重构这层#

中后台的实时页面通常有两个冲突:

  • 一边是高频事件流(通话、转写、告警、状态变更)
  • 一边是频繁页面切换(详情、审核、工单、报表)

只靠内存态会丢上下文;全塞进重型全局状态又会导致耦合高、排障难。
所以我在重构里先把“历史/配置数据层”独立出来,专门由 IndexedDB 承接。

方案全景#

flowchart LR API[后端接口] --> VER[获取当前版本 key] VER --> HIT{本地 key 是否命中?} HIT -->|是| IDB[直接返回 IndexedDB list] HIT -->|否| NET[请求最新数据] NET --> SAVE[写回 IndexedDB: id/key/list] SAVE --> IDB IDB --> UI[页面渲染]

这张图对应的是“版本门禁 + 读穿缓存”单主线:

  • 读取优先本地命中
  • 版本不一致才回源
  • 回源结果立即写回本地,供后续命中

一、IndexedDB:从“缓存”升级为“版本门禁缓存”#

项目里的 IDB 封装很薄(openDB / put / cursor / delete),但关键不在 API 包装,而在 缓存协议

1) 缓存记录结构#

对字典类数据,统一按这三个字段存:

  • id:数据集标识(例如某个下拉选项集合)
  • key:版本键(后端版本号或前端失效键)
  • list:实际数据

也就是你说的这套机制:

  1. 先拿“当前数据版本”
  2. 读本地记录并比较 record.key === currentKey
  3. 命中就直接用本地;不命中就请求后端并更新本地

这其实是标准的 read-through cache(读穿缓存),区别在于它是前端本地版本门禁。

2) key 的来源策略#

key 不一定只能是后端 dataVersion,但优先级建议如下:

  1. 后端版本号 / etag(最优):语义清晰,可控
  2. 业务批次号:配置变更时手动切版本
  3. 时间键(如 YYYY-MM-DD):适合日更字典兜底

经验上,后端有明确版本号时,前端逻辑会简单非常多:
“比较版本 -> 决定是否回源”,几乎不需要猜测过期条件。

3) 实时数据仓与索引#

转写类数据仓除了主键,还要建 callId 索引。
否则回放或按通话聚合时会退化成全量扫描,数据量一上来就卡。

在实时写入侧我只持久化“稳定片段”(结束态),而不是每个中间增量,原因很直接:

  • 降低写放大
  • 避免历史回放出现大量中间脏版本

4) 这层的工程边界#

当前这类原生 IDB 封装常见短板也要正视:

  • schema 升级缺少统一迁移函数
  • 读事务误用 readwrite
  • 缺少 TTL/容量治理

所以重构不是“能存就完事”,而是至少要补上:

  • 版本迁移策略
  • 容量裁剪策略
  • 错误可观测(命中率、回源率、失败率)

二、这次重构最有价值的点#

从结果看,这套方案的收益不是“技术栈更新”,而是数据链路更可控:

  • 页面切换后不再频繁丢上下文
  • 高频字典请求被版本门禁拦住
  • 实时响应和历史回放职责清晰

它不追求最炫,而是追求“复杂业务里长期稳定”。

三、后续演进建议#

  • 给 IDB 增加统一 schema migration
  • 给版本缓存增加命中率埋点
  • 回放查询按 callId 分段分页,避免全量游标

如果再做一次同类重构,我依然会优先做这层版本缓存:
先把“本地命中与回源边界”定义清楚,后续性能与稳定性会容易很多。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

中后台系统重构实践:IndexedDB 数据版本门禁缓存
https://kongdf.com/posts/learning/projects/crm-indexeddb-light-subscribe/
作者
孔大夫
发布于
2026-04-25
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时