fix: MKH-BOM 修复在途旧查询回写缓存,导致「同步最新数据」点完仍是旧数据 #20

Merged
yanhui merged 1 commits from codex/mkh-cache-stale-write-fix-0922 into main 2026-09-22 23:58:05 +08:00
Collaborator

目的与范围

目的:修复「点『同步最新数据』后,页面 2~10 分钟内仍显示旧数据」的问题(业务表现为「像在用缓存」)。

范围:仅 backend/src/services/mkhBomService.js 的缓存写入路径 + 对应单测,无接口字段删除、无口径变更。

根因

本页 10 个进程内缓存都是同一写法:

xxxCache = { pending, value: null, expiresAt: 0 };   // 发起查询
const value = await pending;
xxxCache = { pending: null, value, expiresAt: ... }; // ← 无条件写回

若某个查询 in-flight 期间用户点了「同步最新数据」(clearMkhBomCache() 清空全部缓存),
那个发起于清缓存之前的查询读到的仍是旧快照,返回时无条件把旧值写回缓存,
覆盖掉清缓存后新请求填入的新值 —— 谁后完成谁生效。

后果被放大:清缓存不但没生效,反而把旧数据「钉」在缓存里 2~10 分钟(各缓存 TTL),
用户看到的就是「点了同步还是旧数据」。并发越高越容易撞上(多人同时使用、或先点「刷新数据」
再点「同步最新数据」)。

修法:缓存代际 cacheEpoch

  • clearMkhBomCache() 先自增代际,再清各缓存;
  • 每个查询发起时记下当时的代际,写回缓存前比对:代际对不上 ⇒ 中途被清过缓存 ⇒
    本次结果只返回给发起它的那个请求,不写缓存。

覆盖 7 个变量型缓存(officialBomIndex / businessBom / productBom / mkhProduct /
workOrderIndex / orderIndex / replenishIndex)与 3 个按 key 的 Map 缓存
(officialBomDetail / componentLocation / componentStock)。

POST /api/mkh-bom/sync 响应新增 cacheEpoch 字段(纯新增,便于线上核对清缓存是否生效)。

同步与兼容性

  • 基于 origin/main = 4946018,git rev-list --count HEAD..origin/main = 0。
  • 不改任何 SQL、不改取数口径、不改返回字段语义;clearMkhBomCache() 返回对象仅多一个键。
  • 过期查询结果仍照常返回给发起它的请求(不改变单个请求自身的语义),只影响是否写缓存。

验证

  1. 复现测试(对照实验):新增用例「清缓存后在途的旧查询结果不得写回缓存」。
    对照组(临时去掉写回保护)实测报 expected: 1 / actual: 9 —— 正是「点同步后拿到旧值」;
    加保护后通过。另加「同代际内缓存照常命中」回归保护用例。
  2. 后端全量:node scripts/check-syntax.js 通过;node --test test/*.test.js
    340 用例 / 339 通过,唯一失败为环境固有的 resilientSecretStore Windows 权限断言(与基线一致)。
  3. 本地端到端:预热 24.3s → 缓存命中 58ms;POST /sync 返回 cacheEpoch=1;
    清缓存后冷查 8.5s(确实重查库);再查 37ms(缓存重新生效);前后数据指纹一致。

风险与回退

  • 风险:低。改动只在「是否写缓存」这一个判断上,不触及取数逻辑与口径。
    唯一新增行为是 sync 响应多一个 cacheEpoch 字段。
    极端情况下若代际判断有误,表现为「缓存命中率略降、查询变慢」,不会返回错误数据。
  • 回退:git revert 本提交即可,无数据迁移、无配置变更���无兼容性包袱。

关联

  • 排查报告:outputs/验收-同步按钮数据时效核验-20260922.md
    (同批核验结论:按钮链路本身无缓存污染,取数逐字段与直连库吻合;另存在本 PR 修复的
    并发回写竞态,以及上游侧 jst_inventoryInfo 09-22 白天停更的时效问题 —— 后者非应用层可修)。

未包含(待另议)

  • PR #19(codex/mkh-sale-exclude-special-order-0919,出库校验 + 箱表兜底)仍未合并;
    本 PR 基于 main,与该分支在 mkhBomService.js 上可能有小冲突,需按业务语义融合。
  • 页面加「数据截至时间」提示 / 同步按钮进度提示:另一独立任务。
## 目的与范围 **目的**:修复「点『同步最新数据』后,页面 2~10 分钟内仍显示旧数据」的问题(业务表现为「像在用缓存」)。 **范围**:仅 `backend/src/services/mkhBomService.js` 的缓存写入路径 + 对应单测,**无接口字段删除、无口径变更**。 ### 根因 本页 10 个进程内缓存都是同一写法: ```js xxxCache = { pending, value: null, expiresAt: 0 }; // 发起查询 const value = await pending; xxxCache = { pending: null, value, expiresAt: ... }; // ← 无条件写回 ``` 若某个查询 **in-flight 期间**用户点了「同步最新数据」(`clearMkhBomCache()` 清空全部缓存), 那个**发起于清缓存之前**的查询读到的仍是旧快照,返回时**无条件把旧值写回缓存**, 覆盖掉清缓存后新请求填入的新值 —— **谁后完成谁生效**。 后果被放大:清缓存不但没生效,反而把旧数据「钉」在缓存里 2~10 分钟(各缓存 TTL), 用户看到的就是「点了同步还是旧数据」。并发越高越容易撞上(多人同时使用、或先点「刷新数据」 再点「同步最新数据」)。 ### 修法:缓存代际 `cacheEpoch` - `clearMkhBomCache()` **先自增代际**,再清各缓存; - 每个查询发起时记下当时的代际,**写回缓存前比对**:代际对不上 ⇒ 中途被清过缓存 ⇒ 本次结果只返回给发起它的那个请求,**不写缓存**。 覆盖 **7 个变量型缓存**(`officialBomIndex` / `businessBom` / `productBom` / `mkhProduct` / `workOrderIndex` / `orderIndex` / `replenishIndex`)与 **3 个按 key 的 Map 缓存** (`officialBomDetail` / `componentLocation` / `componentStock`)。 `POST /api/mkh-bom/sync` 响应**新增 `cacheEpoch`** 字段(纯新增,便于线上核对清缓存是否生效)。 ## 同步与兼容性 - 基于 `origin/main` = `4946018`,`git rev-list --count HEAD..origin/main` = 0。 - 不改任何 SQL、不改取数口径、不改返回字段语义;`clearMkhBomCache()` 返回对象仅**多一个键**。 - 过期查询结果**仍照常返回给发起它的请求**(不改变单个请求自身的语义),只影响是否写缓存。 ## 验证 1. **复现测试(对照实验)**:新增用例「清缓存后在途的旧查询结果不得写回缓存」。 对照组(临时去掉写回保护)实测报 **`expected: 1 / actual: 9`** —— 正是「点同步后拿到旧值」; 加保护后通过。另加「同代际内缓存照常命中」回归保护用例。 2. **后端全量**:`node scripts/check-syntax.js` 通过;`node --test test/*.test.js` **340 用例 / 339 通过**,唯一失败为环境固有的 `resilientSecretStore` Windows 权限断言(与基线一致)。 3. **本地端到端**:预热 24.3s → 缓存命中 **58ms**;`POST /sync` 返回 `cacheEpoch=1`; 清缓存后冷查 **8.5s**(确实重查库);再查 **37ms**(缓存重新生效);前后数据指纹一致。 ## 风险与回退 - **风险**:低。改动只在「是否写缓存」这一个判断上,不触及取数逻辑与口径。 唯一新增行为是 `sync` 响应多一个 `cacheEpoch` 字段。 极端情况下若代际判断有误,表现为「缓存命中率略降、查询变慢」,**不会返回错误数据**。 - **回退**:`git revert` 本提交即可,无数据迁移、无配置变更���无兼容性包袱。 ## 关联 - 排查报告:`outputs/验收-同步按钮数据时效核验-20260922.md` (同批核验结论:按钮链路本身无缓存污染,取数逐字段与直连库吻合;**另**存在本 PR 修复的 并发回写竞态,以及上游侧 `jst_inventoryInfo` 09-22 白天停更的时效问题 —— 后者非应用层可修)。 ## 未包含(待另议) - **PR #19**(`codex/mkh-sale-exclude-special-order-0919`,出库校验 + 箱表兜底)仍**未合并**; 本 PR 基于 `main`,与该分支在 `mkhBomService.js` 上可能有小冲突,需按业务语义融合。 - 页面加「数据截至时间」提示 / 同步按钮进度提示:另一独立任务。
yanhui added 1 commit 2026-09-22 18:40:00 +08:00
问题:点「同步最新数据」后,页面在 2~10 分钟内仍显示旧数据,用户表现为「像在用缓存」。

根因:本页 10 个进程内缓存都是「先挂 pending → await 结果 → 无条件写回」的写法。
若某个查询 in-flight 期间用户点了同步(clearMkhBomCache 清空全部缓存),那个**发起于
清缓存之前**的查询读到的仍是旧快照,返回时会无条件把旧值写回缓存,覆盖掉清缓存后新
请求填入的新值 —— 谁后完成谁生效。并发越高(多人同时使用、或先点刷新再点同步)越容
易撞上,且清缓存反而把旧数据「钉」在缓存里 2~10 分钟(各缓存 TTL),表现为按钮失灵。

修法:引入缓存代际 cacheEpoch。
- clearMkhBomCache() 先自增代际,再清各缓存;
- 每个查询发起时记下当时的代际,写回缓存前比对:代际对不上说明中途被清过缓存,
  本次结果只返回给发起它的那个请求,不写缓存。
覆盖 7 个变量型缓存(officialBomIndex / businessBom / productBom / mkhProduct /
workOrderIndex / orderIndex / replenishIndex)与 3 个按 key 的 Map 缓存
(officialBomDetail / componentLocation / componentStock)。
POST /api/mkh-bom/sync 响应新增 cacheEpoch 字段,便于线上核对清缓存是否生效。

验证:
- 新增 2 个用例:① 复现竞态——对照组(去掉写回保护)报 expected 1 / actual 9,正是
  「点同步后拿到旧值」;加保护后通过。② 同代际内缓存照常命中的回归保护。
- 后端全量 node --test test/*.test.js:340 用例 / 339 通过,唯一失败为环境固有的
  resilientSecretStore Windows 权限断言(与基线一致)。
- 本地端到端:预热 24.3s → 缓存 58ms;sync 返回 cacheEpoch=1;冷查 8.5s 确实重查;
  再查 37ms 缓存重新生效;数据指纹前后一致。
yanhui merged commit a58fc7d67b into main 2026-09-22 23:58:05 +08:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: lunanting/cangku#20
No description provided.