混合架构源码剖析:3个关键坑点与完整示例 混合架构源码剖析:3个关键坑点与完整示例 官方文档翻了三遍还是没看懂?别急,大多数人都卡在“概念太多、代码太散”这一步。今天直接上完整示例,用 3 个真实踩坑案例拆解混合架构的核心逻辑,看完就能在面试或项目中直接用。 项目目标:为什么必须搞懂混合架构 应届毕业找后端或全栈岗位,简历里写“熟悉微服务”的人太多,但能讲清楚混合部署模式下数据一致性、服务间调用链路的少之又少。混合架构不是简单的“单体+微服务拼凑”,它涉及同步与异步消息混合、本地缓存与分布式缓存混合、强一致与最终一致混合等复杂场景。 根据 2023 年某招聘平台数据,后端岗位 JD 中“混合架构”相关描述占比从 2021 年的 12% 升至 2023 年的 28%。这不是趋势,是现实——中大型业务系统不可能纯单体,也不可能纯微服务,必然是混合。你如果连混合架构的边界都没摸清楚,面试官问“为什么这里用 MQ 而不是 RPC?”你只能答“因为性能高”,这种答案直接淘汰。 核心目标:通过一个订单系统实战,掌握混合架构中三个高频痛点——服务降级策略、分布式事务补偿、多级缓存穿透防护,每个点都配可运行的完整示例。 目录结构:先看清骨架再填肉 别急着写代码,先搭好目录。混合架构项目最容易乱的地方就是模块边界不清。以下是我验证过多次的目录结构,按职责划分,不是按技术栈划分: order-service/ ├── api/ # 对外接口层,只做参数校验和路由 │ └── OrderController.java ├── domain/ # 领域层,核心业务逻辑 │ ├── entity/ # 订单实体 │ ├── service/ # 业务服务 │ └── strategy/ # 策略模式:降级、补偿 ├── infra/ # 基础设施层 │ ├── cache/ # 多级缓存实现 │ ├── mq/ # 消息队列封装 │ └── rpc/ # RPC 调用封装 ├── common/ # 公共模块 │ ├── exception/ # 统一异常处理 │ └── config/ # 配置类 └── test/ # 单元测试与集成测试 关键原则:domain 层不依赖 infra 层的具体实现,只依赖接口。这样换 MQ 实现、换缓存实现时,领域层零改动。很多新人项目一上来就把 Redis 操作写在 Service 里,后期重构哭都来不及。 核心代码实现:三个痛点的完整示例 1. 服务降级:别只写 try-catch 混合架构中,下游服务(库存、支付)挂掉是常态。很多新人只会写: try { inventoryService.deduct(orderId); } catch (Exception e) { log.error(扣减库存失败, e); } 这根本不算降级,这叫“吞异常”。真正的降级要有熔断、兜底、恢复三要素。以下是基于 Resilience4j 的完整示例,生产环境可直接用: @Service public class OrderService { @Autowired private InventoryService inventoryService; // 定义熔断器:5秒内错误率超50%则熔断 private final CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults(); public OrderResult createOrder(OrderDTO dto) { // 包装下游调用,加上熔断保护 SupplierOrderResult orderSupplier = () - { // 调用库存服务,带超时控制 boolean success = inventoryService.deduct(dto.getSkuId(), dto.getQuantity()); if (!success) { throw new BizException(库存不足); } // 本地事务:创建订单记录 Order order = OrderFactory.fromDto(dto); orderRepository.save(order); return OrderResult.success(order.getId()); }; // 执行带熔断的调用 OrderResult result = circuitBreakerRegistry .circuitBreaker(inventoryService) .executeSupplier(orderSupplier) .withFallback((supplier, throwable) - { // 降级逻辑:记录到补偿队列,稍后重试 log.warn(库存服务熔断,订单进入补偿队列, throwable); compensationQueue.enqueue(new CompensationTask(orderId, throwable)); return OrderResult.degraded(系统繁忙,请稍后重试); }); return result; } } 逐行讲解: CircuitBreakerRegistry 是 Resilience4j 的核心,每个下游服务独立一个熔断器实例,避免“一个服务挂,全局熔断”。 withFallback 是降级钩子,这里选择“进入补偿队列”而非直接返回错误,保证用户体验不中断。 CompensationTask 是补偿任务的载体,后续由定时任务扫描并重试。 避坑点:很多团队把降级策略写成“返回默认值”,比如库存不足时返回“有货”。这是严重事故,用户下单后支付成功,发货时发现没货,投诉直接爆炸。降级只能返回“可重试状态”,不能返回“错误数据”。 2. 分布式事务补偿:Saga 模式实战 混合架构中,订单、库存、支付三个服务分属不同数据库,本地事务失效。很多人第一反应是“用 Seata”,但 Seata 的 AT 模式有锁表问题,TP 模式要改代码。Saga 模式更适合这种场景:每个服务维护自己的本地事务,通过消息驱动正向或反向操作。 以下是订单创建失败后的补偿完整示例: @Component public class OrderSagaProcessor { @Autowired private InventoryService inventoryService; @Autowired private PaymentService paymentService; @Autowired private OrderRepository orderRepository; // 处理订单取消时的补偿逻辑 @Transactional public void compensateOrder(String orderId) { Order order = orderRepository.findById(orderId); if (order == null || !order.getStatus().equals(OrderStatus.CREATED)) { log.warn(订单状态异常,无需补偿: {}, orderId); return; } try { // 1. 如果已扣库存,回滚库存 if (order.getInventoryDeducted()) { inventoryService.rollback(order.getSkuId(), order.getQuantity()); order.setInventoryDeducted(false); } // 2. 如果已支付,发起退款 if (order.getPaymentCompleted()) { paymentService.refund(order.getPaymentId(), order.getAmount()); order.setPaymentCompleted(false); } // 3. 更新订单状态为已取消 order.setStatus(OrderStatus.CANCELLED); orderRepository.save(order); log.info(订单补偿成功: {}, orderId); } catch (Exception e) { log.error(订单补偿失败,需人工介入: {}, orderId, e); // 记录到死信队列,告警通知 deadLetterQueue.send(new CompensationAlert(orderId, e.getMessage())); throw e; // 重新抛出,让 MQ 重试 } } } 关键点: 幂等性:每个补偿操作必须幂等。比如 inventoryService.rollback 内部要判断“是否已回滚”,避免重复回滚导致库存负数。 补偿顺序:先回滚库存,再退款,再更新订单。顺序错了会出现“钱退了但库存没还”的数据不一致。 死信队列:补偿失败不能静默吞掉,必须进入死信队列并告警。生产环境人工介入是兜底手段,不是常态,但不能没有。 3. 多级缓存穿透防护:别只加空值缓存 用户查询不存在的商品 ID,缓存未命中,直接打到数据库,数据库查不到,再写缓存空值?这招防不住恶意攻击——攻击者用 10 万个随机 ID 打接口,每个都打到数据库,数据库直接雪崩。 以下是多级缓存+布隆过滤器+空值缓存的完整示例: @Service public class ProductService { @Autowired private RedisTemplateString, Object redisTemplate; @Autowired private BloomFilterLong skuIdBloomFilter; @Autowired private ProductRepository productRepository; public Product getProduct(Long skuId) { String cacheKey = product:sku: + skuId; // 1. 布隆过滤器预判:如果肯定不存在,直接返回 if (!skuIdBloomFilter.mightContain(skuId)) { log.debug(布隆过滤器拦截: {}, skuId); return null; } // 2. 查 L1 缓存(本地 Caffeine) Product cached = localCache.getIfPresent(skuId); if (cached != null) { return cached; } // 3. 查 L2 缓存(Redis) Object redisValue = redisTemplate.opsForValue().get(cacheKey); if (redisValue != null) { // 命中空值缓存 if (redisValue.equals(EMPTY_MARKER)) { return null; } Product product = (Product) redisValue; localCache.put(skuId, product); // 回填 L1 return product; } // 4. 查数据库 Product product = productRepository.findById(skuId); if (product == null) { // 写空值缓存,TTL 60s redisTemplate.opsForValue().set(cacheKey, EMPTY_MARKER, 60, TimeUnit.SECONDS); return null; } // 5. 回填两级缓存 redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES); localCache.put(skuId, product); return product; } } 避坑点: 布隆过滤器误判率:设置为 0.01(1%),即 1% 的概率把存在的 ID 判为不存在。这个误判会导致用户查不到真实商品,所以布隆过滤器只用于“肯定不存在”的拦截,不能用于“肯定存在”的判断。 空值缓存 TTL:不能设太长,否则新增商品后缓存里还是空值。60s 是平衡点,既防穿透又不过期太慢。 L1 缓存一致性:本地缓存和 Redis 可能不一致,但商品查询场景可接受。如果是余额、库存这类强一致数据,禁用 L1 本地缓存。 运行与测试:别只跑单元测试 混合架构的测试难点在于依赖隔离。你不能在单元测试里连真实的 MQ 和 Redis,也不能在集成测试里真的发支付请求。 测试分层策略: 测试类型 覆盖范围 工具 依赖处理 单元测试 领域逻辑、策略类 JUnit 5 + Mockito 全部 Mock 集成测试 服务间调用、缓存、MQ Testcontainers 真实 Redis/MQ 容器 混沌测试 降级、补偿、故障恢复 Chaos Monkey 注入网络延迟、服务宕机 完整示例:混沌测试验证降级 @SpringBootTest @Testcontainers public class OrderServiceChaosTest { @Container static GenericContainer? inventoryService = new GenericContainer(inventory-service:latest) .withNetworkMode(host); @Autowired private OrderService orderService; @Autowired private ChaosMonkey chaosMonkey; @Test public void testDegradationWhenInventoryDown() { // 1. 正常场景:订单创建成功 OrderResult result1 = orderService.createOrder(validDto); assertThat(result1.isSuccess()).isTrue(); // 2. 注入故障:让库存服务响应超时 chaosMonkey.injectLatency(inventory-service, Duration.ofSeconds(10)); // 3. 熔断后:订单进入降级 OrderResult result2 = orderService.createOrder(validDto); assertThat(result2.isDegraded()).isTrue(); assertThat(result2.getMessage()).contains(系统繁忙); // 4. 验证补偿队列中有任务 ListCompensationTask tasks = compensationQueue.peek(); assertThat(tasks).isNotEmpty(); assertThat(tasks.get(0).getOrderId()).isEqualTo(result2.getOrderId()); } } 关键:混沌测试必须覆盖“故障注入→熔断触发→降级执行→补偿入队”全链路。只测降级返回,不测补偿队列,等于没测。 优化扩展:生产环境的三个进阶技巧 熔断器指标可视化:Resilience4j 默认不暴露指标,必须接入 Micrometer + Prometheus。否则你只知道“服务挂了”,不知道“哪个熔断器触发了”、“错误率多少”。生产环境没有监控的降级策略等于没有。 补偿任务优先级:不是所有补偿任务都同等重要。支付失败的订单优先级高于库存回滚,因为涉及资金。补偿队列要支持优先级设置,高优先级任务插队执行。 缓存预热:系统启动时,把热点商品数据预热到 L1 和 L2 缓存。否则冷启动时大量请求直接打到数据库。预热脚本要独立于业务代码,通过配置开关控制。 小结:混合架构不是技术炫技 混合架构的本质是在成本、性能、一致性之间做权衡,不是把最牛的技术全堆上去。应届生最容易犯的错误是“为了用而用”——明明单体能解决的,非要拆微服务;明明同步调用够用的,非要上 MQ。 记住三个原则: 能用同步不用异步:同步链路短、调试容易,异步只在“解耦”或“削峰”场景用。 能不强一致就不强一致:最终一致性能提升吞吐量 5-10 倍,业务能接受就别较真。 降级必须有兜底:没有兜底的降级就是故障放大器。 你公司项目里是怎么处理混合架构中的服务降级的?是直接用 Resilience4j,还是自己写的?欢迎评论区聊聊,特别是踩过坑的,说说你的补偿策略怎么设计的。