尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

高转化电商搜索推荐系统架构:SpringCloud与ES、Redis、Kafka实战

高转化电商搜索推荐系统架构:SpringCloud与ES、Redis、Kafka实战 做过电商后端的人都有体会搜索和推荐是最能直接拉动成交额的两个模块但也是最容易把系统搞复杂的地方。用户搜出来不相关转身就去竞品推荐推得不精准首页点击率惨淡。更麻烦的是大促流量一来商品同步、搜索请求、行为上报全部叠加在一起系统就容易雪崩。SpringCloud、Elasticsearch、Redis、Kafka这套组合是我在多个电商项目中反复验证过的技术栈也是目前业内做搜索推荐系统用得最多的一套搭配。SpringCloud拆微服务、ES扛搜索和向量检索、Redis挡缓存和分布式锁、Kafka做异步削峰和数据同步。这套方案能够覆盖商品数据同步、搜索联想、语义检索、个性化推荐、高并发缓存等完整的业务链路。这篇文章我就围绕这套组合把从商品数据进入ES、到用户发起搜索请求、再到推荐结果落地的全流程拆开讲一遍。重点包括Canal监听MySQL的binlog同步到ES的细节、ES索引和查询的建模思路、Redis在缓存和分布式锁上的用法、Kafka在同步链路和用户行为分析中的配置以及我在实际项目里踩过的一些坑和排查方法。适合正在搭建搜索推荐系统、或者准备做技术选型的后端开发、架构师参考。1. 整体架构设计四个组件各管哪一块1.1 为什么非要搞「搜索推荐一体化」早期的电商系统里搜索和推荐往往是两套独立的东西。搜索基于关键词匹配数据库推荐则是定时任务离线算好结果放在缓存里。这样做的问题很明显一是数据重复建设商品信息既要在数据库存一份、又要在搜索引擎同步一份、还得给推荐系统准备一份二是响应速度跟不上数据库的模糊查询在千万级商品面前基本属于自杀式操作三是用户行为和实时反馈完全接不上用户刚看完某件商品推荐结果里还是昨天算好的旧数据转化率自然上不去。把搜索和推荐放到一个统一的架构里处理核心思路是让数据只有一个来源也就是MySQL里的商品库和用户行为库然后通过同步机制把数据分发到ES、Redis、Kafka这些组件里各取所需。搜索需要的是商品的结构化字段和文本相关性推荐需要的是用户行为序列和商品的实时热度缓存需要的是高并发场景下的热点数据消息队列负责把这些数据变更和用户行为异步地流转起来。这套设计的价值不只是技术上优雅它直接关系到电商最核心的指标转化率。搜索准了用户找到想要的东西下单概率自然高推荐准了用户逛的时间更长逛得越久买得越多。这也是我把这套链路称为高转化引擎的原因。1.2 四个核心组件的职责边界很多人做技术选型容易犯一个错就是什么组件火就用什么结果把系统用得四不像。在搜索推荐这个场景里SpringCloud、ES、Redis、Kafka各自有非常清晰的分工谁也别越权。SpringCloud是微服务的基础设施负责把整个系统拆分成多个独立部署的服务模块。商品服务管商品数据搜索服务管ES查询推荐服务管推荐策略用户服务管行为数据订单服务管交易闭环。同时它还提供网关路由、服务注册发现、配置中心、熔断降级这些微服务治理能力。举个例子网关层可以把/api/search/**的请求路由到搜索服务/api/recommend/**路由到推荐服务前后端联调的时候统一走网关服务之间的调用关系也清晰可控。Elasticsearch承担的是全文检索和向量检索。电商搜索里用户输入“白色纯棉T恤女”你要能同时匹配到标题、类目、品牌、属性等多个字段并且按照相关性排序这是数据库的LIKE查询做不到的。ES的倒排索引机制天生就是干这个的。同时如果要做语义搜索或者以图搜图ES 8.x以后内置的向量检索能力也能直接支撑不需要额外再搭一套向量数据库。Redis在链路里的角色是减轻后端压力的缓存层同时也是分布式锁和热榜数据结构的载体。搜索推荐系统有一个特点就是热点非常集中。某件爆款商品在搜索结果里被成千上万人点击首页推荐位被反复请求这些高频热点如果每次都打到ES甚至MySQL系统必挂。Redis的单线程模型和内存存储让它能承受几十万QPS的读请求加上各种丰富的数据结构做热榜、搜索建议、用户行为临时存储都非常顺手。Kafka是系统的数据动脉。MySQL的binlog变化通过Canal写入Kafka再异步同步到ES这个过程叫数据管道用户在前端的点击、浏览、加购行为也通过Kafka异步采集和分析不阻塞主流程大促期间的搜索请求暴增也可以通过Kafka削峰填谷先入队再逐步处理。Kafka的高吞吐和消息持久化能力保证数据不丢、不阻塞是整条链路的稳定性基石。1.3 业务模块与消息流向设计一个典型的搜索推荐系统微服务模块大概可以拆成这些商品服务product-service负责商品CRUD、上下架、库存管理是商品数据的源头。搜索服务search-service对接ES提供搜索、筛选、排序、搜索建议接口。推荐服务recommend-service整合用户行为、商品热度、协同过滤结果产出推荐列表。用户行为服务behavior-service接收用户点击、浏览、收藏、加购行为写入Kafka。异步任务服务async-worker消费Kafka里的商品变更和用户行为消息处理数据同步、行为分析等任务。数据流向大概是这样的运营在后台修改商品信息商品服务更新MySQL后Canal监听到binlog变化把变更事件发送到Kafka的product-change主题异步任务服务消费这个消息再调用ES的写入接口更新索引。用户在前端搜索商品搜索服务先去Redis查热门搜索词和缓存结果缓存没有就查ES查到结果后把用户行为点击事件发到Kafka的user-behavior主题推荐服务消费行为数据更新用户画像和商品热度。用户再次请求首页推荐时推荐服务就能根据实时计算的热度和用户偏好返回个性化结果。这套链路的关键设计理念是异步解耦和高并发缓冲。搜索链路中最耗时的ES查询独立成服务缓存层挡在前面行为采集和商品同步全部异步化任何一环出问题都不至于导致整个系统不可用。2. 商品数据同步链路从MySQL到ES的异步管道2.1 数据同步方案对比为什么选Canal加Kafka商品数据从MySQL同步到ES业内常见方案主要有三种业务双写、定时任务全量同步、Canal监听binlog增量同步。业务双写是指业务代码在操作MySQL之后再调用ES的写入接口。这种方式实现简单但耦合度极高。每次商品变更都要写两个地方如果ES写入失败MySQL和ES的数据就不一致了还得额外做补偿。而且一旦后来加了新的数据消费方比如推荐系统也需要商品数据就得改业务代码非常不优雅。定时任务全量同步就是定期把MySQL里的全量商品数据捞出来批量写入ES。这种方式适合数据量小、实时性要求不高的场景。但电商商品数据的实时性要求很高商品价格改动、库存变化、上下架状态都需要尽快反映到搜索结果里定时同步一小时一次根本顶不住。Canal方式是最优解。Canal是阿里巴巴开源的MySQL binlog解析组件它伪装成MySQL的从库实时接收主库的binlog变更日志然后把变更事件转成JSON格式发给Kafka。业务代码完全无感知MySQL是什么数据ES最终就同步成什么数据增量实时性能够做到秒级。我对这套方案的评价是数据同步本该就是旁路的事不应该侵入业务代码。2.2 从Canal部署到ES写入的完整流程Canal加Kafka同步MySQL到ES整体流程可以分成四个环节Canal配置、Kafka主题设计、消费处理、ES写入。Canal的部署需要先开启MySQL的binlog日志设置为ROW模式然后创建一个有权限的账号给Canal使用。在Canal的instance.properties配置里指定数据库地址、要监听的表名比如product表、以及Kafka的地址。这里给一个Canal输出到Kafka的关键配置示例# canal.properties 关键配置 canal.serverMode kafka kafka.bootstrap.servers 192.168.1.10:9092,192.168.1.11:9092,192.168.1.12:9092 # instance.properties 关键配置 canal.instance.master.address 192.168.1.20:3306 canal.instance.dbUsername canal canal.instance.dbPassword canal123 canal.mq.topic product-change canal.mq.partitionsNum 6把监听主题设置为product-change并且分了6个分区目的是让下游消费端可以并行处理提高吞吐量。如果只有1个分区消费端最多只能有一个消费者实例在消费同步速度必然受限制。消费端收到Canal消息后消息格式大致是这样的{ type: UPDATE, database: mall, table: product, data: [ { id: 1001, title: 纯棉白色T恤, price: 79.00, stock: 500, status: 1, update_time: 2025-01-15 10:30:00 } ] }消费程序拿到消息后根据type字段决定做写入还是删除。INSERT和UPDATE就调用ES的批量写入接口DELETE就调用ES的删除接口。ES写入这里要特别说一点不要逐条写一定要批量写。ES的批量写入接口_bulk一次可以提交几百上千条数据吞吐量是逐条写入的几十倍。我在项目里是攒够500条或者1秒钟批量提交一次两个条件先到先触发。消费端用Java实现的话大概思路是先用一个队列缓存待写入数据然后定时或者定量触发批量提交。2.3 同步链路的高可用与幂等设计数据同步链路最容易出的问题就是消息重复消费和消息丢失。Kafka的消费语义是至少一次也就是说消费者可能在处理完数据之后、提交offset之前挂掉重启之后Kafka会重新投递一遍之前的消息。这时候如果不做幂等ES里的数据就可能被重复写入。幂等处理的办法很简单ES写入时带上商品ID作为文档IDES的写入逻辑是同一ID的文档反复写入后者覆盖前者天然幂等。删除操作也是同理重复删除不报错。所以只要保证写入请求里的文档ID是商品ID重复消费就不是问题。消息丢失的情况主要出现在Canal到Kafka这一段。Canal默认的ack机制是等消息发到Kafka之后才确认如果Kafka不可用Canal会阻塞等待。我们项目里把Canal的canal.mq.enableDynamicTopicPartition关了改为静态分区配合Kafka的acksall配置基本能保证数据不丢。提到ack配置Kafka生产端的acksall会把消息确认延长时间但换来的是更强的可靠性。在商品同步这种场景里数据可靠性比极致的吞吐量更重要所以必须用acksall。如果是用户行为上报这类允许少量丢失的场景可以适当放宽来换吞吐量。3. 搜索链路的实现从关键词输入到ES结果返回3.1 搜索请求的完整流程用户在搜索框输入“连衣裙”三个字点击搜索前端发起请求。这个请求首先到达SpringCloud网关网关做了路由匹配发现/api/search/**这个路径开头是搜索服务的路由就把请求转发给搜索服务。搜索服务的处理流程是先查Redis缓存有就直接返回没有就去ES查询查到结果后写回Redis。为什么先查缓存电商搜索有一个典型的二八定律80%的搜索流量集中在20%的热门关键词上。像“连衣裙”这种热门词一天可能被搜索几百万次。如果每次都打ESES的压力会非常大。Redis挡这一层能够把90%以上的重复请求拦截在缓存层。搜索请求在服务端的处理逻辑大致是这样的从请求参数里取出关键词、页码、每页条数、筛选项。先查Redis中这个关键词的搜索建议把相关的联想词返回给前端。查Redis缓存key的设计是search:{keyword}:{page}:{size}缓存时间一般设置60秒到5分钟。缓存没命中拼ES查询DSL执行搜索。搜索结果拿到后解析商品ID列表回查商品服务获取最新的价格、库存信息。结果写回Redis同时异步发送用户行为消息到Kafka。注意第5步很多新手做ES搜索都会踩一个坑把商品的所有字段都塞进ES索引然后搜索完直接从ES的source里取全量信息返回给前端。这样做的问题有两个一是索引体积会很大存储成本高二是商品的价格、库存经常变动同步到ES会有延迟用户看到的可能是过时的数据。更合理的做法是ES里面只放搜索和排序需要的字段比如商品ID、标题、类目、品牌、销量、评分等价格和库存这种强实时性数据在搜索结果出来后从Redis或商品服务实时获取。3.2 商品索引的mapping设计与查询语法ES索引的mapping设计直接决定搜索质量和写入性能。这里给一个商品索引的简化版mapping{ mappings: { properties: { id: { type: long }, title: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, category_name: { type: text, analyzer: ik_max_word, search_analyzer: ik_smart }, brand_name: { type: keyword }, price: { type: double }, sales_count: { type: integer }, score: { type: float }, status: { type: integer }, create_time: { type: date } } } }title和category_name字段使用的是IK分词器这是中文搜索的标配。ik_max_word做索引切分尽可能多地切出词项ik_smart做搜索切分更偏向于保留长词。这种组合能提高中文搜索的召回率同时保证搜索结果的精准度。举个例子搜索“纯棉T恤”ik_max_word会把“纯棉”“棉T”“T恤”都切出来而ik_smart可能只切出“纯棉”“T恤”两个词。查询的时候用bool查询组合多个条件。以“白色纯棉T恤”为例查询逻辑是{ query: { bool: { must: [ { multi_match: { query: 白色纯棉T恤, fields: [title^3, category_name^2, brand_name], type: best_fields } } ], filter: [ { term: { status: 1 } }, { range: { price: { gte: 0, lte: 500 } } } ] } }, sort: [ { _score: desc }, { sales_count: desc } ], from: 0, size: 20 }这里的^符号是权重title的权重是3category_name是2brand_name是1。意思是标题命中的商品比类目命中的商品排序更靠前。status字段用term精确匹配filter不参与评分但能过滤结果还能利用ES的缓存机制提升查询性能。价格区间用range过滤。排序这里也有讲究。默认情况下ES按相关性得分排序但电商场景不能光看相关性。两件商品都匹配“纯棉T恤”一个是销量10万的爆款一个是销量100的普通商品用户大概率会选爆款。所以我把sales_count作为第二排序条件相关性相同的情况下销量高的排前面。更高级的做法是用function_score把销量、评分、转化率这些业务指标加权计算到得分里实现更精细的排序策略。3.3 向量检索做语义搜索的实践传统的关键词搜索有一个明显的短板用户搜“怎么搭配显瘦”但商品标题里的文案是“修身显瘦连衣裙”关键词搜法不一定能匹配上。这时候就需要语义搜索。ES从8.0开始支持向量检索。基本思路是把商品的标题、描述通过Embedding模型转成向量用户搜索的关键词也转成向量然后在ES里做相似度计算找到语义上最接近的商品。这套能力配合现有的文本检索可以显著提升搜索的召回率和相关性。在实际落地中我碰到的最大问题是搜索延迟。向量检索的计算量比关键词检索大得多数据量一上来每次向量计算都可能把查询响应拖到几百毫秒甚至一秒以上。“es向量检索时间太长”是搜索推荐系统里一个常见痛点。后来采用的办法是混合检索加候选集粗排。先用关键词检索快速召回1000条候选商品这1000条再拿去做向量相似度重排。这样向量计算只在1000条数据上做而不是全索引扫描查询响应可以控制在100毫秒以内。同时ES的HNSW算法参数也要调m参数控制在32左右ef_construction控制在100左右过大的值会显著增加内存和写入耗时而检索收益并不明显。另外向量字段建议单独放到一个索引或者冷热分离存储里避免和文本字段互相影响性能。3.4 搜索建议功能用Redis的有序集合实现搜索框的联想要做到毫秒级响应这个功能我在项目里是用Redis的有序集合Sorted Set简称ZSet实现的。用户每搜索一次就把关键词作为member追加计数score是这个词的热度值。搜索建议查询的时候直接用ZSet的逆序查询取热度最高的前10个词返回。# 用户搜索一次热度加1 ZINCRBY hot_search_words 1 连衣裙 # 用户搜索另一个词 ZINCRBY hot_search_words 1 纯棉T恤 # 查询热度前10的搜索词 ZREVRANGE hot_search_words 0 9这套方案实现简单、性能极高商品搜索场景下的搜索联想和热词榜单都能覆盖。需要注意两个问题词条数量会无限增长所以要定期清理低热度词每小时的搜索词热榜可以做多个ZSet key比如hot_search_words:20250115:10代表1月15日10点的榜单方便后续做基于时间段的分析。4. 高转化推荐的核心逻辑从个性化到热榜4.1 用户行为数据怎么用Kafka收集和分析推荐系统的输入是用户行为。用户在什么时间、看了什么商品、停留了多久、是否加购这些行为数据是推荐算法最宝贵的原料。行为采集的前端SDK把用户的点击和浏览行为封装成标准格式上报到行为服务行为服务把数据写入Kafka的user-behavior主题。这个过程是异步的用户点击之后不需要等待数据处理完成所以对用户体验完全没影响。Kafka在这里充当了缓冲区即使瞬时涌入大量行为数据Kafka也能扛住下游的消费者根据自己的处理能力慢慢消费不会造成数据丢失。Kafka消费端的处理逻辑是按照行为类型分流。点击、浏览类行为更新商品的实时热度计数写入Redis的ZSet加购、下单类行为权重更高更新用户偏好标签同时保留一份原始行为日志写入批量存储供离线分析使用。注意用户在Redis里的偏好标签统一用JSON结构存储key设计为user:preference:{userId}TTL设置为7天这样既能保证个性化推荐的实时性又不会让用户画像数据无限膨胀。4.2 推荐系统的个性化与热门兜底策略推荐系统最怕的是冷启动新用户没有行为历史不知道推什么。我的做法是个性化推荐和热门榜结合没有用户画像就用热门商品兜底。推荐服务的核心逻辑是读取用户偏好标签匹配商品索引里对应的类目或品牌按热度排序取前20个如果用户没有偏好标签或者匹配结果为空就返回全局热门商品榜。全局热门商品榜的数据就是从前面说的Redis ZSet热榜里取的。这里还有一个关键细节推荐结果需要做去重和多样性控制。连续推两件一模一样的T恤用户会觉得推荐系统是个摆设。我在同一类目下最多取3件商品并且通过ES的terms聚合或者查询后再做一次业务层去重。推荐结果写入Redis缓存key设计为recommend:{userId}:{page}缓存时间10分钟。这个时间不能太短否则推荐服务的计算压力太大也不能太长否则用户行为的最新变化体现不出来。4.3 商品热榜的动态更新机制热榜数据是推荐系统和搜索结果排序的共同依赖。商品每被点击一次就在Redis里给这个商品的热度加权。这里要注意权重的设计单纯的点击次数不能完全反映商品的热度因为用户可能误点。我参考了比较常见的加权策略给不同行为设置不同的权重系数点击权重1、收藏权重3、加购权重5、下单权重10。另外热度是会衰减的。一个三天前的爆款和一个今天的爆款哪怕累计热度一样今天的显然更有推荐价值。我用了一个简单的时间衰减因子每天凌晨把前一天的ZSet数据乘以0.8的衰减系数再合并到当天的数据里。这样做的好处是系统始终能反映近期的用户偏好变化不会让历史爆款霸榜太长时间。4.4 商品下架和库存不足的实时过滤推荐和搜索的结果里如果出现已下架或者无货的商品用户体验特别差。用户点了半天发现买不了转化率直接下降。ES索引里的status字段就是干这个的查询时统一过滤status1的商品。价格和库存这种高频变化的数据严格来说应该放到Redis里推荐结果排序后回查Redis确认商品状态。Redis的value可以直接存商品的核心状态JSON包括价格、库存、上下架状态由商品服务在变更时更新。这样推荐接口的每个商品都能快速校验是否可售避免出现用户看到心仪商品但点进去显示已下架的尴尬情况。5. 关键配置详解Kafka、ES、Redis参数调优实录5.1 Kafka集群部署与核心参数配置Kafka在搜索推荐系统中的角色太重要了整个数据流动都靠它所以Kafka集群的部署和配置值得单独说。Kafka集群至少三台机器起步每台机器上部署一个Broker。版本选型我用了Kafka 3.0加内置的KRaft模式不再依赖ZooKeeper部署和运维都简单不少。网上搜kafka集群安装能搜到不少教程但大多是基于旧版的ZooKeeper模式其实新版本用KRaft模式更省事直接通过kafka-storage.sh格式化日志目录然后启动即可。搜索推荐场景里的三个核心topic我来分别说明。第一个是商品变更主题product-change分区数建议6到12个副本因子至少2retention时间7天生产端acksall。第二个是用户行为主题user-behavior分区数可以多一些比如12个因为这个主题的流量最大retention时间1天就够了acks1可以提高吞吐允许极端情况下少量数据丢失。第三个是订单结果主题order-result这个主题的消费端需要保证数据不丢失所以acksallretention时间7天。消费者端的配置同样重要。enable.auto.commit建议设置为false改为手动提交offset。手动提交的意义在于数据处理的可靠性由消费端自己控制。处理成功再提交offset处理失败不提交下次重启还会消费到这条消息不会丢数据。当然配合幂等写入即使重复消费最终结果也是正确的。关于“kafka生产消费命令启动一次会一直运行吗”这个问题我在新手群里被问过很多次。生产端的kafka-console-producer.sh启动后进入交互式输入模式你输入一行消息就发送一条CtrlC退出。消费端的kafka-console-consumer.sh --from-beginning启动后会一直监听有新消息就打印没有消息就一直挂着也是CtrlC退出。这两个命令是用来调试和验证集群连通性的不是生产使用的工具。生产环境里如果遇到消息积压用kafka-consumer-groups.sh查看消费者组的lag值lag表示积压的消息条数这个命令是排查问题的利器。5.2 ES索引调优与存储空间优化ES的调优可以从写入性能和查询性能两个维度来考虑。写入性能方面商品全量同步或者重建索引的时候高峰期数据的写入压力很大。ES默认的refresh_interval是1秒意味着每秒钟都会生成一个新的Lucene分段频繁分段会造成大量的IO和CPU开销。在批量导入阶段建议把refresh_interval临时调整为30秒甚至-1关闭刷新批量导入完成后再改回来。同时关闭副本number_of_replicas0导入完成后开启副本。这两招能让写入速度提升好几倍。查询性能方面最有效的优化是索引分片数量的设计。分片数不是越多越好每个分片都是一个Lucene索引分片过多会导致查询时需要聚合多个分片的结果反而增加开销。我常用的经验值是一个分片控制在30GB左右举例来说现有500GB数据设置16个分片各30多GB的量级就比较合适。如果是刚起步的数据量先设置5个分片后续通过索引模板支持扩容即可。关于ES存储空间优化重点在于索引映射的设计。字段能不用text类型就不用精确匹配类字段用keyword不需要的字段在mapping里直接index: false不索引但可以存储关闭_source字段的存储或者用source filtering只保留必要字段。ES里占用存储空间的大头其实是倒排索引和_source精简字段直接能显著降低存储成本。另外定期清理旧索引比如日志索引保留30天就够了用索引生命周期管理工具ILM自动处理。5.3 Redis的序列化、数据类型选择和主从部署Redis在搜索推荐场景里使用的数据类型很多我逐个说明。热榜和搜索建议用的是ZSet这个前面已经讲过了。缓存搜索结果使用String类型value是JSON序列化后的结果。用户行为偏好标签使用Hash类型field是标签名value是标签对应的权重或者分数。分布式锁用String加SET key value NX EX命令实现。整个看下来Redis的常见类型在这个项目里都能找到用武之地。关于Redis的序列化问题很多新手直接用JDK默认的序列化方式结果存进去的消息是乱码字节其它系统读不出来。推荐统一用JSON序列化工具比如Fastjson或者Jacksonkey和value都转成可读的字符串。Redis Desktop Manager是我常用的可视化管理工具排查key是否存在、看TTL、查看某个key的value都非常方便比命令行直观得多。部署上面Redis必须用主从加哨兵模式条件允许就上Cluster集群。主从模式下写操作在主节点读操作分发到从节点在主节点数据量过大的时候能够显著分担读压力。用Docker部署主从的时候有三件事要特别注意一是通过配置文件启动而不是默认配置二是从节点必须配置replicaof指向主节点三是开启appendonly yes开启持久化避免重启丢数据。Redis分布式锁在搜索推荐系统里的典型应用场景是刷新缓存。多个实例同时发现缓存过期同时去查ES写缓存会造成缓存击穿。用一个分布式锁让只有一个实例去执行查询和写缓存的逻辑其它实例等待。这个锁的失效时间不能设得太短否则业务还没执行完锁就释放了也不能太长否则万一持有锁的实例挂了其它实例要等很久。一般是设置30秒然后配合续期机制。如果不想自己实现续期直接使用Redisson框架它内置了看门狗自动续期功能。6. 常见问题排查与实战避坑6.1 搜索推荐链路中的典型问题速查我把做这个系统过程中实际遇到的、以及社区里被问得比较多的问题整理成了一张速查表方便遇到类似问题时快速定位。问题现象可能原因排查思路与解决建议ES搜索结果和MySQL数据不一致canal同步延迟或消息丢失查看Kafka里product-change主题是否有积压用消费组工具查lag再查消费者的日志确认写入是否成功搜索响应变慢超过500msES查询语句不够优化或缓存未生效先看Redis命中率再看ES慢查询日志检查是否缺少过滤条件、排序字段是否做了doc_values优化大促期间消息积压严重消费者处理能力不足Kafka分区数不够增加消费者实例数不超过分区数检查消费端是否有批量处理逻辑逐条处理必然慢推荐结果总是旧的用户行为不生效行为数据消费异常Redis缓存时间过长查用户行为topic的消费lag出现积压说明行为服务异常适当缩短推荐缓存的TTL缓存雪崩大量缓存key在同一时间过期缓存过期时间增加随机值比如基础5分钟加随机0到60秒避免key集中失效Redis内存暴涨热榜ZSet和缓存数据无限增长给ZSet加词条上限定期清理低热度词缓存key加TTL大key拆分6.2 缓存穿透、击穿、雪崩的应对实践搜索推荐是高并发场景缓存问题几乎是每天都要面对的。缓存穿透是指查询了一个不存在的商品ID每次请求都打到数据库。最简单的解决办法是布隆过滤器把所有正常商品ID初始化到布隆过滤器里请求来了先过过滤器不存在就直接返回空。还有一种做法是空值缓存查询结果为空时也在Redis里缓存一个空值TTL设置短一些比如60秒。缓存击穿是指某个热点商品缓存过期瞬间大量请求同时涌入。用前面说的Redis分布式锁可以解决让一个线程去查源数据其他线程有阻塞等待和快速降级两种策略。快速降级是拿到不锁的线程直接返回旧的缓存数据或者降级提示用户体验上损失最小。查询源数据的线程查完之后写回缓存后续请求就都能命中缓存。缓存雪崩的经典应对方案是TTL加随机值以及永久热点数据主动更新。对于搜索推荐场景头部商品的热度非常高这类数据可以设置较长的TTL甚至不设置过期时间由后台定时任务在业务低峰期主动刷新。这样即使部分缓存过期也不会导致所有热点数据同时失效。6.3 SpringCloud微服务落地时的几个坑SpringCloud是整个系统的基础设施几个常见的坑值得记录一下。第一个是网关路径前缀的问题。默认情况下SpringCloud Gateway会把请求路径直接转发给下游服务如果你的服务里接口路径是/api/search/search但你的服务只注册了/search/search就匹配不上。我习惯的做法是网关层统一做StripPrefix把路径里的/api前缀剥掉再转发例如请求路径/api/search/search经过网关后变成/search/search这样下游服务的接口定义可以更干净。第二个是服务间调用超时导致的雪崩效应。推荐服务在调用搜索服务时设置3秒超时但搜索服务可能在3秒内返回不了导致推荐服务自己也超时进而拖垮前端接口。给每个服务都加上Feign的超时配置和线程池隔离必要时开启熔断降级防止单个服务的故障扩散到整个链路。第三个是配置管理不统一。微服务数量多了以后每个服务的配置散落在各自的application.yml里改一个配置要登录好几台机器效率低还容易出错。我用SpringCloud Config或者Nacos统一管理配置配置变更通过消息推送到各个服务不用重启就能生效。特别是ES地址、Kafka地址、Redis地址这种公共配置统一管理能避免很多低级错误。6.4 搜索推荐系统的压测与容量评估系统上线前一定要做压测不压测的系统上线就是在赌运气。压测的核心指标是确认系统在预估流量下的响应时间和资源水位。先用业务数据推算出峰值QPS。比如往年双11的搜索峰值是每秒5000次搜索请求那就要保证搜索服务单机至少能扛住这个量级同时Redis、ES、Kafka各自的水位都不能打满。压测时我常用JMeter构造混合场景搜索结果、推荐接口、行为上报按比例并发持续压测5分钟以上观察各项指标。压测发现系统瓶颈后扩容抓大放小。优先级是先看Redis命中率是否足够高命中率低于90%就加缓存再看ES的CPU和查询耗时CPU超过70%就考虑扩容或者优化查询最后看Kafka的消费lag只要消费能力跟得上Kafka就不是瓶颈。7. 实际操作中的一些体会做搜索推荐系统这几年最大的感受是这个领域看似技术点很多ES、Redis、Kafka、SpringCloud每一个单拿出来都有大量资料但真正有挑战的是把它们组合起来形成一个完整可用的业务闭环。数据怎么流转、消息怎么保证不丢、缓存怎么防止击穿、搜索怎么保证实时性每一个环节出了问题最终都是用户来承担代价。如果让我给正在做或者准备做这套系统的朋友一个建议我建议不要一上来就追求复杂的算法和高大上的架构。先把最基础的链路跑通MySQL的商品数据能实时同步到ES用户能搜到商品搜索和推荐的高频请求能被Redis挡住Kafka能稳定承接行为数据的洪峰。这条链路稳定了再去优化相关性排序、引入向量检索、调优推荐策略。技术永远是为业务服务的搜索推荐系统的最终目标不是用了多少先进组件而是用户能不能更快、更准地找到他想要的商品并最终完成下单。
返回列表