微服务拆分套路拆解:BFF 服务于前端的法则与六大拆分原则,以电商为例

拆分微服务,先搞懂这七条法则 BFF 不是新概念,但翻车率极高 当你决定从单体拆微服务,问得最多的问题往往是:“前端到底该调哪个服务?” 见过太多次这种场景——前端对着十几个 API 接口陷入选择困难症:一个商品详情页要调 5 个服务才能拼完整,首页要调 8 个。于是前端自己写了个"聚合层",但没有服务端治理能力,比单体时代还乱。 BFF(Backend For Frontend,为前端服务的后端) 就是来解决这个的。它不是简单在前面加个代理,而是有明确的拆分法则。 BFF 拆分三法则 按客户端维度切分 不同端消费场景天然不同: 移动端 BFF:接口瘦、响应快、流量敏感,需要数据压缩和裁剪 Web 端 BFF:数据全、可交互多,可能需要 SSE 之类推送能力 小程序/第三方 BFF:安全校验严密,接口格式受平台约束 某团队早期把移动和 Web 共用一个 BFF,结果移动端要的"轻量接口"和 Web 端要的"完整数据"打架,BFF 越写越臃肿,成了一个"新型大单体"。 核心法则:一个端一个 BFF 实例。 代码可以复用,但部署实例要独立,避免互相影响。 BFF 只做编排,不做业务 BFF 层最容易踩的坑是"顺手把业务逻辑也写了"。 它的职责边界非常清晰: 该做的:接口聚合、数据裁剪、字段格式化、请求路由、Token 校验 不该做的:优惠计算、库存扣减、订单校验、风控规则 BFF 是服务员,不是厨师。厨师在后厨(业务服务),服务员只负责拼盘上菜。 关注点分离——BFF 不做跨服务事务 BFF 同时调了订单服务和库存服务,发现库存扣减成功但订单创建失败——这时候 BFF 能回滚吗?不能。BFF 层没有分布式事务能力。 碰到需要事务强一致的场景,BFF 必须把这个"烫手山芋"扔给下游的编排服务(比如用 Saga 模式),别自己在 BFF 层 try-catch 补偿。 flowchart LR subgraph Client["📱 客户端层"] WEB(["Web App"]) APP(["移动 App"]) MINI(["小程序"]) end subgraph BFF["🔀 BFF 层"] WB[Web BFF\n内容聚合+认证] MB[Mobile BFF\n数据裁剪+压缩] XB[三方 BFF\n签名校验+格式转换] end subgraph Biz["⚙️ 业务服务层"] BS[商品服务] CS[购物车服务] OS[订单服务] US[用户服务] PS[支付服务] end subgraph Store["💾 数据层"] DB[(MySQL)] CACHE[(Redis)] ES[(Elasticsearch)] end WEB -->|HTTP| WB APP -->|HTTP| MB MINI -->|HTTP| XB WB -->|RPC| BS & CS & OS & US & PS MB -->|RPC| BS & CS & US XB -->|RPC| OS & PS BS & CS & OS & US & PS --> Store classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2.5px,color:#fce7f3,font-weight:bold; classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold; classDef bff fill:#0f172a,stroke:#3b82f6,stroke-width:2.5px,color:#bfdbfe,font-weight:bold; class WEB,APP,MINI startEnd; class WB,MB,XB bff; class BS,CS,OS,US,PS process; class DB,CACHE,ES data; 业务能力拆分——最直觉的切法 最简单的拆分方式:按业务功能划分。电商天然就能分成商品、订单、用户、支付、库存这些模块。每个服务对应一个业务域,内部有独立数据库,对外暴露接口。 ...

二月 18, 2023 · 4 分钟 · 682 字 · yaomingye

DDD 重构实战——什么时候该用 DDD?什么时候 MVC 就够了?

DDD vs MVC:如何选择? 📖 前置阅读:本文假设读者已理解 DDD 的核心概念(实体/值对象/聚合根/限界上下文)和战术代码模板(四层架构/Repository/Domain Service)。如果还不熟悉,建议先阅读 DDD 本质 和 DDD 战术落地。 一、⚡ DDD 这么好——是不是所有服务都要重构一遍? 看完前两篇——概念清楚了——代码模板也有了——冲动上来了: "先把所有微服务用 DDD 重构一遍!" ① user-service → DDD ② order-service → DDD ③ product-service → DDD ④ account-service → DDD ⑤ inventory-service → DDD → 加班 2 个月——重构了一堆——代码没更好——反而更复杂了 DDD 不是银弹——不是所有代码都值得用 DDD。这篇的核心就是告诉你:什么该改、什么不改、改到什么程度。 二、🔍 诊断——我们现有的三个服务——各自是什么情况 2.1 user-service——经典 MVC——不改 // user-service——现有结构 controller/ └─ UserController.java @RestController——GET/POST/PUT service/ └─ UserService.java 简单的增删改查 + 缓存操作 mapper/ └─ UserMapper.java MyBatis——selectById/insert/update model/ └─ User.java 15 个字段——getter/setter // UserService 最复杂的方法——也就 20 行 @Service public class UserService { @Autowired private UserMapper userMapper; @Autowired private RedisTemplate<String, User> redisTemplate; public User getUser(Long userId) { String cacheKey = "user:" + userId; User cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) return cached; User user = userMapper.selectById(userId); if (user != null) redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES); return user; } public void updateUser(User user) { user.setUpdatedAt(LocalDateTime.now()); userMapper.updateById(user); redisTemplate.delete("user:" + user.getId()); // 失效缓存 } } 判断——不需要 DDD: ...

十二月 23, 2022 · 13 分钟 · 2614 字 · yaomingye

DDD 战术落地——代码怎么写

DDD 代码怎么写? 📖 前置阅读:本文假设读者已理解实体、值对象、聚合根、限界上下文、领域事件的核心概念。如果还不熟悉,建议先阅读 DDD 本质——领域驱动设计的核心概念。 一、⚡ 概念都懂了——但代码从哪个 package 开始建? 上一篇搞清楚了实体和值对象的区别、聚合根是"一致性边界"——但回到 IDE 中: 现有项目结构(MVC——三层): controller/ ├─ OrderController.java service/ ├─ OrderService.java (3000 行——上帝类) mapper/ ├─ OrderMapper.java ├─ UserMapper.java ← 跨表调用——OrderMapper 也调 UserMapper ├─ ProductMapper.java ← 跨表调用 model/ ├─ Order.java ← 只有 getter/setter——贫血 ├─ User.java ├─ Product.java 问题——现在要改成 DDD——应该怎么建目录?Repository 放哪?Domain Service 放哪? 这篇就是答案——从目录结构开始——到每一层的代码——完整的落地模板。 二、📂 项目结构——DDD 四层架构 2.1 四层——不是"三层 + 一层" 传统 MVC 三层: Controller → Service → Mapper → Service 层无限膨胀——3000 行——什么都往里塞 DDD 四层: interfaces(接口层) → 接收请求、返回响应——薄薄一层 application(应用层) → 编排业务流程——调 Repository、发事件——没有业务逻辑 domain(领域层) → 业务逻辑——聚合根、值对象、Repository 接口、领域事件 infrastructure(基础设施层)→ 技术实现——Repository 实现、数据库访问、MQ 发送 order-service/ ├── interfaces/ ← ① 接口层 │ ├── rest/ │ │ └── OrderController.java # HTTP 接口——接受请求——转给 application 层 │ ├── dto/ │ │ ├── CreateOrderRequest.java # 入参 DTO │ │ └── OrderResponse.java # 出参 DTO │ └── mq/ │ └── OrderEventListener.java # MQ 消息消费——转到 application 层 │ ├── application/ ← ② 应用层 │ ├── OrderApplicationService.java # 编排——调 Repository + 发事件——不包含业务逻辑 │ ├── command/ │ │ └── CreateOrderCommand.java # 应用层自己的命令对象——DTO 转换后的内部对象 │ └── event/ │ └── OrderEventPublisher.java # 事件发布接口——实现在 infrastructure │ ├── domain/ ← ③ 领域层——核心——不依赖任何外部框架 │ ├── model/ │ │ ├── aggregate/ │ │ │ └── Order.java # 聚合根 │ │ ├── entity/ │ │ │ └── OrderItem.java # 聚合内部实体 │ │ ├── valueobject/ │ │ │ ├── Money.java # 值对象——金额 │ │ │ ├── Address.java # 值对象——地址 │ │ │ └── OrderStatus.java # 枚举——订单状态 │ │ └── event/ │ │ ├── OrderCreatedEvent.java # 领域事件 │ │ └── OrderPaidEvent.java │ ├── repository/ │ │ └── OrderRepository.java # Repository 接口——只有接口——没有实现 │ └── service/ │ ├── OrderDomainService.java # 领域服务——跨聚合的逻辑 │ └── PricingService.java # 领域服务——价格计算策略 │ └── infrastructure/ ← ④ 基础设施层 ├── persistence/ │ ├── OrderRepositoryImpl.java # Repository 实现——调 JPA/MyBatis │ ├── mapper/ │ │ ├── OrderMapper.java # MyBatis Mapper │ │ └── OrderItemMapper.java │ └── converter/ │ └── OrderConverter.java # DO ↔ Domain 对象转换 ├── messaging/ │ └── RocketMQEventPublisher.java # 事件发布实现——发到 RocketMQ └── external/ └── UserServiceAdapter.java # 防腐层——隔离外部 User 服务 依赖方向——只能是单向的: ...

十二月 22, 2022 · 12 分钟 · 2499 字 · yaomingye

DDD 本质——领域驱动设计的核心概念

搞懂 DDD 的核心概念 一、⚡ 一个下单方法 800 行——你知道拆不开是因为什么吗? 先看一段熟悉的代码——我们所有微服务的 Controller/Service 大概都长这样: @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private UserMapper userMapper; @Autowired private ProductMapper productMapper; @Autowired private InventoryMapper inventoryMapper; public Order createOrder(CreateOrderRequest request) { // ① 查用户——有没有被封号 User user = userMapper.selectById(request.getUserId()); if (user == null || user.getStatus() == UserStatus.BANNED) { throw new BusinessException("用户不存在或已封号"); } // ② 查商品——库存够不够 BigDecimal totalAmount = BigDecimal.ZERO; List<OrderItem> items = new ArrayList<>(); for (CreateOrderItemRequest itemReq : request.getItems()) { Product product = productMapper.selectById(itemReq.getProductId()); if (product == null || product.getStatus() != ProductStatus.ON_SALE) { throw new BusinessException("商品 " + itemReq.getProductId() + " 不可售"); } if (product.getStock() < itemReq.getQuantity()) { throw new BusinessException("商品 " + itemReq.getProductId() + " 库存不足"); } // ③ 扣库存——直接在 Service 里 UPDATE product.setStock(product.getStock() - itemReq.getQuantity()); productMapper.updateById(product); totalAmount = totalAmount.add(product.getPrice() .multiply(BigDecimal.valueOf(itemReq.getQuantity()))); items.add(new OrderItem(itemReq.getProductId(), itemReq.getQuantity(), product.getPrice())); } // ④ 扣余额——直接操作 Account 表 Account account = accountMapper.selectByUserId(request.getUserId()); if (account.getBalance().compareTo(totalAmount) < 0) { throw new BusinessException("余额不足"); } account.setBalance(account.getBalance().subtract(totalAmount)); accountMapper.updateById(account); // ⑤ 创建订单 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(request.getUserId()); order.setTotalAmount(totalAmount); order.setStatus(OrderStatus.PENDING_PAY); order.setItems(items); orderMapper.insert(order); // ⑥ 发通知——MQ rocketMQTemplate.syncSend("order-created", order); return order; } } 问题不是代码长——问题是:你想加一个"首单 9 折"的功能——该加在哪? ...

十二月 21, 2022 · 12 分钟 · 2357 字 · yaomingye

Spring Cloud Alibaba 微服务中间件体系概念解析

Spring Cloud Alibaba 微服务中间件体系概念解析:从单体拆分到组件选型的避坑指南 📖 一、开篇:一个电商系统的"拆服务"血泪史 某人接手了一个电商项目。最开始就一个 Spring Boot 单体,订单、库存、支付、物流全塞在一起。单机跑得飞快,部署就一个 jar 包,轻松得很。 然后业务起来了。 大促期间,用户疯狂下单,库存扣减开始排队。支付回调偶尔超时,整个服务直接 502。最要命的是改一行订单逻辑,得把整个项目重新部署一遍。一次发布,全员瑟瑟发抖。 于是开始拆微服务。 📌 前置知识:微服务(Microservice)是一种架构风格,把一个大应用拆成多个独立部署的小服务,每个服务有自己的数据库和业务边界,服务之间通过网络(HTTP/RPC/MQ)通信。 拆完之后,新问题来了——不是技术的,是运维的。服务之间怎么发现对方?怎么保证不出错?出错了怎么处理? 以前一个方法调用 orderService.deduct() 就行,现在得想:库存服务在哪台机器上?万一它挂了怎么办?万一它响应太慢拖死订单服务怎么办? 这些问题,每一家互联网公司都会遇到。阿里巴巴把自己踩过的坑、写的解决方案打包开源,就是今天的 Spring Cloud Alibaba(一套与 Spring Cloud 生态集成的微服务中间件集合,由阿里巴巴开源)。 这篇博客不是教你写代码的。是让你看完之后,能跟同事说清楚:“网关是用来干什么的?Sentinel 和 Hystrix 选哪个?Nacos 和 Eureka 有什么区别?什么时候该用 Seata,什么时候千万别用?” ⚠️ 新手提示:如果你刚接触微服务,先记住一句话——微服务不是银弹。如果你系统 QPS(每秒请求数)不到 100,用微服务是给自己找麻烦。 单体 + Nginx 负载均衡,能解决你 90% 的问题。 🗺️ 二、总览图:六大组件,一张图看清 先把全景图画出来。一个标准的微服务架构,从上到下由这几个关键组件拼成: flowchart LR classDef entry fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold; classDef gateway fill:#2d1a05,stroke:#f59e0b,stroke-width:2px,color:#fde68a,font-weight:bold; classDef protect fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold; classDef rpc fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold; classDef registry fill:#0f172a,stroke:#3b82f6,stroke-width:1.5px,color:#bfdbfe,font-weight:bold; classDef mq fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; classDef tx fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; subgraph ACCESS_LAYER["接入层"] USER[用户/客户端] GW["Gateway 网关\n路由 + 限流 + 鉴权"] end subgraph SERVICE_LAYER["服务层"] ORDER[订单服务] STOCK[库存服务] PAY[支付服务] end subgraph MIDDLEWARE["中间件层"] NACOS["Nacos\n注册中心 + 配置中心"] SENTINEL["Sentinel\n流量控制 + 熔断降级"] ROCKETMQ["RocketMQ\n异步消息"] SEATA["Seata\n分布式事务"] end USER --> GW GW --> ORDER GW --> STOCK GW --> PAY ORDER -->|RPC调用| STOCK ORDER -->|RPC调用| PAY STOCK -->|RPC调用| PAY ORDER -.->|注册/发现| NACOS STOCK -.->|注册/发现| NACOS PAY -.->|注册/发现| NACOS SENTINEL -.->|保护| ORDER SENTINEL -.->|保护| STOCK SENTINEL -.->|保护| PAY ORDER -->|发送消息| ROCKETMQ ROCKETMQ -->|消费消息| STOCK SEATA -.->|协调事务| ORDER SEATA -.->|协调事务| STOCK SEATA -.->|协调事务| PAY class USER entry; class GW gateway; class SENTINEL protect; class ORDER,STOCK,PAY rpc; class NACOS registry; class ROCKETMQ mq; class SEATA tx; 是不是有点懵?没关系,拆开看。每一层就干一件事: ...

十月 11, 2022 · 5 分钟 · 948 字 · yaomingye

一次性讲明白 Filter、Interceptor、RequestAdvice、ResponseAdvice

一次性讲明白 Filter、Interceptor、RequestAdvice、ResponseAdvice:执行顺序与实际应用 一、从一个常见需求说起 开发一个 Web 接口,通常需要处理以下事情: 记录每个请求的耗时日志 校验登录态,未登录拒绝访问 对请求参数做预处理(比如解密、格式转换) 对响应结果做统一封装(比如统一返回 {code, msg, data} 格式) 这四个需求对应的正是四个组件: 需求 对应组件 执行位置 记录请求日志 Filter Servlet 容器层(最外层) 登录校验 Interceptor Spring MVC 层(Controller 前后) 请求参数预处理 RequestAdvice Controller 方法执行前 响应统一封装 ResponseAdvice Controller 方法执行后 这四个组件在一条请求链路中各自负责不同的阶段。先看一张总览图,建立位置感: flowchart TD %% 半暗底色 + 高亮描边:完美适配博客深色/浅色双主题 %% classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:2px,color:#e5e7eb; classDef data fill:#052e16,stroke:#16a34a,stroke-width:2px,color:#bbf7d0,font-weight:bold; classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2.5px,color:#fce7f3,font-weight:bold; A[客户端请求] --> B[Filter] B --> C[DispatcherServlet] C --> D[Interceptor.preHandle] D --> E[RequestAdvice] E --> F[Controller] F --> G[ResponseAdvice] G --> H[Interceptor.postHandle] H --> I[Interceptor.afterCompletion] I --> J[Filter 返回] J --> K[客户端响应] class E,G data; class A,B,C,D,F,H,I,K process; class J startEnd; 这张图只需要记住一个核心原则: Filter 在最外层,Interceptor 在中间层,Advice 在最内层(紧贴 Controller)。 请求进来从外到内,响应出去从内到外。 ...

十月 9, 2022 · 7 分钟 · 1426 字 · yaomingye

从单体到微服务

🏗️ 从单体到微服务:拆分决策、业务边界分析与中间件选型全指南 🏗️ 一、问题切入:一个电商系统的"临界点" 假设你接手了一个运行了两年的电商单体应用。它使用 Spring Boot + MyBatis + MySQL 开发,所有模块——用户、商品、订单、库存、支付、物流——都在一个 Git 仓库、一个进程、一个数据库里运行。 刚开始 3 个开发,CI/CD 流水线 3 分钟跑完。现在团队扩到了 18 人,一个订单功能的改动要等 UI 模块的测试先跑完才能部署。上周,运营活动模块的内存泄漏导致支付服务也一起挂了——整个系统 40 分钟不可用。 这种场景不是假设,它是大多数高速增长的业务最终都会撞上的临界点。接下来的问题是: 你该不该拆?如果拆,怎么拆?拆完各服务怎么通信?中间件怎么选? 这篇文章回答这四个问题。 flowchart TD %% ========================================== %% 样式定义 %% ========================================== classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold;; classDef problem fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold;; classDef question fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold;; subgraph MONOLITH ["单体架构现状"] M[单个 Spring Boot 进程\n所有模块耦合在一起] --> S1[代码冲突频繁\n18 人改同一仓库] M --> S2[部署互相阻塞\n改订单要等 UI 构建] M --> S3[故障无隔离\n内存泄漏拖垮全站] end subgraph DECISION ["你需要回答四个问题"] Q1([该不该拆?]) --> Q2([怎么拆?]) Q2 --> Q3([怎么通信?]) Q3 --> Q4([中间件选什么?]) end S1 -.->|推动决策| Q1 S2 -.->|推动决策| Q1 S3 -.->|推动决策| Q1 class M startEnd; class S1,S2,S3 problem; class Q1,Q2,Q3,Q4 question; 🏗️ 二、什么时候该拆:六个关键信号 拆分的收益永远伴随着代价——分布式事务、网络延迟、运维复杂度。在讨论"怎么拆"之前,必须先确认"该不该拆"。以下六个信号同时出现 3 个以上时,才值得启动拆分。 ...

十月 3, 2022 · 8 分钟 · 1559 字 · yaomingye
Cat Radio