ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringCloud+Vue构建电子图书推荐系统:微服务拆分、爬虫与分布式锁实战

SpringCloud+Vue构建电子图书推荐系统:微服务拆分、爬虫与分布式锁实战 做一个豆瓣风格的电子图书推荐系统说起来简单真要把爬虫采数据—推荐算匹配—微服务扛并发—前端做体验这条链路完整跑通坑远比想象中多。尤其是当你选了SpringBootVueSpringCloud这套组合拳还要强行上分布式很多事情就不再是写个接口那么简单了。这篇文章不打算讲那种悬浮在PPT里的架构而是把这套系统从零到落地过程中最容易被忽略、最容易翻车的环节一个一个拆开聊。全文围绕微服务拆分、爬虫策略、分布式锁与幂等、推荐策略、前后端联调这几个核心板块展开按我实际踩坑的顺序来讲。1. 项目背景与整体技术选型为什么是SpringCloud而不是单机一把梭先交代一下我在做什么。这个项目的目标很直接爬取豆瓣公开的图书条目数据加工成结构化信息构建电子图书库然后基于用户行为做个性化推荐最终以Web应用形式呈现支持关键词检索、分类浏览、评分预测和热门榜单。听起来是个人人都能做的图书管理系统但要往上叠微服务分布式这顶帽子整个设计思路就得换一遍。1.1 单体应用在什么阶段开始卡脖子很多人的第一版推荐系统就是一个SpringBoot单体项目acontroller包天下一个MySQL库扛所有。图书数据几万条还好但一旦爬虫持续写入、用户行为日志不断累积、推荐计算要频繁读取全量特征单体的脆弱点就暴露出来了爬虫模块和推荐模块耦合在一起爬虫一旦卡住或反爬触发整个用户请求链路都会被拖慢甚至OOM。搜索引擎和推荐引擎对资源的需求完全不同。搜书名是IO密集算相似度是CPU密集挤在同一个JVM里互相干扰严重。分布式锁、消息队列、异步任务这些机制在单体里虽然也能实现但代码边界很容易变成一团乱麻。所以这个项目从一开始就定了基调哪怕前期只有一台服务器也要按微服务的思路去拆为后续水平扩展留好余地。这也是很多面试官喜欢追问的点——你为什么分那么多服务如果你能答出爬虫写库和推荐计算解耦避免IO与CPU互相抢占用户行为采集独立成服务方便后续上流式计算这比背十遍微服务概念都有说服力。1.2 技术栈选定的理由整套系统的骨架如下模块技术选型承担的职责服务注册与发现Nacos / Eureka服务实例注册、健康检查、配置中心网关Spring Cloud Gateway统一鉴权、路由转发、限流图书爬虫服务SpringBoot HttpClient Jsoup XXL-Job抓取豆瓣图书数据、解析、清洗、落库图书管理与搜索SpringBoot Elasticsearch MySQL图书元数据存储、全文检索用户行为服务SpringBoot Redis Kafka可选收藏、评分、浏览历史采集推荐服务SpringBoot PythonTF-IDF/协同过滤基于内容和协同过滤生成推荐列表前端Vue 3 Element Plus ECharts用户端图书浏览、推荐展示、管理端数据看板分布式基础设施Redis分布式锁 RabbitMQ异步解耦缓存、消息通知、任务分发选SpringCloud而不是Dubbo核心原因是生态完整度。整个项目里有网关、有配置中心、有负载均衡、有熔断降级SpringCloud全家桶配起来最省心Dubbo强在RPC性能和治理但网关层、配置中心这些还是要另配。做图书推荐这种偏业务型的系统SpringCloud更顺手。2. 微服务拆分的第一道坎图书爬虫模块的独立设计与反爬对抗爬虫是这个系统最脏最累的活但也是最能体现工程能力的地方。很多人一听到爬虫就以为写个for循环套Jsoup就完事了真去爬豆瓣图书页你会发现豆瓣的反爬策略虽然不算最狠但绝对够恶心请求频率高了封你IP请求头不对直接返回403页面结构时不时微调字段解析错位是家常便饭。2.1 为什么要把爬虫单独拆成一个服务我的做法是让爬虫服务完全独立于其他业务模块只通过消息队列和数据库变更来通知下游。这样做的直接好处是爬虫任务可能出现长时间阻塞、超时甚至死循环独立部署后最多挂掉自己不影响用户正在用的搜索接口。爬虫的数据写入往往是批量、高频的如果直接写主业务库会造成锁竞争独立服务配合独立库表可以把脏活隔离掉。后续要加采集源或调整采集策略时只改爬虫服务不必重新发布推荐服务或网关。还有一个隐藏优势爬虫服务需要频繁迭代解析逻辑每次改动都要快速验证独立出去之后可以单独走一套发布流程不用跟着整个微服务大版本走。2.2 爬虫采集策略与解析的实操细节采集链路是分层的我把它做成四步流水线种子URL管理把豆瓣图书分类页的URL作为种子入库到crawl_task表每个任务带上优先级和状态字段。请求调度从任务表捞待抓取的任务通过HttpClient发送请求。这里最关键的三个点是对User-Agent、Referer和Cookie的管理。public class CrawlHttpClient { private static final String[] UA_POOL { Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36, Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0 Safari/537.36 }; public static CloseableHttpClient buildClient() { return HttpClients.custom() .setUserAgent(UA_POOL[new Random().nextInt(UA_POOL.length)]) .setDefaultRequestConfig(RequestConfig.custom() .setConnectTimeout(5000) .setSocketTimeout(10000) .build()) .setConnectionTimeToLive(30, TimeUnit.SECONDS) .build(); } }解析与清洗用Jsoup解析HTML抓住#content里的书名、作者、出版社、出版年、ISBN、评分、评价人数等字段。解析时最容易出的问题是豆瓣偶尔会用不同结构的HTML渲染同一本书比如有时作者区域是div classpub有时又是span。我的兜底方案是先用选择器组合尝试解析不到就降级为正则提取再不行就丢弃该条并记录日志后面人工补采。落库与消息通知清洗后的数据通过MyBatis-Plus批量写入图书表写入时用ISBN做唯一键做去重。写完发送一条MQ消息给搜索服务通知它同步增量数据到Elasticsearch。2.3 反爬三件套限速、重试、代理这一块属于说破了不值钱但不做必然翻车的内容。限速豆瓣单IP的容忍度大约在每秒2-3次请求超过了大概率触发验证。我给爬虫设置了令牌桶限流核心是控制单任务抓取速率。public class RateLimiter { private final Semaphore semaphore; private final int maxPermits; public RateLimiter(int maxPermits) { this.maxPermits maxPermits; this.semaphore new Semaphore(maxPermits); } public void acquire() throws InterruptedException { semaphore.acquire(); // 每秒放一个令牌超过即等待 } public void release() { semaphore.release(); } }重试机制对于403或5xx响应不做盲目重试而是采用指数退避策略。第一次等待2秒第二次等待4秒最高到30秒。连续失败超过5次就把任务标记为blocked状态等下一轮巡检再处理。代理池真正要抓大批量数据时单IP是扛不住的。我给爬虫服务预留了代理池接口通过切换IP来分摊压力。使用代理时有一个细节不是所有代理都支持HTTPS先做连通性测试再放行不然请求会一直卡在握手阶段。整个爬虫模块的代码量占全项目的三成左右但它本身不该有任何业务逻辑它就是一个数据搬用工。你把这层的定位想清楚了后面所有模块的接口设计都会清爽很多。3. 推荐系统的冷启动设计与分布式环境下的数据一致性图书推荐听起来是个锦上添花的功能但在这个项目里它才是真正的核心价值。我调研过很多开源的推荐方案最后没有引入Mahout或Spark MLlib而是自己实现了轻量级的推荐算法理由后面细说。3.1 基于内容的推荐TF-IDF向量化与余弦相似度图书和图书之间的相似度可以通过内容标签来计算。比如一本书的简介、标签、分类信息分词后拼成一个文档用TF-IDF把每本书变成向量再算余弦相似度。这样当用户点击一本书时系统可以把与它最相似的几本书推出来。分词这块我用了HanLP它在中文分词上的效果比直接按空格切分好太多。SpringBoot整合HanLP也很简单引入依赖后加载词典然后把每本书的关键字段拼接后丢给分词器。public class BookVectorizer { public MapString, Double tokenize(String rawText) { ListString terms HanLP.segment(rawText).stream() .map(term - term.word) .filter(word - word.length() 1) // 过滤单字 .collect(Collectors.toList()); // 计算TF MapString, Double tfMap new HashMap(); // 这里就是遍历term计数然后除以总词数 return tfMap; } }这一步的工程难点在于几万本书两两计算相似度是O(n²)量级的操作单机内存和时间都扛不住。我的优化思路是只对同一分类下的书做相似度计算比如小说分类下算一次历史分类下算一次缩小计算域。预先离线算好相似度矩阵存入Redis不搞实时计算。用户点了一本书直接去Redis取TopN相似图书毫秒级返回。3.2 协同过滤的取舍为什么没用Spark MLlib很多教程一上来就建议用Spark做协同过滤但对于这个体量的项目说实话有点杀鸡用牛刀。Spark集群起步就要三台机器而且会极大增加部署和运维成本。我的做法是第一步用基于物品的协同过滤ItemCF统计用户对图书的评分矩阵计算物品之间的共现矩阵再按共现次数和评分加权得到推荐分数。第二步用基于内容的推荐兜底处理新用户没有行为数据的情况冷启动。我封装了一个轻量级的推荐引擎输入是用户行为日志表输出是每个用户的TopN推荐列表通过定时任务每天凌晨跑一次结果写入Redis的recommend:user:{userId}键。3.3 分布式环境下用户行为采集的一致性陷阱推荐效果好不好数据准确是关键。用户的行为数据分散在很多服务里可能在图书详情页评分可能在搜索结果里点击还可能在收藏夹操作。这些数据如果各自为政推荐模块拿到手就是一堆乱账。所以项目里做了一个用户行为采集服务所有行为通过统一的API上报格式如下{ userId: 10001, bookId: 10087, behaviorType: score, score: 8.5, timestamp: 1715590400000 }行为数据先写Redis再异步刷到MySQL的user_behavior表。这里必须强调一个点如果使用Spring Cloud的OpenFeign异步上报必须注意超时。之前我在采集服务里直接同步调推荐服务结果推荐服务偶尔Full GC导致采集接口超时前端就报错了。后来改成采集接口只负责写Redis不阻塞。定时批量同步Redis到MySQL。推荐服务只读MySQL中的行为表离线计算。这套异步链路跑起来之后用户侧没再出现过因为推荐逻辑触发导致的接口超时。4. SpringCloud核心组件落地从注册中心到网关限流的配置细节技术选型时可以讲大道理但真正落地时每个组件的坑又会让你怀疑人生。这一节只讲实际配置过程中最容易出问题的地方。4.1 Nacos注册中心命名空间与服务分组这个项目的服务注册用Nacos版本是2.x。本地起单机Nacos很简单startup.cmd -m standalone就能跑。但在微服务配置里有几个细节需要特别留意命名空间namespace建议按环境划分dev、test、prod各一个namespace防止开发环境的服务注册到生产环境。服务分组group默认是DEFAULT_GROUP一般不用改但如果你有多个团队共用一套Nacos最好分组隔离。临时实例ephemeral默认true如果用K8s部署服务实例的注册和摘除频率很高临时实例反而更合适。每个服务的bootstrap.yml大致长这样spring: application: name: book-recommend-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 namespace: d3800240-c2a3-4a1a-9d87-1b3f0a123456 config: server-addr: 127.0.0.1:8848 file-extension: yaml配置中心存的是公共配置比如数据源、Redis连接、消息队列地址。这样做的好处是线上配置改一处全部服务自动刷新不用逐台服务器去改配置文件。注意要用RefreshScope来让配置在运行时生效。4.2 Gateway网关断言、过滤器与限流网关在项目里承担三个职责统一入口路由、Token鉴权、接口限流。路由配置核心如下spring: cloud: gateway: routes: - id: book-search-route uri: lb://book-search-service predicates: - Path/api/search/** filters: - StripPrefix1 - id: book-recommend-route uri: lb://book-recommend-service predicates: - Path/api/recommend/** filters: - StripPrefix1这里最容易踩的坑是StripPrefix1和Path的配合。如果前端请求的是/api/search/books经过StripPrefix后后端服务收到的是/books不匹配的controller路径就会直接404。我之前调试半天最后用- StripPrefix1加上后端controller映射统一修改才解决。网关层做限流用的是RequestRateLimiter需要自定义KeyResolver。这里有个小技巧按用户ID限流比按IP限流更准确因为校园网或公司网络下大量用户共享出口IP按IP限流容易误伤。Bean public KeyResolver userKeyResolver() { return exchange - { String userId exchange.getRequest().getHeaders().getFirst(userId); return Mono.just(userId ! null ? userId : anonymous); }; }4.3 OpenFeign调用超时与熔断的合理设置服务间调用我用了OpenFeignRibbonSpringCloud 2020之后是Spring Cloud LoadBalancer。配置超时时千万别用默认值默认的1秒超时在跨服务查询时几乎是必超时的。ribbon: ReadTimeout: 5000 ConnectTimeout: 3000 feign: hystrix: enabled: true后来项目升级到SpringCloud 2021Hystrix进入了维护模式我把熔断器换成了Sentinel。Sentinel的好处是可以在控制台实时看到每个接口的QPS、RT和异常比例比Hystrix的可视化强不少。熔断规则我配合Nacos做了动态配置不用重启服务就能调整阈值这在线上排查问题时特别救命。5. 分布式锁与数据幂等那些你以为简单却翻车的并发场景爬虫服务、用户行为服务和推荐服务之间有很多并发写库的节点。刚开始我用的就是最朴素的做法先查一遍数据库不存在就插入。但分布式环境下先查再插是典型的并发地雷。5.1 爬虫服务中的重复数据问题两条爬虫线程同时抓取同一本ISBN的图书各自判断数据库里没有这本书然后双双执行insert结果主键冲突或者产生重复数据。解决办法是给ISBN建唯一索引然后利用数据库的唯一索引兜底ALTER TABLE book ADD UNIQUE KEY uk_isbn (isbn);这样即便并发插入也只有一条能成功另一条会报DuplicateKeyException捕获后转为更新操作即可。这个方法比分布式锁更可靠因为它是数据库本身保证的不依赖Redis的可用性。5.2 Redis分布式锁的正确打开方式但有些场景不能只靠数据库唯一索引比如推荐任务缓存刷新这种操作多个服务节点同时执行会造成重复计算浪费资源不说还可能导致缓存数据不一致。这时就要上分布式锁。我一开始看网上教程用的是SETNX命令配合expire这个写法有个经典问题如果SETNX执行成功后、还没执行expire时服务宕机这把锁会永远不释放后续所有任务都会被阻塞。正确的姿势是用Redisson它内部封装了看门狗机制会自动给锁续期在绝大多数场景下直接拿来用就好。Autowired private RedissonClient redissonClient; public void refreshRecommendCache(Long userId) { RLock lock redissonClient.getLock(recommend:lock: userId); boolean isLocked lock.tryLock(3, 30, TimeUnit.SECONDS); if (!isLocked) { log.info(获取锁失败其他节点正在处理); return; } try { // 执行推荐计算和缓存刷新 } finally { lock.unlock(); } }关于这个锁的经验锁的粒度一定要细。刚开始我用recommend:lock:all这把全局锁来锁所有用户的推荐刷新结果执行一次要十几秒期间其他用户刷新全部阻塞。后来把key改成按userId粒度大幅降低了锁竞争。5.3 幂等性设计的兜底策略分布式锁能保证同一时刻只有一个节点在跑但消息队列的重复投递问题仍然存在。爬虫定时任务通过RabbitMQ通知搜索服务增量更新如果消息消费成功但确认失败消息会被重新投递搜索服务就会收到两条一模一样的更新通知。我的幂等方案是给每条消息生成一个业务唯一IDRedis里存一个message:consumed:{msgId}的键设置过期时间为24小时。消费端拿到消息先检查这个键存在就直接返回不存在才处理业务并写入该键。public void onMessage(BookUpdateMessage msg) { String msgKey message:consumed: msg.getMessageId(); Boolean first redisTemplate.opsForValue() .setIfAbsent(msgKey, 1, Duration.ofHours(24)); if (Boolean.FALSE.equals(first)) { // 已消费过直接丢弃 return; } // 真正执行增量同步 bookSearchService.syncBookToEs(msg.getBookId()); }这里用setIfAbsent原子操作天然避免了并发下的重复执行问题比先查再插靠谱得多。6. 推荐策略背后的数据链路从Elasticsearch到Redis缓存的实战配置推荐系统跑起来之后最影响用户体验的是响应速度。用户从点击我的推荐到页面渲染出图书卡片整个过程必须控制在1秒以内。这背后考验的是数据链路的每一个环节。6.1 Elasticsearch在图书搜索中的角色图书检索这块我没有全盘依赖MySQL的like查询而是引入了Elasticsearch做全文检索。原因很简单当用户输入三体你要他能匹配到三体、三体全集、三体X观想之宙等书名当用户输入东野圭吾你要能匹配到该作者的所有作品。MySQL的like%三体%在数据量小的时候跑得还行到了几十万条书目的量级性能就会明显下降。我用的是Elasticsearch 7.x版本索引结构如下{ mappings: { properties: { title: { type: text, analyzer: ik_max_word }, author: { type: keyword }, isbn: { type: keyword }, summary: { type: text, analyzer: ik_max_word }, rating: { type: float }, category: { type: keyword } } } }中文分词用了IK分词器把ik_max_word和ik_smart区别讲清楚ik_max_word切词更细适合搜索召回ik_smart切词更精确适合搜索排序。索引场景用ik_max_word查询场景用ik_smart两套配合才能召回多而不乱。SpringBoot整合Elasticsearch最经典的坑是版本不匹配。我用的是Spring Data Elasticsearch 4.x它对应的ES客户端版本是7.x。如果你的SpringBoot版本和ES版本对不上连接时会出现Unable to parse response for node之类的诡异报错。解决方法是引入和ES服务端版本一致的RestHighLevelClient。官方文档里虽然说着不建议直接用RestHighLevelClient但说实话对于这种体量的项目反而是最稳定的选择。6.2 Redis缓存策略防穿透与防雪崩图书列表、推荐列表、分类榜单这些都是高频读取低频率更新的数据。我用了两级缓存模式第一级本地Caffeine缓存处理单机内高频访问。第二级Redis缓存处理跨节点的共享缓存。先从Redis查查不到就从MySQL查成功后再写Redis并设置随机过期时间。随机过期时间很关键如果一大批缓存键同时过期MySQL会在瞬间被打爆这就是缓存雪崩。代码里设置过期时间是base random.nextInt(300)秒。防止缓存穿透的逻辑是如果MySQL里没有这本书也写一个空值到Redis过期时间设置短一点比如60秒。这样下次请求同一个ID不会每次都打到MySQL。6.3 推荐结果的组装与排序从Redis拿到推荐图书ID列表后还需要从ES或者MySQL捞出每本书的封面、作者、评分等字段组装成前端需要的JSON。这个组装过程如果每次都实时查库性能会很难看。我的做法是把图书核心字段在推荐计算时就序列化好直接存在推荐结果的缓存里前端一次请求全部返回。推荐的排序不只是按评分高低还要叠加以下因素用户行为加权用户浏览过的分类里的书排序权重1.2。内容新鲜度最近上架的图书权重1.5。评分稀释评价人数太少的书评分乘以一个置信系数防止冷门书靠少数高分霸榜。排序逻辑全部在Java侧完成通过实现自定义Comparator灵活调整规则。7. 前端Vue3与SpringCloud网关的对接细节后端微服务再怎么拆前端看到的仍然是一个统一的入口。Vue这边我用了Vue3 Vite Element Plus Pinia路由和状态管理都做了模块化拆分。但真正花时间调试的是前后端跨域和鉴权这两个环节。7.1 Vite开发代理与生产环境网关转发开发环境下Vite的vite.config.js需要代理所有/api请求到网关地址export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })生产环境下前端打包成静态资源后部署在Nginx里Nginx再把/api请求反向代理到网关所在的服务地址。这里有一个必须注意的点网关转发时会修改Host头后端服务如果依赖request.getServerName()获取域名可能会拿到内网IP。解决方法是网关层配置X-Forwarded-For和X-Forwarded-Host头转发。7.2 JWT鉴权从登录到网关过滤器的实现链路系统里用户登录和token校验采用了JWT方案无状态鉴权非常契合微服务分布式场景。用户在登录服务验证通过后得到一个签名后的Token里面包含userId和角色信息。前端在每次请求时通过Authorization请求头携带Token网关会先解析Token校验签名和过期时间然后把userId写入请求头转发给下游服务。网关里的JWT校验过滤器是整个链路的安全防线我遇到最多的两个问题是放行路径控制登录注册接口必须放行如果写在/api/auth/**路径后面注意在application.yml里配置白名单。刷新Token逻辑Token有2小时过期时间如果用户在看书过程中Token过期了前端必须自动刷新并重新发起请求。Vue这边我用Axios拦截器统一处理401响应。service.interceptors.response.use( response response, error { if (error.response?.status 401) { // 刷新token或跳转登录 router.push(/login) } return Promise.reject(error) } )7.3 页面性能图书封面懒加载与ECharts数据看板图书列表页是图片密集型页面几百本书同时加载的话浏览器并发连接数会打满。我用了Vue的懒加载指令img v-lazybook.coverUrl alt封面 /主图加载完成前先显示一个骨架屏占位这个交互细节很影响用户体感。管理端的数据大屏我用ECharts绘制了图书分类分布饼图、评分趋势折线图、热门榜Top10条形图。这些数据来自后端单独的统计接口由管理服务聚合各模块的数据后返回。统计接口的思路很简单查询MySQL时用GROUP BY分组统计结果Redis里缓存5分钟前端轮询拉取。8. 部署过程中最容易被忽视的环境问题与调优记录本地开发一切正常一上服务器就各种问题这类情况在微服务项目里几乎无法避免。我在这套系统部署时踩过几个比较典型的坑单独列出来供参考。8.1 JDK版本与服务端兼容性SpringBoot 2.7 SpringCloud 2021.x 在JDK8上是完全没问题的但我最初在本地用的是JDK17编译和运行都很正常结果部署到服务器上的JDK8环境时直接报了UnsupportedClassVersionError。这类问题的排查很快但会白白浪费很多时间。建议项目一开始就统一在.mvn配置里声明JDK版本properties java.version1.8/java.version maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties8.2 Docker容器化部署构建镜像时的网络与内存问题微服务项目如果一个个手动启动环境依赖很折磨人。我给这套系统写了完整的Docker Compose编排把Nacos、MySQL、Redis、Elasticsearch、RabbitMQ、业务服务全部编排在一起。在构建后端服务镜像时有几个容易忽视的细节Maven构建阶段和JRE运行阶段要分开用多阶段构建把镜像体积从几百MB压到一百多MB。JVM内存参数必须显式设置Docker容器内默认的-XX:MaxRAMPercentage在不同JDK版本上表现不同容易导致容器内存超限被干掉。FROM eclipse-temurin:8-jre-alpine COPY target/book-recommend-service.jar app.jar ENV JAVA_OPTS-Xms256m -Xmx512m -XX:UseG1GC ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar]8.3 Sentinel控制台与分布式限流的落地前面提到熔断降级用了Sentinel这里补充一下如何把Sentinel限流规则持久化到Nacos。默认情况下在Sentinel控制台上配置的规则是保存在内存里的服务一重启规则就没了。要持久化需要引入Sentinel的Nacos数据源扩展。我配置好之后发现一个现象限流有时候生效有时候不生效。排查后发现Sentinel限流的粒度是资源而不是用户。比如我限流规则配的是GET:/api/recommend这个资源每秒QPS限50那么50个用户同时请求会被拦下但如果有人恶意刷接口照样会打满50个QPS其他正常用户的请求会被拒绝。优化思路是把Token鉴权后的userId作为参数用Sentinel的热点参数限流功能按用户维度限流。9. 这套系统的后劲从豆瓣图书推荐扩展到通用领域做完了豆瓣电子图书推荐系统后我最大的体会是微服务架构和推荐算法这两块其实是通用的换一个数据源、换一套业务规则骨架可以原封不动搬走。图书换成电影、音乐、课程爬虫解析规则改一改推荐特征换一换系统照样跑。具体来说这套架构至少可以往三个方向延伸视频课程推荐把图书特征换成课程的学科分类、难度、讲师信息协同过滤逻辑不变就能给在线教育平台做个性化课程推荐。新闻资讯推荐用户行为采集模块直接复用推荐模块调整特征权重接入实时爬虫数据可以做资讯类App的个性化动态。商品推荐加上价格区间偏好作为特征把评价数据换成销量和库存数据就能支撑电商场景的猜你喜欢。这些扩展的价值在于你投入在分布式锁、ES同步、推荐算法上的所有代码都不是一次性代码而是可以在多个业务领域复用的底层能力。做这类系统还有一个容易忽略的运维要点——数据质量监控。爬虫模块偶尔会抓到残缺或错误的数据如果不做清洗推荐结果就会被污染。我给清洗环节增加了一套校验规则书名和ISBN不能为空评分范围必须在0-10之间作者字段不能包含佚名等无效值。不合格的数据进入待处理队列由管理后台人工确认或者后续补采。10. 最后的几个细节提醒从这套系统踩过的坑里挑出几个最值得记住的单独说一下第一接口返回格式必须全局统一。项目里我用了ResultT封装所有接口返回值包含code、message和data字段。这个约定的价值在联调阶段体现得最充分前后端不用为这次接口返回的是数组还是对象反复扯皮。第二日志链路追踪做起来。微服务一个请求会经过网关、搜索服务、推荐服务排查问题如果不看TraceId纯看时间戳根本对不上。我引入了SleuthZipkin每个请求生成一个TraceId贯穿全局。这个改造在开发期觉得烦但一上线立刻真香。第三压测要趁早别等上线前才做。我用JMeter对核心接口做了压测网关限流阈值就是通过压测摸出来的。压测时要注意把数据库连接池、Redis连接池的参数一并调优不然很可能最先撑不住的不是代码而是连接池。这套系统的代码量大约在两万行左右对于个人开发者来说算是个不小的工程。从爬虫采集到微服务拆分从推荐计算到前端可视化每一层都有值得深挖的细节。做这种全栈分布式项目最大的收获不是技术栈本身而是建立了一种能力把一个看似简单的业务需求拆解成可持续扩展的工程方案。如果你也想做类似的实践项目建议先别贪大从单服务跑通数据链路再逐步拆分微服务、引入分布式组件每一步都要能说出为什么这么做这个项目才算真正做透了。
返回列表