JVxeTable 子表字典回显请求优化:前端请求合并 + Redis 短缓存兜底

JVxeTable 子表字典回显请求优化:前端请求合并 + Redis 短缓存兜底
SOIL1900一、问题现象
QMS 质量管理系统中,检验任务表单(来料 / 制程 / 成品 / 出货检验等)的子表基于 j-vxe-table(vxe-table 4.x)渲染,多个列使用 selectDictSearch 表字典搜索下拉组件(检验规范、工序、检验站、量具等),单元格存储的是字典表主键 id,展示时需要实时翻译成文字。
实际运行中发现:
- 表格每滚动一次,每个有值的单元格都会发起一次字典翻译 HTTP 请求(如
/sys/dict/loadDictItemNotFilter/specification_p,expression,id?key=xxx),且每次都穿透到数据库; - 以 50 行数据、3 个字典列为例,一次打开 / 滚动到底即产生 约 150 个后端请求;行数更多时,虚拟滚动回收单元格还会导致请求反复爆发;
- 大量并发请求造成后端接口与数据库压力陡增,页面打开伴随明显的请求风暴。
二、问题原因
逐层排查后定位为四个叠加因素:
- 渲染机制触发重复查询:单元格组件通过
watch(innerValue, { immediate: true })在挂载时发起翻译请求。JVxeTable 默认开启纵向虚拟滚动(scrollY: { enabled: true },行数超过阈值后生效),滚动过程中单元格被回收 / 重建,每次重新挂载都会再次发起请求;即使未触发虚拟滚动,首次渲染也会一次性发出”行数 × 列数”个请求。 - 组件自带的缓存处于失效状态:组件内原本设计了
LabelMap内存缓存用于防重复查询,但写入逻辑因历史 bug 被注释停用,缓存永远为空,导致每次挂载必然发请求。 - 历史 bug 的根因(缓存被停用的原因):
- 后端翻译接口在 key 查不到数据时,会把 key 本身(即 id)当作文本返回(
delNotExist=false语义)。旧的缓存写入未过滤这种”污染数据”,text=id被写入模块级缓存后会话期内永不过期,表现为”单元格一直显示 id 而不是文字”; - 缓存 key 只用了值本身、未区分字典来源,不同字典表的相同 id 会互相污染;
- 防抖用的
requestId是模块级全局变量,所有单元格共享,并发搜索时互相作废对方响应,进一步造成”显示原始 id”的假象。
- 后端翻译接口在 key 查不到数据时,会把 key 本身(即 id)当作文本返回(
- 后端无任何缓存:表字典翻译接口(
queryTableDictByKeys系列)每次直查数据库,代码注释声称有 Redis 缓存但实际未实现。
三、实现方案
整体思路:前端把”N 行 × M 列”的碎片化请求合并为”每列一个”的批量请求,并用带 TTL 的内存缓存消除重复;后端加按 key 的 Redis 短缓存吸收残余并发,两层均严格规避”缓存污染”问题。
3.1 前端:批量合并 + 缓存重建(组件层统一解决)
改动集中在 JVxeSelectDictSearchCell.ts 一个文件,全部 53 个使用该组件的页面自动受益,业务表单零改动:
- 微任务批量合并:同一渲染波次挂载的单元格不再立即发请求,而是按字典定义分组进入批量队列,延迟到下一个宏任务统一冲刷——每个字典列合并为一个请求(后端接口原生支持逗号分隔多 key、按 key 顺序返回),单批超过 100 个 key 自动分片防止 URL 超长;
- in-flight 请求去重:进行中的相同
dict+value请求直接复用 Promise,相邻渲染波次不会重复发起; - 缓存重建(根治历史 bug):
- 缓存 key 改为
dict::value完整格式,不同字典定义(含 where 条件、筛选条件)彻底隔离; - 建立”三不缓存”规则:后端查不到回显的 key(
text === value)、空文本、多值一律不写入缓存,从机制上杜绝”id 显示被固化”的问题复现; - 缓存条目增加 10 秒 TTL,过期读取时就地删除并自动走批量链路重查(有合并兜底,过期瞬间也不会爆发请求),兼顾请求量与数据新鲜度;
- 下拉搜索结果同步写入缓存,用户选过的值后续回显零请求;
- 缓存 key 改为
- 竞态修复:
requestId由模块级改为组件实例级,消除多单元格并发搜索互相作废响应的问题。
3.2 后端:按 key 的 Redis 兜底短缓存(TTL = 3 秒)
改动集中在 SysDictServiceImpl 的两个表字典翻译方法(queryTableDictByKeys / queryTableDictByKeysNotFilter):
- 缓存粒度为单个 key 而非整个请求:不同请求的 key 组合可互相命中,3 秒内任何用户、任何页面引用同一字典项均可复用;
- 读取流程:先按 key 批量查 Redis → 仅未命中的 key 用一条
IN (...)查库 → 按原有语义组装返回(保持入参顺序、delNotExist=false时查不到回显 key 等行为完全不变); - 缓存 key 设计:
sys:cache:dictTable::{table,text,code}[|where:...]::{字典项key},由字典三段 + where 条件唯一确定字典定义,且在 SQL 注入校验与转义之后构造; - 三不缓存 + 降级保护:查不到的 key 不缓存(防止”回显 id”被固化)、空文本不缓存、Redis 异常时 try-catch 自动降级为纯查库——兜底层只允许减性能,不允许减功能;
- 3 秒 TTL 的定位是削峰而非持久缓存:自然过期、无需任何主动失效逻辑,对字典表的增删改零侵入,一致性成本约等于零。
3.3 兼容性保障
- 4 段式字典(
table,text,code,筛选条件)与table where ...风格均验证兼容:前端按完整 dict 字符串隔离缓存与批量队列;后端回显链路对筛选条件的处理保持既有行为; - 多值(含逗号)场景自动降级为原有单请求路径,不破坏 key↔text 的位置对应关系;
- 接口的出入参契约、返回顺序、边界语义与优化前完全一致,属于纯性能优化,无行为变更。
四、最终效果对比
以 50 行数据 × 3 个字典列为例:
| 场景 | 优化前 | 优化后 |
|---|---|---|
| 首次打开表单请求数 | 约 150 个 | 3 个(每字典列 1 个批量请求) |
| 滚动 / 来回滚动 | 请求反复爆发 | 0 个(命中内存缓存) |
| 关闭弹窗后重开 | 再次约 150 个 | 0 个(10 秒缓存期内) |
| 缓存过期后再次触发 | 仍是每行一个请求 | 自动合并为每列 1 个请求 |
| 3 秒内多人 / 多标签页重复打开 | 每次都查库 | 命中 Redis,0 次 DB 查询 |
| 数据库查询次数(首次打开) | 约 150 次单行查询 | 3 次 IN(...) 批量查询 |
| 受益范围 | — | 全项目 53 个使用 selectDictSearch 的页面自动生效,后续新增页面免维护 |
五、技术亮点
- 系统性根因分析:从”滚动重复请求”表象出发,逐层定位到虚拟滚动单元格重建、缓存写入被停用、缓存污染(查不到回显 key)、全局请求 id 竞态四个叠加根因,并逐一修复;
- 请求合并模型设计:设计”批量队列 + 宏任务冲刷 + in-flight 去重 + 带 TTL 的防污染缓存”四层机制,将请求复杂度从 O(行数 × 列数) 降为 O(字典列数);
- 组件层收口:方案落在基础组件内部,相比逐表单挂钩子的方式,一次改动覆盖全部 53 个页面且对未来新增页面免疫,业务代码零侵入;
- 缓存安全性设计:前后端一致的”不缓存查不到的 key”原则,从机制上根治”缓存导致显示 id”的历史 bug,而不是简单绕过;
- 兜底层的健壮性:Redis 短缓存定位为削峰(TTL 3 秒、无主动失效、零侵入),配合异常降级,保证缓存层故障时功能完全不受影响。
评论
匿名评论






