简介:微服务架构是当前分布式系统设计的核心思想,它将单体应用拆分为多个独立部署的服务,通过轻量级通信机制协作,解决业务复杂度高、迭代效率低、扩展性受限等工程痛点。SpringCloud作为微服务生态的主流技术栈,通过注册中心、网关、配置中心等组件,为服务治理提供了标准化的基础设施。电商平台是微服务架构的典型应用场景,涉及商品、订单、用户、支付等核心业务模块,天然适合按领域边界拆分服务。同时,人工智能技术正在重塑电商体验,基于用户行为的商品推荐和基于语义理解的智能检索,成为提升转化率的关键能力。本文从一份完整的SpringCloud电商平台源码出发,深入拆解其多模块工程结构、Nacos服务注册与发现链路、Spring Cloud Gateway网关路由规则,以及基于协同过滤的AI推荐服务和基于Elasticsearch的智能搜索实现。通过梳理启动顺序、接口调用链和缓存降级策略,帮助开发者快速掌握从单体到微服务改造的完整路径,并为课程设计或生产项目提供可直接落地的参考范例。
1. 先拆开看:SpringCloud 电商平台这套源码,解决的是从单体到微服务的那道坎
做电商平台后端开发的人,迟早会遇到一件事:手里的单体项目越改越乱,订单、商品、用户全塞在一个工程里,改一个登录逻辑就得把整个服务重新打包。这份基于 SpringCloud 的电商平台源码,解决的就是这个问题——它把商品、订单、用户、推荐、搜索拆成独立微服务,用注册中心、网关、配置中心把它们串成一条完整链路,人工智能部分则落在商品推荐和智能检索上,不是挂个名字而已。
对正在学微服务、准备做课程设计或想把手头单体商城项目改造成微服务的人来说,这份 zip 解压后的多模块工程,比看一百篇概念文章都直观。你会看到真实的 Feign 调用、网关路由、Nacos 配置、服务间鉴权,以及推荐接口从数据库行为表到结果返回的完整代码。下面我按拆包、启动、接线、排错、改造的顺序,把这套源码从头到尾过一遍。
2. 组件选型与模块拆解:注册中心、网关、AI 服务各自扮演什么角色
2.1 为什么是 SpringCloud:电商场景下的选型理由
先回答一个很多人问过的问题:电商平台做微服务,为什么常见教材和课程设计都选 SpringCloud,而不是 Dubbo 或者直接上 Kubernetes?我的理解是——SpringCloud 全家桶对业务开发者的侵入感最低。你不需要先懂容器编排,也不需要手动维护服务目录,只要把依赖加进 pom,注解一标,服务就能注册、能被发现、能被网关转发。
这套源码走的也是标准 SpringCloud 路线。注册中心用的是 Nacos,网关是 Spring Cloud Gateway,服务间调用走 OpenFeign,配置集中放在 Nacos Config,熔断降级接入 Sentinel。这套组合在中小型电商项目里出镜率最高,原因是它们都有对应的控制台界面,出问题了能直接看到服务实例、调用链和限流日志,对新手友好,对调试也友好。
那人工智能模块放在哪里?这个很关键——它不是独立部署一个大模型服务,而是以两个业务接口的形态嵌在商品服务和搜索服务里。商品推荐接口读用户行为表,基于协同过滤思路算相似商品;搜索接口做分词和联想词补全。这种落地方式的好处是:不动整体架构,AI 作为一个可替换的业务模块存在,后面你想换成自己的模型,只需要改一个 Service 实现。
2.2 源码工程结构:一个 zip 解压出来是什么样
解压这份源码后,你会看到一个多模块 Maven 工程,顶层目录大致是这样:
ecommerce-ai-platform/ ├── pom.xml # 父工程,统一管理依赖版本 ├── ecommerce-gateway/ # 网关服务,端口 8080 ├── ecommerce-auth/ # 认证服务,端口 8100 ├── ecommerce-user/ # 用户服务,端口 8101 ├── ecommerce-product/ # 商品服务,端口 8102 ├── ecommerce-order/ # 订单服务,端口 8103 ├── ecommerce-recommend/ # 推荐服务(AI),端口 8104 ├── ecommerce-search/ # 搜索服务(AI),端口 8105 └── sql/ # 初始化脚本,建库建表 ├── init_user.sql ├── init_product.sql └── init_order.sql每个子模块都是独立的 Spring Boot 应用,通过父 pom 统一管理版本。你不需要手动一个个导入,用 IDEA 打开顶层 pom.xml,选择“Open as Project”,Maven 会自动把子模块识别出来。
各服务模块的角色定位如下:
| 模块 | 端口 | 职责 | 关键依赖 |
|---|---|---|---|
| gateway | 8080 | 统一入口,路由转发、跨域处理 | gateway, nacos discovery |
| auth | 8100 | 登录、token 签发与校验 | security, jjwt |
| user | 8101 | 用户信息 CRUD | mybatis-plus, mysql |
| product | 8102 | 商品分类、商品详情 | mybatis-plus, mysql |
| order | 8103 | 订单创建、订单查询 | mybatis-plus, seata |
| recommend | 8104 | 商品推荐(AI 协同过滤) | redis, mysql |
| search | 8105 | 商品搜索(AI 分词联想) | elasticsearch |
这里要提醒一句:如果你看到自己的解压目录里少了某个模块,先别急着怀疑资源不完整,去检查sql目录和doc目录。有些教学版源码会把搜索服务简化成打包好的 jar,不放在源码目录里,但 SQL 脚本一定在。
2.3 服务间调用链路:从下单到推荐的完整流程
这套源码里最具参考价值的,是它把业务链路打通了。用户浏览商品、下单、支付后,订单服务会通过 OpenFeign 调用推荐服务,把“用户最近购买的商品 ID 列表”传过去,推荐服务再基于这个列表计算相似商品。
举个例子,订单服务里会有一段这样的调用代码:
@Component public class RecommendFeignClient { @Autowired private RecommendService recommendService; /** * 用户支付完成后,触发推荐刷新 * * @param userId 用户 ID * @param productIds 本次购买的 SKU 列表 */ public void refreshAfterOrder(Long userId, List<Long> productIds) { // 同步调用推荐服务,传用户ID和商品ID集合 // 推荐服务内部会更新 Redis 中的用户偏好集合 RecommendRequest request = new RecommendRequest(); request.setUserId(userId); request.setProductIds(productIds); // 这里走 Feign,超时时间在配置中心里设置 recommendService.refreshUserPreference(request); } }这段代码的逻辑很简单:用户在订单服务完成支付后,异步或同步地告诉推荐服务“这个用户买了什么东西”,推荐服务拿到数据后更新 Redis 里的用户行为集合。下次用户再请求首页推荐位,推荐接口直接查 Redis,而不是临时跑一遍计算。
参数说明:userId是用户主键,productIds是订单关联的商品 ID 列表。如果商品数量大,建议把refreshAfterOrder方法改成用 MQ 做异步通知,避免支付接口被推荐刷新拖慢。这套源码里没有引 MQ,算是给学生版做的简化,实际生产环境一般会加 RocketMQ 或 RabbitMQ 解耦。
2.4 数据库设计:五张核心表是怎么支撑 AI 推荐的
推荐模块能跑起来,依赖三张核心表:user(用户表)、product(商品表)、order_item(订单明细表)。AI 推荐的计算逻辑是从order_item里统计“买了 A 商品的用户还买了 B 商品”,这个统计结果会定时生成一张相似度表product_similarity。
-- 商品相似度表,用于协同过滤推荐 CREATE TABLE `product_similarity` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_id` bigint(20) NOT NULL COMMENT '商品A', `similar_product_id` bigint(20) NOT NULL COMMENT '商品B', `score` decimal(6,4) NOT NULL COMMENT '相似度评分 0~1', `update_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_product_similar` (`product_id`, `similar_product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品相似度表';这张表是推荐服务的“饲料”。启动后会有一个SimilarityJob定时任务,每天凌晨跑一次,扫描order_item里所有用户的历史购买记录,用余弦相似度计算商品之间的关联度,把结果写入product_similarity。用户请求推荐时,推荐服务直接查这个表,按score降序返回 Top-N 商品。
这里有个关键点:定时任务不是用 xxl-job 这类分布式调度框架,而是用 Spring 自带的@Scheduled注解实现的。在单机部署下没问题,如果你后面要部署多副本,这个任务会重复执行,到时候需要加分布式锁或改用 xxl-job。源码注释里应该也标了这一点。
3. 从 zip 到跑通:环境匹配、建库建表与五步启动顺序
3.1 环境版本匹配:先对号入座再动手
拆开源码后先别急着运行,把环境版本对好能省掉后面一大半的兼容性问题。以教学版 SpringCloud 源码的常见搭配来说,JDK 1.8 + Spring Boot 2.3.x + Spring Cloud Hoxton.SR12 是一套被验证过无数次的组合,Nacos 用 1.4.x 版本最稳妥,Sentinel 控制台用 1.8.x 也可以正常接上。
如果你电脑里装的是 JDK 17 甚至更高,又没改过 pom.xml,大概率会在编译阶段收到cannot find symbol或package javax.servlet does not exist的报错。这不是源码有问题,是 Lombok 和旧版 Spring Boot 对新 JDK 的支持没跟上。
| 组件 | 建议版本 | 备注 |
|---|---|---|
| JDK | 1.8 | 兼容性最好,遇到问题最少 |
| Maven | 3.6.3+ | 3.8 以上需要检查镜像源 |
| MySQL | 5.7 或 8.0 | 8.0 需要改 driver 配置 |
| Redis | 5.0+ | 推荐服务缓存用 |
| Nacos | 1.4.x | 2.x 需要额外注意鉴权配置 |
| Elasticsearch | 7.x | 搜索服务依赖 |
如果你手里的源码用的是 Spring Boot 2.4 以上版本,那么配置文件从application.yml变成了bootstrap.yml优先加载,访问spring.cloud.nacos.config的写法也要对应调整。先看一眼父 pom 里的版本号,再决定用哪个 JDK,这个顺序不能反,翻车重来很浪费时间。
3.2 配置中心与服务注册:Nacos 初始化
第一步不是启动 MySQL,而是先把 Nacos 跑起来。推荐服务、商品服务、网关服务启动时都要向 Nacos 注册自己,Nacos 起不来,所有服务都会报连接超时。
# 单机模式启动 Nacos,端口 8848 cd nacos/bin startup.cmd -m standalone # Windows 环境 # ./startup.sh -m standalone # Linux / Mac 环境启动后访问http://localhost:8848/nacos,默认账号密码是nacos/nacos。你需要在控制台创建一个命名空间,命名空间 ID 填ecommerce-dev,因为源码里的配置会引用这个 ID。如果不创建命名空间或者 ID 对不上,服务启动时会一直重试拉取配置,日志里会出现config data not found的提示。
接下来把sql/目录下的三个 SQL 脚本按文件名顺序导入数据库。先导init_user.sql,再导init_product.sql,最后导init_order.sql,顺序倒过来会因为外键约束报错。
mysql -uroot -p --default-character-set=utf8mb4 < init_product.sql这里必须加--default-character-set=utf8mb4,否则商品表里的中文商品名会被写成乱码。这个坑我踩过不止一次,后面避坑章节会展开说。
3.3 修改配置文件:数据库连接与 Redis 地址
服务模块下有src/main/resources/目录,里面都有对应的application.yml。需要修改的是数据库密码、Redis 密码和 Nacos 地址。以商品服务为例:
server: port: 8102 spring: application: name: ecommerce-product cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 namespace: ecommerce-dev file-extension: yaml datasource: url: jdbc:mysql://127.0.0.1:3306/ecommerce_product?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 127.0.0.1 port: 6379 password: your_redis_password配置说明:server.timezone=Asia/Shanghai千万不要省略,MySQL 8.0 默认时区是 UTC,不改成上海时间的话,订单表里的create_time会和本地时间差 8 小时,排查问题的时候会怀疑人生。driver-class-name如果源码用的是com.mysql.jdbc.Driver,而你的 MySQL 是 8.0,编译器会提示已弃用,建议直接改成com.mysql.cj.jdbc.Driver,两个都能跑,但新版写法更稳。
Redis 密码如果不为空,所有连 Redis 的模块都要改,包括推荐服务和搜索服务。网关和认证服务不直接连 Redis,但认证服务校验 token 时可能会通过 Feign 调用户服务,用户服务连不上 Redis 就会引发级联失败。
3.4 启动顺序:先基础设施,再业务服务
这套源码启动顺序有讲究,不是随便挨个mvn spring-boot:run就能全起来的。正确顺序是:
- 启动 Nacos(前面已完成)。
- 启动 MySQL 和 Redis,确认端口被监听。
- 启动认证服务
ecommerce-auth。 - 启动用户、商品、订单三个基础业务服务。
- 启动推荐服务和搜索服务(AI 模块)。
- 最后启动网关服务。
网关为什么最后启动?因为它启动时要拉取所有服务的路由注册信息,如果前面的服务还没注册上来,网关会出现短暂的路由缺失,虽然 Nacos 会在后续自动同步,但是第一次启动时经常看到No route found的日志,容易吓到自己。
每个模块单独启动的方式是一样的:
cd ecommerce-auth mvn spring-boot:run如果你用的是 IDEA,也可以直接在AuthApplication.java上右键 Run。启动成功的标志是控制台出现Registered instance with nacos日志,说明这个服务已经注册到 Nacos 了。所有服务都起来后,打开 Nacos 控制台的服务列表页面,应该能看到 6 个服务都是健康状态,这时候再去调网关接口就不该报连接拒绝了。
3.5 接口验证:一条命令确认链路通不通
服务全部启动后,先别急着打开前端页面,用 curl 验证一下链路是最快的。先走认证服务拿 token,再带 token 调商品接口和推荐接口。
# 第一步:登录获取 token curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 第二步:携带 token 查询商品列表 curl http://localhost:8080/api/product/list \ -H "Authorization: Bearer <上一步返回的token>" # 第三步:调用推荐接口 curl http://localhost:8080/api/recommend/home?userId=1 \ -H "Authorization: Bearer <上一步返回的token>"三个接口都返回正常 JSON 而不是 401 或 503,说明网关转发、服务注册、Feign 调用、MyBatis 查询整条链路都是通的。如果第二步返回 401,先检查认证服务生成的 token 里包含的角色权限和网关的路由鉴权配置是否匹配。如果第三步返回 500,直接去推荐服务的控制台看异常堆栈,十有八九是 Redis 缓存没命中后查 MySQL 时表名找不到。
4. 人工智能落地点:商品推荐与智能检索的两个真实接口
4.1 推荐服务接口:从用户行为表到 Top-N 商品
推荐模块是这份源码里“人工智能”含量最高的地方。它的实现逻辑不复杂,但很完整,走的是经典协同过滤路线。核心入口是RecommendServiceImpl里的recommendForUser方法:
@Service public class RecommendServiceImpl implements RecommendService { @Autowired private ProductSimilarityMapper similarityMapper; @Autowired private RedisTemplate<String, Object> redisTemplate; private static final String REDIS_KEY_PREFIX = "recommend:user:"; @Override public List<ProductVO> recommendForUser(Long userId, int size) { // 1. 先从 Redis 查缓存,缓存命中直接返回 String redisKey = REDIS_KEY_PREFIX + userId; List<ProductVO> cached = (List<ProductVO>) redisTemplate.opsForValue().get(redisKey); if (cached != null && !cached.isEmpty()) { return cached.subList(0, Math.min(size, cached.size())); } // 2. 缓存未命中,查用户最近购买的商品 ID List<Long> boughtProductIds = orderItemMapper.findProductIdsByUserId(userId); // 3. 用购买过的商品去 product_similarity 表找关联商品 List<ProductVO> recommended = similarityMapper.findSimilarProducts( boughtProductIds, size * 2 // 多取一批,过滤掉已购买商品后取前 size 个 ); // 4. 过滤掉用户已经买过的商品 recommended.removeIf(item -> boughtProductIds.contains(item.getId())); // 5. 写入 Redis 缓存,过期时间 1 小时 redisTemplate.opsForValue().set(redisKey, recommended, 1, TimeUnit.HOURS); return recommended.subList(0, Math.min(size, recommended.size())); } }这段代码的逻辑分五步,每一步都有明确的工程考量。第一步查缓存是为了挡高并发,推荐接口是首页高频调用,每次请求都去 MySQL 跑一次相似度 SQL 的话,商品服务扛不住五分钟。第五步设置一小时过期时间,是让用户的行为变化能在一小时后反映到推荐结果里,如果设成永久缓存,用户买了新商品后推荐结果不会更新。
参数说明:size是前端要求的推荐数量,常见值是 10 或 20;size * 2是为了多查一批候选商品,过滤已购买项后保底。你可以把 Redis 过期时间改成 24 小时,但建议不要把size * 2改成size,不然过滤完可能不足size条。
这套逻辑对新手理解 AI 推荐的工程落地很有帮助。它没有训练模型、没有特征工程,但作为课程设计或生产项目的 MVP 版本,该有的缓存策略、降级策略、过滤策略都有了。如果你想把它升级成真正的协同过滤算法,替换findSimilarProducts的实现即可。
4.2 智能检索接口:分词、联想词与搜索历史记录
搜索模块的 AI 感体现在两个地方:一是商品名称分词检索,二是用户输入前缀时的联想词补全。这两个能力都是基于 Elasticsearch 实现的,源码里对应的操作封装在SearchServiceImpl:
@Service public class SearchServiceImpl implements SearchService { @Autowired private RestHighLevelClient esClient; @Override public List<String> suggestKeywords(String prefix, int limit) { // 构建前缀补全查询,只查商品名称字段 CompletionSuggestionBuilder suggestionBuilder = new CompletionSuggestionBuilder("name_suggest") .prefix(prefix) .size(limit); SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); sourceBuilder.suggest(new SuggestBuilder().addSuggestion("suggest", suggestionBuilder)); // 执行查询并解析联想词 // 常见做法是取第一个 suggestion 的 options 列表 return parseSuggestOptions(esClient.search(...)); } }这里的name_suggest字段用的是 Elasticsearch 的 completion 类型,底层数据结构是 FST(有限状态转移机),专门做前缀联想,性能比普通 match 查询高一个量级。电商搜索框的“实时联想”基本都这么做。
分词功能则由 IK 分词器插件负责,商品名称为“华为 Mate 60 Pro 手机”时,会被切分成“华为 / mate / pro / 手机”等词元,用户搜“华为手机”也能命中。源码里有一个ElasticsearchIndexInitializer类,负责在服务启动时自动创建索引和 mapping,你不用手动执行PUT /product命令。唯一要做的是保证 Elasticsearch 7.x 版本和 IK 插件版本对应,IK 装错版本会直接导致搜索服务启动失败。
4.3 AI 服务的降级逻辑:缓存挂了怎么办
线上系统最忌讳“AI 服务挂了,整个页面都打不开”。这套源码里做了一个很实用的兜底策略:推荐接口查 Redis 失败时,不直接抛异常,而是降级查 MySQL 热门商品表,把点赞数最高的 10 个商品返回给前端。
// 降级方法定义,参数和原方法保持一致 @Degrade public List<ProductVO> fallbackRecommend(Long userId, int size, Throwable e) { // 查热销商品表,按销量倒序取前 size 条 return productMapper.findHotProducts(size); }这个@Degrade注解是 Sentinel 的降级注解,源码里配合 Sentinel 控制台使用。触发降级后,控制台会记录降级次数,你可以配置阈值规则:5 秒内异常数超过 5 次就自动降级,恢复需要 10 秒。这样就保证了推荐服务就算 Redis 崩溃或者相似度表数据没生成,用户依然能看到商品列表,只是推荐精准度变差了而已。
5. 启动避坑与排查:端口、时区、跨域与注册失败的四处翻车现场
5.1 Nacos 启动闪退:startup.cmd一闪而过
现象:双击startup.cmd后黑窗口闪了一下就没了,控制台没有任何报错信息。
原因:Nacos 默认以集群模式启动,单机环境没有配置集群节点,启动脚本直接退出。另外 JDK 环境变量没配好也会导致闪退,Windows 上尤其常见。
解决:先检查JAVA_HOME是否指向 JDK 1.8,确认后在 cmd 里手动执行startup.cmd -m standalone,这样即使启动失败,窗口也会保留错误信息。常见错误是找不到JAVA_HOME或者 Nacos 2.x 版本对 JDK 版本有额外要求,换回 1.4.x 版本基本能解决。
提示:别用老版本的 Nacos 1.3 跑这套源码,配置语法不完全兼容,会出现dataId not found的警告,虽然不阻塞启动,但配置拉不全会导致接口报错。
5.2 MySQL 中文乱码:商品名全是问号
现象:商品列表接口返回的数据里,中文商品名显示为???。
原因:SQL 脚本导入时没有显式指定字符集,或者数据库本身创建时不是 utf8mb4。MySQL 8.0 默认字符集是 utf8mb4,但 5.7 的默认字符集可能还是 latin1,一旦表结构建错,后面改起来非常麻烦。
解决:删除数据库重建,导入前执行SET NAMES utf8mb4;,并确保 URL 参数里带了characterEncoding=utf8。检查表结构的字符集:
SELECT TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'ecommerce_product';如果看到latin1开头的排序规则,说明表建错了,必须重新建库。这个字段必须全部是utf8mb4_general_ci或utf8mb4_unicode_ci。
5.3 网关路由报 503:Service Unavailable 问题
现象:前端页面能打开,但调用登录接口、商品接口时网关返回 503。
原因:服务已经启动了,但网关启动时这些服务还没注册到 Nacos,或者 Feign 客户端懒加载没有触发重试。还有一种情况是服务注册到 Nacos 了,但是网关的路由配置里的lb://服务名写错了,比如商品服务注册名是ecommerce-product,配置里写成了product-service,服务名对不上就找不到实例。
解决:先打开 Nacos 控制台,查看“服务管理-服务列表”,确认所有服务实例是否为健康状态。再核对网关配置文件里的服务名:
spring: cloud: gateway: routes: - id: product-route uri: lb://ecommerce-product predicates: - Path=/api/product/**lb://后面必须和spring.application.name完全一致,大小写也要一致。如果确认都对,重启网关后等待 10 秒再试,服务注册有延迟,刚启动完立刻调接口偶尔会遇到实例信息还没同步的情况。
提示:504 和 503 不一样,504 是网关超时,通常是 Feign 调用业务服务时对方处理太慢,需要调大ribbon.ReadTimeout而不是排查路由。
5.4 跨域报错:前端联调时 Access-Control-Allow-Origin 缺失
现象:用前端项目(特别是小程序 web-view 或本地 Vue 开发服务器)调用网关接口时,浏览器控制台报跨域错误,请求根本发不出去。
原因:网关默认不开启跨域配置。这套源码的 gateway 模块里需要设置全局 CORS,但很多课程设计模板漏了这一层,或者只在某个 Controller 上加@CrossOrigin,结果只有那一个接口能跨域,其他接口全部被浏览器拦截。
解决:在网关模块加一个全局 CORS 配置类,代码如下:
@Configuration public class CorsConfig { @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }注意第 7 行setAllowCredentials(true)和addAllowedOrigin("*")不能同时使用,否则浏览器会报“credentials mode 不兼容”。前端不带 cookie 的话,把setAllowCredentials注释掉即可。小程序端不受浏览器跨域限制,但如果是 H5 端,这一步不做跑不通。
提示:小程序和网页共用这套源码时,建议网关只保留无状态鉴权,token 放请求头传,别依赖 cookie,小程序请求压根不带 cookie。
5.5 推荐服务启动成功但接口 500:ES 索引初始化失败
现象:推荐服务启动没有报错,但调/api/recommend/home时返回 500,日志提示index_not_found_exception。
原因:Elasticsearch 的索引初始化器在服务启动时执行,但如果 ES 连接串配置错误,或者索引已经存在但 mapping 版本不匹配,初始化器可能静默跳过。商品数据没有导入 ES,查询时自然找不到索引。
解决:确认application.yml里的 ES 地址正确后,手动执行初始化接口,这套源码一般会在搜索服务里提供一个POST /api/search/reindex接口,用于重新构建索引。调用后去 Kibana 或 ES 的_cat/indices接口确认索引存在:
curl http://127.0.0.1:9200/_cat/indices?v看到ecommerce_product索引文档数大于 0,再回头调推荐接口就正常了。
6. 进阶:把推荐算法换成你自己的数据,再给网关做一次压测
当你把服务全部跑通,发现推荐接口返回的永远是同一批商品时,就该动真格的了。我建议你做三件事:第一,把推荐服务的数据源换成你自己的订单数据;第二,把相似度表改成定时任务刷新;第三,拿 JMeter 对网关接口做一个基础压测,看看这套源码的吞吐量边界在哪。
先说换数据源。最简单的方式是往order_item表里多插几行模拟数据,覆盖不同用户的购买行为重叠。比如用户 A 买了商品 1、2、3,用户 B 买了商品 2、3、4,那么商品 2 和商品 3 的相似度就应该升高。你可以用 Navicat 直接导几行数据,然后去推荐服务里手动触发一次SimilarityJob.refresh(),再调推荐接口看结果是否变化。如果变化了,说明协同过滤链路完全跑通;如果没变化,检查定时任务是否被@Scheduled正常加载,以及在推荐服务所在模块的启动类上是否加了@EnableScheduling注解。
@SpringBootApplication @EnableScheduling public class RecommendApplication { public static void main(String[] args) { SpringApplication.run(RecommendApplication.class, args); } }这里有个容易被忽视的细节:product_similarity表里的score字段是DECIMAL(6,4),最大只能存 99.9999,余弦相似度的结果在 0 到 1 之间,所以这个字段不会溢出。但如果你把算法改成皮尔逊相关系数,结果范围是 -1 到 1,DECIMAL(6,4)照样够用。真正要注意的是定时任务执行时间——默认是凌晨 2 点,你手动验证时等不到那个点,所以要么改 cron 表达式,要么直接在测试类里调用一次refresh()方法。
第二件事关于缓存策略的优化思路。默认的 Redis 缓存过期时间是一小时,如果你的商品更新频繁,一小时内的推荐结果可能滞后。我习惯把热门商品的推荐缓存改成 10 分钟,把冷门商品改成 2 小时。实现方式不复杂,在写入 Redis 时按商品类目判断:
long ttl = product.getCategoryId() == 1 ? 10 : 120; redisTemplate.opsForValue().set(redisKey, recommended, ttl, TimeUnit.MINUTES);这种按业务维度区分缓存时间的做法,算是对推荐系统的一种实用调优,比一刀切设一个固定 TTL 效果要好。
第三件事是压测。用 JMeter 创建一个线程组,50 并发对http://localhost:8080/api/recommend/home?userId=1发起 GET 请求,观察吞吐量。没有配 Sentinel 限流的话,这台机器上推荐服务的 QPS 应该能到几百。如果你发现吞吐量异常低,先看是不是 Redis 连接池配置太小,再看网关线程数。推荐服务的spring.redis.lettuce.pool.max-active默认是 8,50 并发时这个连接池会被瞬间打满,续约不够快就会产生等待,压测结果自然难看。
spring: redis: lettuce: pool: max-active: 32 max-idle: 16 min-idle: 8 max-wait: 3000ms把这些参数填进去,重启推荐服务,再压一次,吞吐量通常能翻一倍。从那以后我每拿到一套 SpringCloud 电商源码,都会先做三件事:解压后查版本、按顺序启动服务、用 curl 把关键链路走通,然后再去动代码。这个顺序听起来枯燥,但真的能帮你省下两小时的定位时间。希望这份 SpringCloud 电商平台源码,也能成为你第一条跑通的微服务完整链路。
本文还有配套的精品资源,点击获取