news 2026/9/9 4:41:45

后过滤召回塌陷:Redis候选被ES全部过滤掉后的四级兜底方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
后过滤召回塌陷:Redis候选被ES全部过滤掉后的四级兜底方案

先说你最可能在哪儿遇到这个问题:电商搜索、信息流推荐、垂类内容筛选,凡是后端存在"先用 Redis 出候选、再交给 ES 做精细化过滤"这种两段式检索的系统,都躲不开一个诡异的线上现象——用户明明没做错任何操作,前端却返回了"没有找到相关商品"或者"暂无数据"。

我排查过不止一次这类事故,链路看多了之后发现,问题的根源非常统一:Redis 先召回 → ES 再过滤,如果 ES 的过滤逻辑把 Redis 召回的候选全灭了,下游又没有任何兜底,用户看到的就是一张空页面。行业里管这种情况叫"后过滤召回塌陷"。本文就围绕这个场景,把塌陷的原因、防线、兜底方案和数据一致性治理一次讲透,重点是回答标题那个问题:全部被过滤掉之后,系统到底该怎么办。

1. "后过滤"塌陷长什么样:一次线上空结果事故复盘

1.1 用户搜索"iPhone 15 蓝色 256G",结果一个都没剩

先还原一个我实际处理过的案例。某电商 App 的商品搜索,当时架构是:

  • 用户输入 query → 先到 Redis 里取一批商品 ID;
  • Redis 里存的是运营配置的热门商品池、类目热销榜、用户最近浏览过的商品等轻量级列表;
  • 拿到这批 ID 后,再去 ES 里根据完整的商品文档做过滤,比如品牌、颜色、存储容量、库存、配送区域、优惠券可用性等;
  • ES 返回过滤后的结果,直接展示给用户。

某天线上监控发现,"iPhone 15 蓝色 256G"这个搜索词的空结果率突然飙到 30% 以上。用户搜索后看到的不是商品,而是"没有找到相关商品,换个关键词试试吧"。

跟进链路数据后真相很简单:Redis 里热销榜的 iPhone 15 有 200 个商品 ID,但 ES 过滤时叠加了五个条件——品牌包含 Apple、颜色为蓝色、存储容量 256G、当前库存大于 0、配送区域覆盖华东。五个条件用 And 串起来之后,200 个 ID 最终只剩 0 个。

问题不在 ES,也不在 Redis,而在于:后过滤本身就是一个天然存在"塌陷概率"的环节。只要过滤条件不是恒真,那么"候选集被全部过滤掉"就是一个概率事件,条件越多、候选集越小,概率就越高。

1.2 Redis先召回、ES再过滤,问题恰恰出在"再"

"后过滤"这个词,对应的是"前过滤"。前过滤是在数据写入时就按规则分桶,查询时直接命中某个桶,比如"华东区商品桶""高转化商品桶",里面放的都是已经符合过滤条件的数据。这种模式的好处是查询快,坏处是规则一变,桶就要重建,灵活性差。

后过滤则是先粗召回、再精过滤,Redis 负责粗召回,ES 负责精过滤。这种两段式架构很常见,但很少有人认真想过:Redis 召回与 ES 过滤之间,存在一条信任边界。

Redis 说"这些是候选",ES 说"这些才符合条件"。两者一旦出现数据不同步、规则口径不一致、召回量过小,信任就崩塌了,表现就是过滤后结果为 0。

从系统架构看,Redis 召回和 ES 过滤其实是两个团队、两套数据、两套更新链路,中间通常只靠消息队列异步同步。只要一次同步延迟,或者一次数据写失败,Redis 里的 ID 在 ES 里就可能是"不存在""已下架""库存为 0"的状态。这种脏数据一旦被过滤逻辑扫过,自然就没了。

1.3 塌陷是概率事件,不是bug

很多人第一次遇到"全部被过滤掉"时,第一反应是查 bug,但查到最后发现每个环节都"没毛病":Redis 有数据,ES 过滤逻辑正确,数据同步也正常。那为什么结果是空?

因为塌陷是概率性故障,而不是确定性 bug。候选集大小、过滤条件数量、用户画像匹配度、并发下缓存状态,这些因素叠加后,某些 query 在某个时间片内就是会被过滤空。

我做过一个统计:当 Redis 召回的候选数量小于 20 个时,过滤后为空的比例超过 15%;候选数量在 100 个以上时,这个比例降到 1% 以下。这不是某段代码写错了,而是数学上必然存在的长尾。如果你的系统没有针对这个概率做防线,那么每天线上都会有小流量用户撞上空结果,区别只是你有没有监控到。

2. 为什么Redis召回的数据,会被ES一把梭清空

2.1 Redis擅长的是"快",不是"精细化过滤"

很多人对 Redis 有误解,觉得 Redis 又 has his own kind of "memory" nuance。实际上 Redis 的核心价值是 O(1) 或 O(logN) 的读写性能和丰富的数据结构,适合存 ID 列表、排行榜、用户最近访问、去重集合这类轻量数据。

但 Redis 不适合做复杂条件过滤。你可以用 SINTER、SUNION 做集合运算,也可以用 Lua 脚本做简单逻辑,可一旦涉及"库存大于 0 且品牌为 Apple 且配送区域覆盖华东"这种多字段联合过滤,Redis 就力不从心了。强行把所有过滤维度都塞进 Redis,要么数据冗余爆炸,要么维护成本高到没法看。

所以在两段式架构里,Redis 的定位是快速缩小范围——从千万级商品里先圈出几百个"可能相关"的候选,真正的精细化过滤交给 ES。这个定位本身没问题,问题在于 Redis 圈出的候选往往只基于一个维度,比如"热销""最近浏览""运营推荐",而 ES 过滤是基于全维度。当用户画像、筛选条件、库存状态这些维度一起压上来时,单维度的候选根本扛不住。

2.2 ES过滤条件一多,And逻辑会"吞噬"整个候选集

ES 里的过滤,本质上是布尔 And 逻辑。你写的每个 filter 子句,都是在做一次"淘汰赛"。候选集在每一轮都会被砍掉一部分,条件越多,最后剩下的就越少。

举一个我常用的数据来说明:

Redis召回候选数过滤条件数单条件平均通过率最终结果数
200180%160
200280%128
200380%102
200570%33
200450%12
20570%0

注意最后一行:候选集小的时候,即使每个条件通过率都不低,连乘之后也会归零。这不是 ES 的问题,是概率问题。如果你的过滤条件里有"库存 > 0"这种实时性很强的条件,而 Redis 里的候选本身是从缓存里来的,那么塌陷概率会更大。

2.3 数据不一致:Redis列表里躺着ES早就删掉的ID

除了概率因素,数据不一致是"全部被过滤掉"最常见的确定性原因。

我见过几种典型的脏数据来源:

  • 商品在运营后台下架了,MySQL 状态改了、ES 也更新了,但 Redis 里的热销榜还是旧数据,因为榜单缓存过期时间是 30 分钟;
  • 运营把某个商品移出活动页,活动页缓存由 Redis 管理,但商品的库存、配送区域等字段由 ES 管理,两边更新不是原子的;
  • 数据同步链路用了消息队列,但某次消费者报错后没有重试,消息丢了,Redis 和 ES 从此分叉;
  • 多机房部署时,Redis 主从切换导致部分数据未同步,ES 那边却是完整的。

ES 过滤时遇到这些"幽灵 ID",要么 doc 不存在被过滤掉,要么 doc 存在但状态字段不满足条件被过滤掉。这也就是为什么,你会看到 Redis 里明明有几十个候选,ES 过滤后一个都不剩。

3. 第一道防线:过滤前先给召回量做体检

既然塌陷是概率事件,而且数据不一致很难完全消灭,那么第一步不是设计兜底,而是在过滤逻辑执行之前加一道体检,尽量别让 ES 有机会把候选集清空。

3.1 召回量小于阈值时,跳过精细化过滤

我建议在 Redis 召回之后、ES 过滤之前,先判断候选集数量。如果候选数量低于某个阈值,就不要走完整过滤链路,而是走"安全过滤"模式。

这里说的"安全过滤",是指只剔除那些绝对不能展示的数据,比如已删除、已违规、未上架。至于库存、配送区域、价格区间这类"软条件",直接忽略。

关键问题是阈值怎么定。我自己的经验是:先看历史日志,统计"过滤后为空"时对应的平均召回量,取一个分位点。比如线上数据显示 95% 的空结果事故中,召回量都小于 30,那阈值就定在 30。另外还要估算一下当前业务里"冷门 query 的召回量分布",不要让阈值误伤正常场景。

代码逻辑上大体是这样:

public SearchResult search(SearchRequest request) { // 1. Redis 召回 List<String> candidateIds = redisRecaller.recall(request); // 2. 召回量体检 if (candidateIds.size() < RECALL_MIN_THRESHOLD) { // 候选太少,不做过细过滤,只做安全过滤 List<Item> safeItems = esFilter.safeFilter(candidateIds); if (!safeItems.isEmpty()) { return buildResult(safeItems, true); } // 安全过滤也空了,才走完整兜底链路 return fallbackChain.fallback(request, candidateIds); } // 3. 正常后过滤 List<Item> filteredItems = esFilter.strictFilter(candidateIds, request); if (filteredItems.isEmpty()) { return fallbackChain.fallback(request, candidateIds); } return buildResult(filteredItems, false); }

这个逻辑看起来简单,但很多人不敢做,怕"跳过过滤"会展示不符合条件的数据。我的观点是:在召回量极小的情况下,展示几条"不完全满足软条件"的数据,远比给用户一个空页面要好。你可以在前端标注"已为您放宽筛选条件",用户是能理解的。

3.2 分级过滤:安全条件、硬条件、软条件各管各的

更进一步的做法,是不要把过滤条件一锅端扔给 ES。把条件分成三档:

条件类型举例过滤失败时怎么办
安全条件商品未删除、未违规、已上架不能放宽,放宽了会出合规风险
硬条件用户主动筛选的品牌、类目、是否有货尽量保留,若为空可提示用户调整
软条件价格区间、配送时效、优惠券可用可以逐级放宽,不影响核心意图

在实现时,ES 的 filter 子句不要一次性全部拼接。先拼安全条件,过滤一次;如果结果为空,说明候选集本身就全是脏数据,直接走兜底。如果安全条件过滤后有结果但不满足硬条件,就先去掉硬条件和软条件,只保留安全条件再查一次。

这里有一个细节:硬条件里的"用户主动筛选"和"系统默认条件"要区分开。用户主动选择了"只看有货",那你要谨慎放宽;如果只是系统推荐流里的默认过滤,放宽后用户感知不强。最简单的做法是,把用户显式选择的筛选条件传到前端,由前端在空结果页给出"清除筛选条件"的按钮,后端同时悄悄返回放宽后的结果。

4. 真正要回答的问题:全部被过滤掉之后怎么办

防线只能降低塌陷概率,不能消除它。所以这一节才是核心:当 ES 过滤后结果为空,下一步怎么走。我会按优先级给出四级兜底链路,每一级都要承担一部分流量。

4.1 一级兜底:透传Redis原始候选,但先剔除安全硬伤

ES 过滤为空时,最直接、成本最低的兜底就是"信任 Redis 一次",把 Redis 召回的原始候选直接返回。

注意,这里不是原封不动全量返回。你需要做一层极轻量的过滤:查一下这些 ID 在 ES 里是否存在、是否处于上架状态、是否被运营强制下架。安全的做法是用es.mget批量查询这些 ID 的基础状态,而不是走完整的过滤链路。

为什么要保留这层轻过滤?因为直接返回已经删除的商品,用户点进去会看到 404 或"商品已下架",体验更差。保留基础状态校验,能避免大多数投诉。

透传时的代码大概是这样:

private List<Item> transparentFallback(List<String> candidateIds) { // 只查基础状态,不查复杂过滤条件 Map<String, EsItem> docs = esClient.mget(candidateIds, "status", "deleted"); return candidateIds.stream() .map(docs::get) .filter(Objects::nonNull) .filter(doc -> doc.status == 1 && !doc.deleted) .collect(toList()); }

透传后,排序默认按 Redis 列表里的顺序来。因为 Redis 列表往往是按热度、运营权重排好的,直接返回比随机排序要好。

4.2 二级兜底:多路召回,别把鸡蛋放在Redis一个篮子里

只靠 Redis 一路召回,本身就是架构缺陷。Redis 里的数据再多,也只是一个视角——热度/运营视角。用户真实意图可能藏在 ES 的倒排索引里、用户行为序列里、向量语义空间里。

我强烈建议把召回做成多路:

  • Redis 路:热销榜、运营推荐、用户最近浏览;
  • ES 路:基于 query 的 BM25 全文召回;
  • 向量路:基于 embedding 的语义召回;
  • 协同过滤路:基于用户行为相似度的"看了又看"。

在 ES 过滤之前,先把多路召回结果做一次合并去重,再统一交给过滤层。这样即使 Redis 路召回的数据被全部过滤掉,其他路召回的 ID 也可能通过过滤,不会一空俱空。

多路合并时要注意权重设计。我的实践是:不提前硬切,把每路召回的 ID 都带上来源标签和分数,合并后先按"是否满足硬条件"过滤,再按综合分排序。同一批候选,Redis 路分数高不代表一定排前面,ES 路的语义匹配分数也要参与比较。

4.3 三级兜底:配置化兜底池,让空结果直接变推荐位

多路召回也可能全部被过滤掉,特别是用户筛选条件特别苛刻时。这时就需要"人为干预"的兜底池。

我在系统里维护了一个运营配置兜底池,本质是一张表/一个配置项:当某个 query 或某个筛选组合过滤后为空,就返回运营预先配置好的推荐商品。比如搜索"iPhone 15 蓝色 256G"过滤为空,兜底池配置了"iPhone 15 全系热销 Top 10",用户看到的就不再是空页面。

这个兜底池有两个实现要点:

  1. 必须可动态更新,用配置中心(如 Apollo、Nacos)管理,紧急时刻运营能改,不需要重启服务、不需要发版;
  2. 要有优先级,先匹配 query 级兜底配置,再匹配类目级兜底配置,最后落到全局兜底配置。

很多人会犹豫:"这不是作弊吗?用户搜的是蓝色,我给推黑色?"——但你要想清楚,空结果对用户的伤害远大于推荐不完全匹配的商品。用户搜冷门组合,本身就说明 ta 的需求不常见,你给一个相关但不完全匹配的结果,再加上"已为您展示更多相似商品"的提示,用户能接受。

4.4 四级兜底:全局热榜,永不落空的最后防线

配置化兜底池需要人去维护,不可能覆盖所有 query。最后一层兜底,是准备一个永不落空的候选池,比如全局热榜 Top 500。

这个池子的特点是:

  • 覆盖全品类,不分 query,任何时候都有数据;
  • 只做安全过滤,不做用户意图匹配;
  • 从 Redis 和 ES 两边定期同步,保证池子里都是有效商品。

当上面三级兜底都为空时,直接返回全局热榜。雖然用户会感觉结果"不太相关",但至少不是空页面,用户还有继续逛下去的机会。热榜同时要承担新用户冷启动、无结果页推荐这两个场景,所以它是整个兜底链里的最后保险丝。

4.5 兜底方案选型对比

兜底级别适用场景优点风险是否推荐
透传Redis候选候选集小、过滤误杀成本低、速度快可能展示不完全满足条件的数据强烈推荐
多路召回合并单路召回质量差召回覆盖面广增加接口耗时强烈推荐
配置化兜底池高频空结果query体验可控、运营干预强依赖人工维护推荐
全局热榜极端冷门场景永不落空相关度低保底必做

我见过的很多系统,只做了透传兜底,没有多路召回,也没有配置化兜底池。结果就是"空结果"变成"不太相关的商品"——虽然比空页面好,但离好的体验还差得远。四层都上,才算是完整的兜底链路。

5. 比兜底更重要的是:让Redis和ES别再"打架"

兜底解决的是"出了问题怎么办",但根本问题是"Redis 和 ES 数据经常不一致"。如果两边数据始终一致,塌陷概率会大幅下降,兜底也只是极少数的保险。

5.1 数据同步:从MQ异步到Canal订阅binlog

Redis 和 ES 的数据同步,常见的有两条链路:

  • 应用双写:业务代码在写 MySQL 后,同时发 MQ 消息,消费者分别更新 Redis 和 ES。
  • Canal 订阅 binlog:让 Canal 监听 MySQL binlog,解析变更后推给下游,下游更新 Redis 和 ES。

两条链路各有优劣。双写逻辑简单,但业务代码侵入强,容易漏发消息;Canal 对业务无侵入,能保证 MySQL 与 ES 的最终一致,但它只管 MySQL,Redis 里的榜单、热度这类衍生数据还得单独处理。

我目前比较推荐的组合是:MySQL 作为唯一数据源,Canal 同步到 ES;Redis 里的榜单/推荐列表由单独的定时任务从 ES 或 MySQL 重建,不直接接收业务写操作。这样 Redis 的数据不是"实时写"的,而是"周期重建"的,反而更容易保证干净。榜单缓存过期时间设为 5~10 分钟,即使重建期间有数据变化,最多也就是几分钟的延迟。

5.2 定期对账:把Redis里的"幽灵ID"清理掉

再好的同步链路,也会因为网络抖动、消息丢失、消费者报错而出现不一致。所以必须有对账任务。

对账的思路不复杂:扫描 Redis 候选列表中的所有 ID,批量去 ES 里查这些 ID 是否存在、状态是否有效,把"幽灵 ID"清理掉。频率可以每天一次,放在凌晨低峰期。

注意,对账任务不要把 ES 当作唯一真相。有些商品在 ES 里可能因为索引重建暂时查不到,简单粗暴地删除 Redis 里的 ID,反而会把好数据误杀。建议先标记为"待确认",连续多次对账都不存在再删除。

抓过一次线上事故后,我把清理策略从"不存在就删"改成了"连续三次不存在才删",误杀率从 3% 降到 0.01% 以下。

5.3 空结果率、透传率、兜底率的立体监控

最后是监控。很多人只监控接口报错,不监控"返回空结果"这种业务层异常,导致塌陷事故一直在发生,却没有任何告警。

我建议至少监控三个指标:

指标定义告警建议
空结果率ES 过滤后为空的请求数 / 总请求数超过 5% 触发 P2 告警
透传率走透传兜底的请求数 / 总请求数超过 10% 说明过滤或召回有问题
兜底率走任意兜底的请求数 / 总请求数超过 15% 需要排查数据一致性

这三个指标要按 query 维度、渠道维度、筛选条件维度拆开看。比如"华东区 + 仅看有货"这个组合的空结果率特别高,可能不是技术问题,而是业务上华东区确实缺货,需要运营补货或调整筛选选项。

6. 落地时最容易被忽略的几个细节

前面把兜底链路讲完了,但真正落地时还有几个小坑,我单独拎出来讲讲。

6.1 透传后的排序、分页、去重怎么处理

透传 Redis 候选时,最容易出现的问题是分页错乱。比如第一页走了正常过滤,用户翻到第二页时 ES 过滤为空,走了透传,两页的数据来自完全不同的集合,用户会觉得"越翻越奇怪"。

最好的办法是:一旦本次请求走了兜底,整个分页都用兜底数据源。前端要感知这个状态,在翻页时带上"兜底模式"的参数,后端统一从 Redis 候选里分页,而不是普通结果一页、兜底结果一页。

去重也别忘了。Redis 候选列表本身一般是去重的,但如果做了多路召回合并,不同路的 ID 可能重复。合并时用 Set 去重,同时保留第一路的分值。

6.2 放宽过滤条件,放到什么程度才算合理

放宽条件不是无脑放。我的建议是排一个"放宽顺序",按业务影响从小到大排:

  1. 配送区域、配送时效;
  2. 价格区间(比如用户选了 3000-4000,放宽到 2500-4500);
  3. 品牌偏好(用户只是默认偏好,不是硬筛选);
  4. 库存状态(这个要谨慎,用户如果主动选了"仅看有货",放宽后体验很差);
  5. 安全合规条件(永远不放宽)。

每次只放宽一级,不要一次把所有条件全砍掉。放宽后如果还有结果,就在前端提示"已为您放宽部分筛选条件";如果放宽到最宽松仍然为空,才走 4.3 和 4.4 的配置化兜底和热榜。

6.3 兜底结果要埋点,更要在前端"说人话"

做兜底不是为了糊弄监控,是为了让用户体验不塌方。所以兜底发生时要埋点,记录:

  • 触发兜底的是哪一级(透传/多路/配置池/热榜);
  • 用户当时的 query 和筛选条件;
  • 兜底结果用户点击率、停留时长、下单率。

有了这些数据,你才能持续优化。比如发现某个 query 经常触发配置化兜底,说明 ES 召回本身有问题,应该去优化召回而不是一味靠兜底撑着。

前端也要配合。透传或放宽条件时,要在页面上明确告诉用户"已为您放宽筛选条件"或"已为您展示更多相关商品"。用户不怕结果不完全匹配,怕的是系统"装死"——明明过滤条件把他想要的东西全滤掉了,还假装什么都没发生。

我在实际项目中还发现一个规律:空结果页如果加上"清除筛选条件"按钮,用户点击率非常高。这说明用户并不是不需要结果,而是筛选条件组合得太死了。与其让用户手动清除,不如后端主动放宽一级条件,再用 UI 告知用户,转化率会明显更好。

最后说一句我自己的体会:后过滤召回塌陷这个问题,单纯加兜底能治好标,但治不了本。真正要长期做的是三件事——召回量体检、数据一致性对账、空结果监控。这三件事和兜底链路加在一起,才能让你的系统在 Redis 和 ES 偶尔"打架"的时候,依然稳稳地给出用户想看的东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 4:41:32

树莓派Pico PIO步进电机非阻塞控制实战:从原理到代码

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 4:41:14

AI Agent如何“上保险”:责任机制设计与工程落地

最近在和几位做 AI 应用落地的朋友交流时&#xff0c;大家不约而同地聊到一个话题&#xff1a;Agent 越来越“能干”了&#xff0c;但一旦它做错事&#xff0c;到底该谁来负责&#xff1f;用户被 Agent 误导下单、Agent 调用工具误删了数据、智能体在无人干预的情况下执行了一笔…

作者头像 李华
网站建设 2026/9/9 4:41:08

IBM x3100 M4安装Win2008/2003 RAID驱动与系统部署详解

简介&#xff1a;IBM X3100 M4服务器所用C100阵列卡的驱动程序资源包&#xff0c;面向企业IT运维、服务器维护人员&#xff0c;用于解决Windows Server 2008、2003&#xff08;含x86/x64&#xff09;环境下阵列卡不被识别或RAID驱动缺失的问题。压缩包共82个文件&#xff0c;涵…

作者头像 李华
网站建设 2026/9/9 4:41:04

Spring Boot水产品交易管理系统设计与实现:订单库存闭环全解析

做毕业设计最烦的事情是什么&#xff1f;大概率是“题目拿了一个月&#xff0c;代码还停在Hello World”。如果是MySQL、SSM那一套的老系统还好&#xff0c;照着crud糊弄一下也能过&#xff0c;可一旦涉及像水产品交易这种带业务场景、带订单流转、带库存和价格联动的管理类系统…

作者头像 李华
网站建设 2026/9/9 4:39:48

代码驱动图表:把架构图工程化,纳入版本控制与自动化流水线

我做架构设计这些年&#xff0c;最怕的不是写代码&#xff0c;而是画图。不是那种随手画给同事看的意思&#xff0c;是正经交付给团队、写进设计文档、贴在 Wiki 里、后续三个月还得继续维护的架构图。你改了一版服务拆分&#xff0c;就得同步改图&#xff0c;改完还得检查有没…

作者头像 李华
网站建设 2026/9/9 4:38:34

DAU异动分析全攻略:从数据拆解到业务行动,面试必会

面试数据分析岗&#xff0c;最容易翻车的一类题不是SQL&#xff0c;不是AB实验&#xff0c;而是这种&#xff1a;面试官给你一张简化过的数据表&#xff0c;告诉你“1月21日DAU跌了&#xff0c;你怎么看”。听起来像日常工作里的一个普通异常&#xff0c;但很多候选人一上来就开…

作者头像 李华