本文整理 DGJ2 商品/物料搜索的 MySQL、MongoDB、Elasticsearch、Redis 和前端增量缓存链路。核心原则:数据库有商品,不代表当前站点的搜索副本有商品;搜索副本有商品,也不代表它具备价格、库存和业务可用性。
适用于商品搜索不到、搜索结果重复、价格/库存/限购展示旧、Mongo 有而 ES 无、ES 开关切换后结果变化、全量/增量任务卡住和前端商品缓存不更新等问题。
1. 业务目标
- 从一个 sid、SKU、invId 和关键字确定请求实际使用的搜索引擎。
- 区分全局物料、站点物料、基础字段、站点扩展字段和实时库存。
- 解释 MySQL、Mongo、ES、Redis、前端本地副本之间的同步关系。
- 找出数据在哪一层首次出现差异,而不是直接重建所有缓存。
- 正确处理全量重建、增量水位、任务锁、批量写入和失败重试。
- 修复后同时验证搜索、精确查询、采购、销售、商城和活动场景。
2. 五层数据模型
flowchart LR
DB["MySQL业务事实\nGoods/Ext/Price/Inventory"] --> BUILD["缓存构建/消息同步"]
BUILD --> MONGO["Mongo站点物料集合"]
BUILD --> ES["ES全局/站点索引"]
BUILD --> REDIS["Redis辅助集合/水位/开关"]
MONGO --> API["搜索与业务查询"]
ES --> API
REDIS --> API
MONGO --> FRONT["PC前端增量物料缓存"]
API --> PAGE["采购/销售/商城/活动页面"]
| 层 | 回答的问题 | 不能替代 |
|---|---|---|
| MySQL | 业务事实是否正确 | 搜索副本是否已同步 |
| Mongo | 当前站点物料副本是否存在 | ES 查询是否命中 |
| ES | 分词/模糊搜索和排序结果 | 实时库存最终事实 |
| Redis | 开关、集合、水位、任务锁 | 完整物料文档 |
| 前端增量 | 客户端何时拿到变化 | 服务端最新状态 |
3. 核心代码地图
| 职责 | 文件 | 核心方法 |
|---|---|---|
| Redis 客户端 | application/Components/Cache/KzCache.php | getInstance |
| Mongo 客户端 | application/Components/Cache/MongoCache.php | getInstance |
| ES 客户端 | application/Components/ElasticSearchFactory.php | make |
| ES 通用封装 | application/Services/Cache/EsSer.php | list/upsert/batchHandle |
| Redis 基础物料 | application/Services/Materiels/MaterielCacheSer.php | save/getMaterielCacheBySkuId |
| Mongo 站点物料 | application/Services/Materiels/MaterielCacheStationSer.php | goodsList/findSaMaterielList |
| ES 站点物料 | application/Services/Materiels/MaterielCacheStationEsSer.php | list/findSaMaterielList |
| 搜索引擎切换 | application/Services/Materiels/MaterielCacheQuerySwitchSer.php | setSid/querySaMaterielList |
| 批量缓存更新 | application/Services/Materiels/MaterielCacheBatchSer.php | batchUpdateBySkuIds |
| 商品中心通知 | application/Services/Notify/MaterielNotifySer.php | 价格/限价同步 |
| Mongo 构建任务 | application/controllers/tasks/Goods.php | 多种增量/库存更新 |
| ES 构建任务 | application/controllers/tasks/GoodsEsCache.php | 全量/增量/站点库存同步 |
| 辅助集合刷新 | application/controllers/tasks/GoodsCacheFlush.php | 限购/有价格 SKU |
| 缓存管理 | application/controllers/inner/CacheManage.php | 白名单/开关维护 |
| 通用物料搜索 | application/controllers/inner/GoodsSearch.php | list |
4. 数据载荷
4.1 基础字段
Mongo/ES 物料副本包含:
id/sid/skuId- 名称、标题、规格、车型、品牌、分类
- 单位、包装、条码、产品码、OE 码
- 状态、采购状态、销售模式
- 采购价、销售价、最低价
- 税率、保质期、属性等
4.2 站点扩展字段
| 字段 | 业务含义 |
|---|---|
qty | 站点库存汇总副本 |
fineQty | 特定库区/精品库存口径 |
disable | 站点是否停用物料 |
description | 站点描述 |
buyLimit | 是否限制采购 |
noPricingFlag | 是否没有价格 |
is_hot | 限价/热点类标识 |
modiflyTime | 基础/扩展变化时间戳(历史拼写) |
modiflyQtyTime | 库存变化时间戳 |
搜索副本的 qty 是展示和筛选副本,不应代替 SCM_INVENTORY_REAL_TIME 做最终库存修复依据。
5. Mongo 集合命名
MaterielCacheStationSer::setSid 使用逻辑集合:
KzEnums::GOODS_LIST_MONGODE_COLLECTION_SID + sid
每个服务站有独立集合。集合内既可包含快准商品,也可包含站点第三方商品,具体用文档的 sid、id 和状态区分。
Mongo 客户端当前使用 secondaryPreferred,只读查询可能命中从节点,因此刚写入后短时间内出现读延迟时要考虑复制滞后。
6. ES 索引命名
MaterielCacheStationEsSer::setSid 的规则:
| sid | 索引 | 含义 |
|---|---|---|
| 0 | dgj_goods_kz | 快准物料全局主索引 |
| 非 0 | dgj_goods_{sid} | 服务站独立物料索引 |
文档 type 为 goods_info。代码带 type 参数,说明兼容的是较老 ES API;升级 ES 客户端或集群前必须验证 mapping/type 兼容。
flowchart TD
GOODS["快准/第三方商品"] --> MAIN["dgj_goods_kz"]
MAIN --> S1["dgj_goods_sidA"]
MAIN --> S2["dgj_goods_sidB"]
INV["实时库存/站点价格/限制"] --> S1
INV --> S2
7. Redis 角色
Redis 不只做结果缓存,还承担开关、集合、同步水位和任务锁。
| 逻辑键 | 作用 |
|---|---|
KEY_IS_OPEN_ES | 是否开启 ES 写入/搜索总开关 |
HASH_ES_SEARCH_BLACKLIST | 指定站点关闭 ES 搜索 |
HASH_ES_SEARCH_WHITELIST | 历史 ES 搜索白名单配置 |
KEY_GOODS_UPDATE_TIME | 商品缓存更新时间 |
KEY_GOODS_SID_UPDATE_TIME | Mongo 站点商品同步水位 |
KEY_GOODS_QTY_SID_UPDATE_TIME | Mongo 站点库存同步水位 |
KEY_GOODS_ES_UPDATE_TIME | ES 主索引增量水位 |
KEY_GOODS_QTY_SID_ES_UPDATE_TIME | ES 站点库存增量水位 |
KEY_EXIST_SYNC_GOODS_TASK | Mongo/商品同步任务锁 |
KEY_EXIST_SYNC_GOODS_ES_TASK | ES 同步任务锁 |
KEY_SYNC_GOODS_*_ERROR_TIMES | 上次任务未结束计数 |
KEY_BUY_LIMIT_SKUIDS:{sid} | 限购 SKU 逗号集合 |
KEY_HAS_PRICE_SKUIDS:{sid} | 有价格 SKU 逗号集合 |
HASH_MATERIEL_SKUID:{sku} | Redis 物料基础 Hash |
MATERIEL_ALL_UPDATE_LIMIT* | 前端全量物料更新限流 |
这些是逻辑 key;连接层和枚举都可能带前缀,排查时不要手工重复拼接。
8. 搜索引擎切换
销售开单通过 MaterielCacheQuerySwitchSer::setSid 选择引擎:
flowchart TD
REQ["销售商品搜索 sid"] --> OPEN{"KEY_IS_OPEN_ES有值?"}
OPEN -- 否 --> M["Mongo MaterielCacheQuerySer"]
OPEN -- 是 --> BLACK{"sid在ES搜索黑名单?"}
BLACK -- 是 --> M
BLACK -- 否 --> E["ES MaterielCacheQueryEsSer"]
M --> OUT["count + list"]
E --> OUT
注意:当前切换逻辑直接使用总开关和黑名单,未读取 HASH_ES_SEARCH_WHITELIST。看到白名单 key 不代表该调用链实际使用它。
9. Mongo 搜索规则
销售搜索 findSaMaterielList:
- 关键字通过
RegexUtil::getQuoteRegex转安全正则。 - 匹配预拼接的
search字段。 - 要求
disable='0'。 - 可限制微仓 SKU 或 invId。
- 只允许正常、临时供给、停止供给、新品等状态集合。
- 按 page/limit 做 skip/limit。
Mongo search 字段由名称、编号、品牌、分类、规格、SKU、车型、产品码、拼音、OE 等拼接,最多保留 300 字符。长字段尾部被截断时,某些关键词可能无法命中。
10. ES 搜索规则
MaterielCacheStationEsSer::list 根据关键词类型分两支。
10.1 英文、数字、斜杠类
multi_match搜索全部搜索字段,minimum_should_match=100%。- 对 SKU、number、productCode、def、spec、name 增加 wildcard。
- 不含
/时使用*keyword*。 - 含
/时按/分段,每段都必须匹配。 - 外层 should 至少命中 1 个。
10.2 中文/分词类
- 先用
ik_max_wordanalyzer 分词。 - token 超过 2 个时
minimum_should_match=90%。 - 否则要求 100%。
- 使用 multi_match 的 must 条件。
flowchart TD
K["search keyword"] --> ASCII{"仅英文/数字/斜杠?"}
ASCII -- 是 --> MM["multi_match 100%"]
MM --> WC["关键字段wildcard"]
ASCII -- 否 --> IK["ik_max_word分词"]
IK --> TOK{"token > 2?"}
TOK -- 是 --> P90["minimum_should_match 90%"]
TOK -- 否 --> P100["minimum_should_match 100%"]
WC --> FILTER["状态/disable/站点条件"]
P90 --> FILTER
P100 --> FILTER
11. Mongo 与 ES 结果差异来源
| 维度 | Mongo | ES |
|---|---|---|
| 关键字 | 拼接字段正则 | IK 分词 + multi_match + wildcard |
| 字段长度 | search 最多 300 字符 | 各字段独立索引 |
| 大小写 | 正则配置决定 | 搜索前转小写、analyzer/mapping 决定 |
| 斜杠 | Regex 整体匹配 | 分段 wildcard 且要求全部段 |
| 排序 | Mongo sort | ES sort/relevance |
| 一致性 | 副本读可能有延迟 | refresh 与增量任务延迟 |
| 字段类型 | 保存时大量转 string | mapping 决定 keyword/text/numeric |
切换引擎后排序、模糊程度和总数可能变化。上线不能只验证“有结果”,还要验证业务期望的前 N 条排序。
12. 精确查询与全文搜索
| 查询 | 推荐定位 |
|---|---|
| 按 invId | Mongo findMateriel / ES findById / MySQL |
| 按多个 invId | findMaterielByInvIds |
| 按多个 SKU | findMaterielBySkuIds |
| 按商品编号 | GoodsSearch::getGoodsListByNumber 或业务库 |
| 关键字销售搜索 | QuerySwitch -> ES/Mongo |
| 品牌分类聚合 | ES collapse 或业务表 |
精确查询能命中而全文搜索不命中,通常是搜索字段、mapping、analyzer、状态过滤或 disable 问题;两者都不命中才优先怀疑副本缺记录。
13. 物料写入 Mongo
saveMaterielToMongo:
- 要求
id非空。 - 只保留定义过的基础、搜索和扩展字段。
- 数量/限制字段转 int,其余多数字段转 string。
- 拼接
search字段。 - 写
modiflyTime。 - 不存在则 insert,存在则
$setupdate。 - 站点 sid 非 0 时更新站点商品更新时间。
sequenceDiagram
participant B as 业务变更
participant S as MaterielCacheStationSer
participant M as Mongo集合
participant T as GoodsService更新时间
B->>S: sid + materiel
S->>S: 过滤字段/转类型/拼search
S->>M: findOne(id)
alt 不存在
S->>M: insertOne
else 已存在
S->>M: updateOne $set
end
S->>T: updateTime(sid)
14. 库存更新时间分离
Mongo 更新方法区分:
- 普通字段:更新
modiflyTime。 - 库存字段:传
is_update_qty=true时更新modiflyQtyTime。
前端增量同步和后台增量任务可能分别依赖这两个水位。把库存变化误写成普通变化,或反过来,会导致增量列表和全量阈值行为不一致。
15. 物料写入 ES
ES 支持:
- 单文档 upsert/update/delete。
- 条件 update/delete。
- bulk create/update/delete/index。
- 索引创建、删除、mapping/settings。
- scroll 分页查询。
MaterielCacheStationEsSer 的 bulk body 阈值为 2000 个数组元素;一个 ES bulk 文档通常包含“动作行+数据行”,所以 2000 元素并不等于 2000 个商品。
16. ES mapping
mapping 必须与查询写法匹配:
skuId.keyword等精确字段要存在 keyword 子字段。- name/brand/category 等全文字段要配置 analyzer。
- qty/fineQty 若要数值排序必须是 numeric。
- status/disable 在当前代码中常以 string term 查询。
- 新增
oe_code/product_model/item_attribute等字段需同步 mapping 和构建字段。
只更新 PHP 字段列表,不更新线上 mapping,可能导致 bulk 拒绝、字段动态映射错误或查询不命中。
17. Mongo 索引
GoodsCache::addMongoIndex 当前示例为每站集合创建 skuId:1 索引,但方法循环中存在历史 die,一次运行可能只处理首个站点。批量建索引前应检查实际代码和已存在索引,不要假设任务覆盖全部站点。
推荐按真实查询核对:
id 唯一/普通索引
skuId 索引
status + disable 组合
modiflyTime
modiflyQtyTime
search 正则是否可利用索引(通常有限)
索引设计需以 explain 和集合规模验证。
18. Redis 物料基础 Hash
MaterielCacheSer 使用:
HASH_MATERIEL_SKUID + skuId
把物料数组写为 Redis Hash,读取时 HGETALL。此层是按 SKU 的基础信息快捷缓存,不等于站点 Mongo/ES 文档,也不包含完整站点价格库存语义。
19. 限购 SKU 集合
BuyLimitSkuCacheSer 把站点限购 SKU 数组用逗号拼接成一个 Redis String。
KEY_BUY_LIMIT_SKUIDS + sid = "sku1,sku2,..."
注意:flushBuyLimitSkuCache 只有数组非空时才 set。若新结果为空,旧 key 不会在该方法中主动删除,可能留下历史限购集合。排查“全部取消后仍限购”时必须验证这一分支。
20. 有价格 SKU 集合
HasPriceSkuCacheSer 同样用逗号 String 保存站点有价格 SKU。非空才 set,空集合也不会主动删除旧值。
GoodsCacheFlush::flushBuyLimitAndPriceCache 会从 Mongo 重新读取 buyLimit/noPricingFlag 后刷新两个集合。
flowchart LR
ITEM["商品中心有价格SKU"] --> DIFF["与本地HasPrice集合求差"]
MONGO["Mongo noPricingFlag"] --> DIFF
DIFF --> UP1["批量更新Mongo"]
DIFF --> UP2["条件更新ES"]
DIFF --> UP3["刷新Redis SKU集合"]
21. 商品中心通知
MaterielNotifySer 处理价格和限价同步:
- 从商品中心查询目标 SKU 集合。
- 从 Redis 集合读取本地状态;为空时回查 Mongo。
- 计算新增和取消差集。
- 每 2000 SKU 组装 Mongo 批量更新。
- ES 总开关开启时更新 ES。
- 刷新 Redis 集合。
ES 更新失败会短暂重试,索引不存在时可被视为站点锁定而忽略;多次失败后告警。业务表/Mongo 成功但 ES 失败会形成暂时不一致。
22. 全量与增量同步
| 模式 | 来源 | 目标 | 适用 |
|---|---|---|---|
| 主商品全量 | Goods/MySQL | 全局 Mongo/ES | 首次构建、字段大变更 |
| 主商品增量 | modifyTime >= 水位 | 全局及受影响站点 | 日常商品变更 |
| 站点库存增量 | 实时库存变化 | 站点 Mongo/ES qty | 日常库存变化 |
| 价格/限制消息 | 商品中心事件 | Mongo/ES/Redis | 实时业务标识 |
| 前端增量 | Mongo modiflyTime | PC 本地物料 | 客户端刷新 |
23. 增量水位
正确增量步骤:
sequenceDiagram
participant R as Redis水位
participant T as 同步任务
participant DB as MySQL
participant C as Mongo/ES
T->>R: 读取last_success_time
T->>T: 固定本次截止时间end_time
T->>DB: 查询(last_success_time, end_time]
loop 分批
T->>C: upsert/delete
C-->>T: 批量结果
end
T->>R: 全部成功后写end_time
水位不能在批处理开始时提前推进,否则中途失败会永久漏数据。需要从代码和日志确认各任务实际写水位时机。
24. ES 任务锁
GoodsEsCache::syncBackupGoodsIdsBySid:
- 检查 ES 是否开启。
- 查
KEY_EXIST_SYNC_GOODS_ES_TASK。 - 已锁时增加错误次数。
- 连续超过阈值会清锁、重置计数并告警。
- 未锁则 set 锁。
- 执行同步。
- 完成后 del 锁。
风险:进程崩溃时锁没有显式 TTL,需要靠后续连续检测清理;直接删锁可能导致上一任务仍运行时并发同步。
25. ES 主索引增量
flushBackupGoodsInfo 的主要逻辑:
- 读取
KEY_GOODS_ES_UPDATE_TIME,无水位时要求先完成基础构建。 - 全量模式可删除主索引后重建。
- 增量模式按 Goods
modifyTime查询。 - 主索引 sid=0,站点索引按库存流水和第三方商品更新。
- 快准主商品在主索引中站点字段置默认值。
- 删除商品会同步删除受影响站点文档。
- 每批组装 bulk body 后提交。
全量删除索引是高风险动作,必须确认重建任务可完成、搜索可降级到 Mongo,并准备失败恢复。
26. 前端增量物料
MaterielFrontCacheSer::getChangeByTime:
- 按
modiflyTime > 客户端时间查 Mongo 更新项。 - 普通站点阈值 2000;部分特殊站点阈值更低。
- 超过阈值时返回
updateAll=1,要求前端全量更新。 - 全量更新受 Redis 计数和超时限流。
- 再从 MySQL 查询已删除商品。
- 按快准/第三方拆分 update/delete。
flowchart TD
REQ["客户端time"] --> Q["Mongo modiflyTime > time"]
Q --> N{"变更数 > 阈值?"}
N -- 否 --> DEL["MySQL补删除列表"]
DEL --> INC["返回增量update/delete"]
N -- 是 --> LIMIT{"全量更新限流允许?"}
LIMIT -- 是 --> ALL["updateAll=1"]
LIMIT -- 否 --> WAIT["暂不触发全量"]
27. 搜索 API
通用内部入口 inner/GoodsSearch::list 接收:
{
"search": "刹车片",
"isPage": 1,
"page": 1,
"limit": 20,
"fields": "id,skuId,name,brandName",
"orderBy": ""
}
该入口调用 MaterielSer::getMaterielBySearch,不一定等同于销售开单 QuerySwitch。排查时必须从实际 Controller/Service 追踪,不要仅凭接口名判断走 ES。
28. 业务可用性过滤
搜索文档存在后还可能被这些条件排除:
| 条件 | 典型字段/来源 |
|---|---|
| 已删除 | isDelete |
| 站点停用 | disable |
| 供给状态 | status/buyStatus |
| 采购限购 | buyLimit + Redis SKU 集合 |
| 无价格 | noPricingFlag + HasPrice 集合 |
| 库存不足 | qty/fineQty 或实时库存查询 |
| 微仓范围 | move_sku |
| 指定商品范围 | invIds |
| 活动状态 | 活动商品表和活动时间,不在基础搜索副本内 |
“搜不到”和“搜到但不能下单”应分开记录。
29. 按 SKU 的排查事实卡
环境:
请求接口/页面:
request_id:
sid:
uid:
skuId:
invId:
关键字:
期望搜索引擎:
KEY_IS_OPEN_ES:
ES黑名单命中:
MySQL商品状态:
Mongo文档/更新时间:
ES文档/索引:
Redis限购/有价格集合:
实时库存:
最后同步任务时间:
30. MySQL 只读核对
SELECT id, sid, skuId, name, number, brandId, categoryId,
status, buyStatus, isDelete, modifyTime
FROM t_bs_goods
WHERE skuId = :sku_id
ORDER BY sid, id;
扩展、价格和站点启停:
SELECT *
FROM t_bs_goods_ext
WHERE invId = :inv_id;
SELECT *
FROM t_bs_goods_purprice
WHERE sid = :sid AND invId = :inv_id;
SELECT *
FROM t_bs_goods_sto
WHERE sid = :sid AND invId = :inv_id;
字段名以目标环境 DDL 为准。生产排查禁止使用 SELECT * 导出大范围数据;示例仅表示关联方向。
31. Mongo 只读核对
使用受控客户端查询目标站点集合:
db.<station_goods_collection>.find(
{ skuId: "<SKU>" },
{
id: 1, sid: 1, skuId: 1, name: 1,
status: 1, disable: 1, qty: 1, fineQty: 1,
buyLimit: 1, noPricingFlag: 1,
modiflyTime: 1, modiflyQtyTime: 1
}
)
不要在公共文档记录 Mongo 地址、账号或密码。
32. ES 只读核对
目标索引按 sid 计算:
GET dgj_goods_<sid>/goods_info/<inv_id>
或按 SKU:
GET dgj_goods_<sid>/goods_info/_search
{
"query": {
"term": {
"skuId.keyword": "<SKU>"
}
},
"_source": [
"id", "sid", "skuId", "name", "status", "disable",
"qty", "fineQty", "buyLimit", "noPricingFlag", "modiflyTime"
]
}
具体 URL 和 type 语法以现网 ES 版本为准。
33. Redis 只读核对
只读检查逻辑:
GET KEY_IS_OPEN_ES
HGET HASH_ES_SEARCH_BLACKLIST <sid>
GET KEY_GOODS_ES_UPDATE_TIME
GET KEY_GOODS_QTY_SID_ES_UPDATE_TIME
GET KEY_EXIST_SYNC_GOODS_ES_TASK
GET KEY_SYNC_GOODS_ES_ERROR_TIMES
GET KEY_BUY_LIMIT_SKUIDS<sid>
GET KEY_HAS_PRICE_SKUIDS<sid>
逗号 String 很大时不要在共享终端完整打印,可先查长度或在安全脚本中判断目标 SKU 是否存在。
34. 常用代码搜索
# 从页面定位实际搜索实现
rg -n "querySaMaterielList|getMaterielBySearch|findSaMaterielList|GoodsSearch" application
# 查ES/Mongo开关
rg -n "KEY_IS_OPEN_ES|HASH_ES_SEARCH_BLACKLIST|HASH_ES_SEARCH_WHITELIST" application
# 查目标字段在哪些同步链更新
rg -n "noPricingFlag|buyLimit|is_hot|fineQty|modiflyQtyTime" application/Services application/controllers/tasks
# 查全量/增量任务和水位
rg -n "KEY_GOODS_.*UPDATE_TIME|syncBackupGoods|flushBackupGoods|updateTime" application/controllers/tasks application/Services
# 查缓存写入和失效
rg -n "updateMaterielToMongo|saveMaterielToMongo|updateByQuery|flush.*SkuCache" application
# 查搜索字段和mapping
rg -n "SEARCH_FIELDS|getMappings|putMappings|ik_max_word|minimum_should_match" application/Services/Materiels application/Services/Cache
35. 故障一:MySQL 有,Mongo/ES 都没有
flowchart TD
A["DB有/副本都无"] --> B{"商品modifyTime晚于水位?"}
B -- 否 --> R1["水位提前/历史漏同步"]
B -- 是 --> C{"同步任务运行?"}
C -- 否 --> R2["任务锁/调度/开关"]
C -- 是 --> D{"商品状态被删除过滤?"}
D -- 是 --> R3["确认应删除副本"]
D -- 否 --> E{"bulk有错误?"}
E -- 是 --> R4["按失败批次补偿"]
E -- 否 --> R5["核对sid与索引/集合"]
不要先全量重建;先找出水位、任务、过滤或 bulk 的首个断点。
36. 故障二:Mongo 有,ES 没有
- 查
KEY_IS_OPEN_ES。 - 查站点是否存在 ES 索引。
- 查 mapping 是否接受目标字段。
- 查商品/价格/库存更新链是否只更新 Mongo。
- 查 ES 任务锁和错误计数。
- 查 bulk response 是否
errors=true,不能只看 HTTP 成功。 - 查目标站点是否因锁定而允许忽略 index not found。
- 先单文档补偿并验证,再决定是否重跑批次。
37. 故障三:ES 有,Mongo 没有
如果站点在 ES 黑名单或总开关关闭,请求会走 Mongo,于是用户搜不到。
检查:
- Mongo 构建任务水位和锁。
- 集合名是否为当前 sid。
- 是否只执行过 ES 全量任务。
- Mongo 文档是否被误删。
- 商品中心通知是否只更新 ES 条件文档。
- Mongo 从节点读是否暂时延迟。
38. 故障四:精确 SKU 能查,关键字搜不到
| 引擎 | 首查 |
|---|---|
| Mongo | search 拼接内容、300 字截断、Regex 转义 |
| ES | analyzer token、mapping、keyword/text、minimum_should_match |
| 共通 | status、disable、move_sku、invIds 过滤 |
用同一关键词直接调用 analyzer 和副本查询,确认是索引内容问题还是查询 DSL 问题。
39. 故障五:库存或价格展示旧
- 先查实时库存/价格业务表,确认事实值。
- 查 Mongo
qty/fineQty/noPricingFlag。 - 查 ES 同字段。
- 查库存/价格增量水位。
- 查通知或任务日志。
- 查页面是否额外读取 Redis SKU 集合。
- 查前端是否仍使用旧增量时间。
库存修复只改搜索副本会再次被同步覆盖;必须先修业务事实或同步链。
40. 故障六:全部取消限购/价格后仍旧
当前两个 Redis 集合刷新方法在新集合为空时不会主动删除旧 key。
排查:
- Mongo 所有目标商品标识是否已清零。
- ES 是否已清零。
- Redis String 是否仍保留旧 SKU。
- 业务读取方优先用 Redis 还是 Mongo。
- 评估通过标准服务补充“空集合删除 key”的修复和回归。
不要手工删除全部站点缓存;先限定目标 sid 和逻辑 key。
41. 故障七:任务一直提示上次未结束
- 查任务进程是否仍存在。
- 查锁 key 是否有 TTL;当前 ES 锁逻辑主要靠错误次数清理。
- 查最后一批日志和水位。
- 查 ES/Mongo 是否仍有写入流量。
- 确认旧进程已停止后再清目标锁。
- 从上次成功水位重跑,不从当前时间跳过。
42. 故障八:全量重建后短时间搜索为空
全量 ES 可能先删索引再重建。窗口期内:
- ES 查询可能 index not found 或返回空。
- QuerySwitch 不会因查询异常自动切 Mongo,除非上层有兜底。
- 站点不在黑名单仍会走 ES。
高风险全量任务应采用新索引构建完成后切 alias 的方式;当前代码是否支持 alias 需单独评估,不能在现网直接假设。
43. 修复策略
| 差异 | 优先修复 |
|---|---|
| DB 错 | 修业务数据和正常业务链 |
| DB 对、Mongo 错 | 目标站点/商品定向重建 Mongo |
| Mongo 对、ES 错 | 定向 upsert ES,保留水位证据 |
| 副本对、Redis 集合错 | 定向刷新/删除目标 key |
| 服务端对、前端错 | 推进增量时间或触发受控全量 |
| 大范围漏同步 | 修水位/任务后按时间窗补偿 |
任何修复都要保留修复前后快照、目标数量、任务日志和验证结果。
44. 数据修复门槛
- 已确认实际搜索引擎。
- 已确认 sid、SKU、invId 的对应关系。
- 已找出第一处不一致层。
- 已固定增量起止水位。
- 已统计影响商品数和站点数。
- 已确认全量/增量任务不会并发覆盖。
- 已准备降级搜索或维护窗口。
- 已验证目标 mapping/索引/集合存在。
禁止:
- 未确认旧任务状态直接删任务锁。
- 先推进水位再补数据。
- 为单商品问题删除整个 ES 索引。
- 全库
FLUSHDB。 - 只改搜索副本而不修业务事实。
- 在公共工单粘贴连接凭证或完整缓存数据。
45. 回归矩阵
| 维度 | 用例 |
|---|---|
| 引擎 | ES 开、ES 关、站点黑名单、索引不存在 |
| 关键词 | 中文、SKU、编号、拼音、OE、带 /、大小写、特殊字符 |
| 查询 | 精确 ID、多个 SKU、全文、品牌分类、空关键字 |
| 商品 | 快准、第三方、正常、停用、删除、临采、停止供给、新品 |
| 站点 | 有价格、无价格、限购、取消限购、库存 0/正/负 |
| 同步 | 单条更新、批量更新、增量、全量、删除、任务中断重跑 |
| 一致性 | DB/Mongo/ES/Redis/前端五层比对 |
| 分页 | 第一页、深分页、排序、总数一致性 |
| 性能 | wildcard、正则、批量 SKU、大站点索引 |
| 降级 | ES 异常时 Mongo 方案、恢复后结果对比 |
46. 改动风险分级
| 级别 | 改动 | 风险 |
|---|---|---|
| 高 | QuerySwitch/ES 总开关 | 影响全站销售商品搜索 |
| 高 | ES mapping/全量删索引 | 搜索中断或批量写失败 |
| 高 | 增量水位/任务锁 | 永久漏同步或并发重复写 |
| 高 | Mongo/ES 字段口径 | 采购销售价格库存判断变化 |
| 中 | 搜索 analyzer/DSL | 命中率和排序变化 |
| 中 | Redis 限购/有价格集合 | 商品可采购/可售范围变化 |
| 中 | 前端全量限流 | 大量客户端同时拉全量 |
| 低 | 单站单商品定向补偿 | 仍需防止后续任务覆盖 |
47. 监控建议
最少监控:
- ES/Mongo 查询成功率和 P95/P99。
- ES index not found、bulk errors 数量。
- 每次任务扫描数、成功数、删除数、失败数、耗时。
- 当前水位距现在的延迟。
- 任务锁持续时间和连续冲突次数。
- DB 与 Mongo/ES 抽样文档差异。
- 每站索引/集合文档数异常变化。
- 前端
updateAll触发和限流次数。
48. 证据来源
| 结论 | 代码证据 |
|---|---|
| ES/Mongo 切换条件 | MaterielCacheQuerySwitchSer::setSid |
| ES 索引命名 | MaterielCacheStationEsSer::setSid |
| ES IK/ASCII 搜索 | MaterielCacheStationEsSer::list |
| Mongo 拼接 search | MaterielCacheStationSer::formatCacheData |
| Mongo 搜索过滤 | MaterielCacheStationSer::findSaMaterielList |
| Mongo 从节点优先 | application/Components/Cache/MongoCache.php |
| Redis 前缀与连接 | KzCache.php、RedisKeys.php |
| ES 批量与 scroll | application/Services/Cache/EsSer.php |
| 价格标识三副本同步 | MaterielNotifySer::syncMaterielHasPriceBySids |
| 2000 SKU Mongo 批量 | MaterielCacheBatchSer |
| 空集合不清旧 key | BuyLimitSkuCacheSer/HasPriceSkuCacheSer |
| 前端增量与全量限流 | MaterielFrontCacheSer::getChangeByTime |
| ES 任务锁与错误计数 | GoodsEsCache::syncBackupGoodsIdsBySid |
| Mongo 索引任务有历史 die | GoodsCache::addMongoIndex |
49. 待环境确认项
- 线上 ES 具体版本、mapping、分片副本、refresh interval 和 alias 使用情况。
- 每个站点 Mongo 集合的真实索引、文档量和复制延迟。
- Goods/库存任务的 crontab 频率、互斥方式和最近成功水位。
- ES 查询异常是否在更上层自动降级 Mongo。
HASH_ES_SEARCH_WHITELIST当前是否有其他入口仍在使用。- Redis 客户端配置前缀与枚举
dgj:组合后的真实 key。 qty/fineQty与实时库存各库区的线上口径。- 前端本地物料存储实现、增量时间保存和全量失败重试。
- 搜索排序的产品验收标准和线上慢查询阈值。
以上环境项必须用只读检查、任务日志和真实请求验证后再补充,不将推测写成现网事实。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 商品搜索 | PC/App/Inner GoodsSearch | sid、关键词、品牌/车型、分页 | inner/GoodsSearch.php | Materiel query switch/ES Service | SKU/物料 ID | 按开关走 ES、Mongo/Redis 或 DB |
| 商品变更刷新 | 商品中心事件/业务修改 | SKU、变更类型、更新时间 | Goods task/Notify | MaterielNotifySer、Cache Service | SKU + update time | 缓存与索引追随商品主表 |
| 批量重建 | CLI/CacheManage | 站点、SKU 范围、批次 | GoodsEsCache/GoodsCacheFlush/CacheManage | Batch/ES Service | task batch | 批量清理或重建索引 |
| 库存展示 | 搜索列表/商品详情 | sid、SKU、仓库 | 查询入口 | Cache + SCM_INVENTORY_REAL_TIME | 库存四维键 | 搜索结果附加当前可售库存 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 查询路由 | QuerySwitch/GoodsSearch | request_id、sid、query、ES 开关/名单 | 明确记录或可代码判定数据源 | 环境开关判断错、无降级 | 同一请求在目标数据源检索 | | ES/缓存查询 | ES/Mongo/Redis Client | index/key、SKU、耗时 | 命中且数据版本合理 | timeout、mapping、反序列化失败 | SKU 与 DB 主表比较 | | 增量刷新 | Goods task/Notify | message ID、SKU、KEY_GOODS_UPDATE_TIME | 目标 key/doc 更新并 ACK | 消费失败、更新时间倒退 | SKU + update time 回查索引 | | 批量重建 | Task 日志 | task、批次、游标、成功失败数 | 总数闭合,可断点继续 | 清空后未重建、部分分片失败 | 批次 SKU 清单抽样校验 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| OLTP 变更 | 商品/库存 Service | 业务事务 | 商品/实时库存旧值 | 商品/库存表、通知 MQ | DB old -> new,生成更新时间 | SKU、表更新时间、消息键 |
| 选择查询源 | MaterielCacheQuerySwitchSer | 只读 | ES 开关、黑白名单、站点 | 无 | 请求路由 DB/cache/ES | KEY_IS_OPEN_ES、名单成员、request ID |
| 增量同步 | MaterielNotifySer/Goods task | 消费事务 | DB 新事实、消息版本 | Redis/Mongo/ES | doc/key old -> new;旧事件不得覆盖新版本 | SKU、update time、ACK |
| 批量重建 | CacheBatch/EsSer | 分批外部写 | DB 分页结果 | index/cache | delete/reindex;每批成功数累计 | batch、游标、源/目标总数 |
| 读时合并 | GoodsSearch/InventoryQuery | 只读 | 商品索引、实时库存 | 无 | 返回快照,不写业务表 | SKU 同时查 ES 文档和实时库存 |
子模块追踪:search-route Mongo、ES 与数据库查询路由
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 路由 | 搜索/精确查询 | sid、query/SKU、engine switch | application/Services/Materiels/MaterielCacheQuerySwitchSer.php | ES/Mongo 开关、查询类型、降级条件 | 查询只读 不写;选择 DB/Mongo/ES | request ID + query + engine | 先确认实际路由,再比较对应数据源 |
| 降级 | 引擎失败/空结果 | engine error、fallback | application/controllers/inner/GoodsSearch.php | 主库、索引水位、业务过滤 | 查询只读 不写 | request ID + fallback result | 空结果区分索引缺失与过滤;合同待环境确认 |
子模块追踪:mongo-index 物料 Mongo 缓存与索引
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 同步 | 全量/增量物料 | task、sid、SKU/invId | application/Services/Materiels/MaterielCacheSer.php | MySQL 商品/站点扩展与旧文档 | 事务外 upsert old doc -> source snapshot | task + collection + SKU + count | 失败键保留,不推进其水位 |
| 核对 | MySQL 有而 Mongo 无 | sid、SKU、collection | application/Components/Cache/MongoCache.php | 源行、文档、索引、updatedAt | 查询只读 不写 | SKU + source/cache time | 只重建缺失 SKU;集合环境待确认 |
子模块追踪:es-index 物料 ES 文档与 mapping
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 写索引 | ES 全量/增量 | index、task、SKU | application/Services/Cache/EsSer.php -> application/Components/ElasticSearchFactory.php | 源物料、mapping、旧文档、任务锁 | 事务外 bulk old doc -> new doc,DB 不变 | task + index + bulk errors | mapping 冲突隔离失败文档 |
| 搜索核对 | SKU 可查但关键词无 | query、index/version | application/Services/Materiels/MaterielCacheStationEsSer.php | analyzer、mapping、业务过滤 | 查询只读 不写 | request ID + query/index + hits | mapping 变更需重建,不以改单条文档长期修复 |
子模块追踪:redis-material Redis 物料、限购与价格集合
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 刷新 | 商品/价格/限购变更 | sid、SKU、key/version | application/Components/Cache/KzCache.php | DB 主事实、旧 Hash/Set | commit 后事务外 cache old -> latest/deleted | event/task + key + SKU | 全部取消也要清旧集合;TTL 待实测 |
| 回查 | 页面值旧 | sid、SKU、cache key | application/Services/Materiels/MaterielCacheStationSer.php | Redis、源表、更新时间 | 查询只读 不写 | request ID + key + hit/time | 源正确只补缓存,不改主表 |
子模块追踪:goods-notify 商品中心增量通知
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 消费 | SKU/价格/状态事件 | message ID、event、SKU、version | application/Services/Notify/MaterielNotifySer.php | 本地映射、版本和站点范围 | 单消息本地事务 old -> new/待刷新标记 | message ID + event + SKU/version | 老版本不覆盖新值;未知事件告警 |
| 传播 | 提交后刷新三类派生层 | SKU、event ID | application/controllers/tasks/Goods.php | 已提交商品和目标更新时间 | 事务外刷新 Mongo/ES/Redis | event + SKU + target results | 只重试失败目标,成功目标不重做 |
子模块追踪:full-sync 全量同步与重建
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 构建 | 管理任务重建索引 | task、scope、new target | application/controllers/tasks/GoodsCacheFlush.php | 主库全量、目标版本、锁、水位 | 事务外 empty target -> full snapshot,主库不变 | task + batch/total/errors | 未完成不切流;保留旧索引回滚 |
| 切换 | 新版本验收后切流 | target、sample SKUs | application/controllers/inner/CacheManage.php | 行数、样本、过滤、差异 | alias/config old -> new,DB 不写 | task + version + diff | 空搜索立即回切并补失败批 |
子模块追踪:increment-sync 增量水位与任务锁
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 扫描 | 定时增量同步 | task、lock、from/to watermark | application/controllers/tasks/GoodsEsCache.php | 成功水位、锁和候选 | 事务外同步;仅成功批 watermark old -> new | task + range + errors | 失败不得越过;确认进程后再清过期锁 |
| 重跑 | 上次未结束/漏数据 | owner、watermark、SKU | application/Services/Materiels/MaterielCacheBatchSer.php | 进程、TTL、源/目标时间 | 查询只读;从旧水位幂等重跑 | task + owner + watermark | 禁止直接跳水位;upsert 不重复文档 |
子模块追踪:search-api 搜索 API 与业务可用性过滤
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 搜索 | 关键字/SKU API | sid、query、filters/page | application/controllers/inner/GoodsSearch.php -> application/Services/Materiels/MaterielCacheQuerySwitchSer.php | 引擎命中、启用、价格库存限购、站点可见 | 查询只读 不写;命中后业务过滤 | request ID + engine + raw/final hits | raw 有 final 无是过滤,不直接重建索引 |
| 复核 | 页面与精确查询不同 | sid、SKU、engine doc | application/Services/Materiels/MaterielCacheStationSer.php | MySQL/Mongo/ES/Redis 快照 | 查询只读 不写 | SKU + each-layer time | 固定同一 sid 与口径比较 |
子模块追踪:search-repair 缓存索引差异修复
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 判定 | 各层不一致 | sid、SKU、version/time | application/Services/Materiels/MaterielCacheSer.php | 主库、派生文档和过滤 | 比对查询只读 不写;确定目标层 | task + SKU + layer diff | 主库错先修来源,不以缓存覆盖主库 |
| 修复 | 单 SKU/小批重建 | batch、SKU list、targets | application/controllers/inner/CacheManage.php | 前快照、任务锁、目标层 | 事务外 stale -> source snapshot,主库不变 | batch + SKU + results | 小批回读;只重试失败层 |