苍穹外卖微服务踩坑实录:从MyBatis绑定异常到架构重构全流程
前言
最近在做苍穹外卖项目的微服务拆分,本来是想把单体架构拆成「管理端」和「用户端」两个独立微服务,结果踩了个让我头疼了一下午的大坑,最后不仅解决了问题,还顺着这个坑重构了整个套餐模块的微服务架构,顺便把微服务面试的核心考点全落地了一遍。这篇博客就完整记录一下从踩坑、排错到架构优化的全过程。
一、遇到的实际难题背景
1. 项目初始架构
我把苍穹外卖拆成了两个核心微服务:
- sky-server-admin-operation:商家管理端微服务,负责套餐、菜品、员工的增删改查,是数据的管理方
- sky-server-user-base:C端用户端微服务,负责用户微信登录、套餐浏览、下单支付,是数据的查询方
当时图省事,两个微服务里都写了一套SetmealMapper.java和对应的SetmealMapper.xml,包名、文件名完全一致,两个微服务都直接操作数据库里的setmeal套餐表。管理端写增删改逻辑,用户端写查询逻辑,看起来各司其职,实则埋了个大坑。
2. 踩坑现场
我在开发管理端的「修改套餐」功能时,启动项目调用接口,直接报了经典的MyBatis错误:- org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.sky.mapper.SetmealMapper.update
复制代码 翻译过来就是:MyBatis找不到SetmealMapper里update方法对应的SQL语句。
3. 初期排错的误区
一开始我以为是常规的Mapper绑定错误,按常规步骤排查了半天:
- 检查了接口方法名和XML里的id,完全一致
- 检查了XML的namespace和接口全类名,完全匹配
- 检查了SQL语法,没有错误
- 其他模块用完全一样的写法都能正常运行,唯独这个模块报错
最离谱的是,我明明在SetmealMapper.xml里写了update的SQL,可项目启动就是找不到。
4. 最终找到的坑点
折腾了快一个小时,我才发现问题的根源:
我把 update 方法的SQL语句,写到了 sky-server-user-base 用户端微服务的 SetmealMapper.xml 里了,而调试的时候我只启动了管理端微服务,根本没启动用户端!
因为两个微服务的Mapper包名、文件名完全一样,IDE自动补全的时候我没注意,直接把SQL写到了另一个微服务的XML里。
但冷静下来我意识到:这根本不是我粗心的问题,而是架构设计本身就有严重缺陷——就算这次解决了,以后还会出现文件写混、数据不同步、业务逻辑耦合的问题,两个微服务直接操作同一张表,本身就违反了微服务「高内聚、低耦合」的核心原则。
二、解决的思路流程
第一步:先止血,解决眼前的报错
先把update的SQL移到管理端对应的XML文件里,同时修复了之前Mapper里deleteByIds方法参数名和XML里collection不匹配的问题(这个问题会导致XML解析失败,整个Mapper都无法加载),先让修改套餐的功能正常跑通。
第二步:反思问题本质,确定优化目标
这次踩坑让我彻底明白,微服务拆分不是简单地把单体拆成多个jar包,核心是数据和职责的拆分。
套餐数据的管理权限,天然就应该归属管理端,用户端只需要拿到查询结果就行,根本不应该碰数据库。
所以我定下了核心优化目标:
彻底移除用户端微服务中所有套餐相关的数据库操作代码,用户端不再直连数据库,所有套餐数据都通过微服务间调用的方式,从管理端获取,从根源上杜绝同类问题。
第三步:技术选型,贴合面试热点
因为项目已经用了Nacos做服务注册中心,所以直接选用Spring Cloud Alibaba生态的标准方案,完全贴合现在Java微服务面试的高频考点:
- 服务调用:Spring Cloud OpenFeign(声明式HTTP调用,替代传统的RestTemplate)
- 服务注册发现:Nacos(已经部署好,直接复用)
- 负载均衡:Spring Cloud LoadBalancer(OpenFeign默认集成)
- 熔断降级:Alibaba Sentinel(防止服务雪崩,面试必考点)
- 接口安全:Spring Cloud Gateway(管控内部接口,禁止外部访问)
三、最终落地的优化架构
1. 架构核心原则
- 单一职责:管理端是套餐数据的唯一管理者,所有数据库操作都在管理端完成
- 彻底解耦:用户端不再直连数据库,仅通过内部接口调用管理端获取数据
- 容错保障:加入熔断降级机制,避免管理端故障导致用户端服务雪崩
- 安全管控:内部接口和外部接口完全分离,网关层拦截外部对内部接口的访问
2. 具体改造步骤
(1)清理用户端的数据库操作层
直接删除用户端的SetmealMapper.java、对应的XML文件,同时精简SetmealService接口,只保留用户端需要的2个查询方法,彻底移除用户端直连数据库的能力。
(2)用户端编写OpenFeign声明式调用客户端
在用户端创建SetmealFeignClient接口,定义调用管理端的接口,同时编写熔断兜底类,当管理端不可用时,返回友好的兜底数据:- @FeignClient(name = "sky-server-admin-operation", fallback = SetmealFeignClientFallback.class)
- public interface SetmealFeignClient {
- @GetMapping("/inner/setmeal/list")
- Result<SetmealVO> searchBycategoryId(@RequestParam("categoryId") String categoryId);
- @GetMapping("/inner/setmeal/dish/{id}")
- Result<DishItemVO> searchDishItemById(@PathVariable("id") String id);
- }
- @Component
- class SetmealFeignClientFallback implements SetmealFeignClient {
- @Override
- public Result<SetmealVO> searchBycategoryId(String categoryId) {
- return Result.success(null);
- }
- @Override
- public Result<DishItemVO> searchDishItemById(String id) {
- return Result.success(null);
- }
- }
复制代码 在用户端启动类添加@EnableFeignClients注解,开启Feign功能,同时在application.yml里开启Feign对Sentinel的支持,配置超时时间。
(3)管理端暴露内部专用接口
在管理端创建InnerSetmealController,路径前缀统一用/inner,专门供微服务间调用,直接复用管理端已有的业务逻辑,不用重复开发:- @RestController
- @RequestMapping("/inner/setmeal")
- public class InnerSetmealController {
- @Autowired
- private SetmealService setmealService;
- @GetMapping("/list")
- public Result<SetmealVO> searchBycategoryId(@RequestParam("categoryId") String categoryId) {
- return Result.success(setmealService.searchBycategoryId(categoryId));
- }
- @GetMapping("/dish/{id}")
- public Result<DishItemVO> searchDishItemById(@PathVariable("id") String id) {
- return Result.success(setmealService.searchDishItemById(id));
- }
- }
复制代码 (4)用户端业务层改造
用户端的UserSetmealServiceImpl不再调用Mapper,直接注入Feign客户端,像调用本地方法一样调用管理端的接口:- @Slf4j
- @Service("userSetmealServiceImpl")
- public class UserSetmealServiceImpl implements SetmealService {
- @Autowired
- private SetmealFeignClient setmealFeignClient;
- @Override
- public SetmealVO searchBycategoryId(String categoryId) {
- log.info("调用管理端微服务查询套餐,分类id:{}", categoryId);
- Result<SetmealVO> result = setmealFeignClient.searchBycategoryId(categoryId);
- return result.getData();
- }
- @Override
- public DishItemVO searchDishItemById(String id) {
- log.info("调用管理端微服务查询套餐菜品,套餐id:{}", id);
- Result<DishItemVO> result = setmealFeignClient.searchDishItemById(id);
- return result.getData();
- }
- }
复制代码 (5)网关层安全管控
在Spring Cloud Gateway里配置路由规则,拦截所有/inner前缀的请求,禁止外部用户访问内部接口,只允许微服务间内部调用:- spring:
- cloud:
- gateway:
- routes:
- - id: user-base-route
- uri: lb://sky-server-user-base
- predicates:
- - Path=/user/**
- - id: inner-api-block
- uri: lb://sky-server-user-base
- predicates:
- - Path=/inner/**
- filters:
- - SetStatus=403
复制代码 四、架构逻辑图解
下面用两张图,清晰对比改造前后的架构差异,一眼就能看出问题和优化效果(Mermaid流程图,掘金、CSDN等平台可直接渲染)。
1. 踩坑时的错误架构
graph TD A[前端小程序] --> B[用户端微服务
sky-server-user-base] C[商家管理后台] --> D[管理端微服务
sky-server-admin-operation] B --> E[SetmealMapper
同包名同名] D --> F[SetmealMapper
同包名同名] E --> G[MySQL数据库
setmeal表] F --> G[MySQL数据库
setmeal表] H[Nacos注册中心] --> B H --> D style E fill:#ffcccc,stroke:#ff0000 style F fill:#ffcccc,stroke:#ff0000 style G fill:#ffffcc,stroke:#ff6600核心问题:两个微服务都直连同一张表,重复定义同名Mapper,极易出现文件写混、数据不一致的问题,职责边界模糊。
2. 优化后的正确架构
graph TD A[前端小程序] --> B[用户端微服务
sky-server-user-base] C[商家管理后台] --> D[管理端微服务
sky-server-admin-operation] B --> E[SetmealFeignClient
声明式调用] E --> F[内部接口/inner
管理端暴露] F --> D D --> G[SetmealMapper
唯一数据操作入口] G --> H[MySQL数据库
setmeal表] I[Nacos注册中心
服务发现/负载均衡] --> B I --> D J[Sentinel
熔断降级] --> E K[Spring Cloud Gateway
拦截外部/inner请求] --> A style G fill:#ccffcc,stroke:#009900 style E fill:#cce5ff,stroke:#0066cc style F fill:#cce5ff,stroke:#0066cc核心优势:管理端是套餐数据的唯一入口,用户端仅通过内部接口调用获取数据,职责清晰,彻底解耦,自带容错和安全保障。
五、踩坑总结与面试收获
这次踩坑不仅让我解决了项目里的实际问题,更重要的是把微服务的理论知识彻底落地了一遍,还覆盖了面试90%的高频考点:
- 微服务设计原则:深刻理解了「高内聚、低耦合」,微服务拆分的核心是数据和职责的拆分,不是简单的代码拆分
- OpenFeign的核心优势:声明式调用让微服务调用像写本地方法一样简单,自动集成负载均衡、序列化,比传统的RestTemplate简洁太多
- 熔断降级的作用:Sentinel的fallback兜底机制,能有效防止服务雪崩,这是面试必问的核心点
- 内部接口的安全管控:通过/inner前缀区分内外接口,网关层拦截外部访问,是微服务安全的最佳实践
最后想说,踩坑不可怕,可怕的是踩完坑只解决了表面问题,没有从根源上优化架构。这次重构之后,不仅再也不会出现SQL写混文件的问题,整个项目的架构也更规范,面试的时候也有了能拿得出手的实战亮点。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |