Loading

【WebWiki】06 · 微服务

07 微服务(Spring Cloud Alibaba)

WebWiki · 微服务篇

0. 理论基础:CAP 与 BASE

微服务本质是分布式系统,一切设计取舍都绕不开 CAP。先把这些理论底座打牢,后面看 Nacos/Eureka 选 AP 还是 CP、看分布式事务为什么只能最终一致,就都顺理成章了。

0.1 CAP 三选二

CAP 是分布式系统在「网络分区必然发生」前提下,对三个属性的取舍:

  • C(Consistency,一致性):每次读取都能拿到最新写入。注意这里指线性一致性(强一致)。
  • A(Availability,可用性):每个非故障节点都能在有限时间内返回非错误响应(不保证是最新数据)。
  • P(Partition tolerance,分区容错性):网络分片(部分节点互相不通)时系统仍能继续工作。

关键认知:P 在分布式系统中几乎必须选。只要允许节点间通过网络通信,分区就可能发生,因此只能在 C 与 A 之间二选一——这就是「CAP 三进二」的真实含义(不是三个里随手选俩)。

组合 含义 典型代表
CP 保一致、弃可用,分区时宁可拒绝服务 Zookeeper(注册中心)
AP 保可用、弃强一致,接受短暂不一致 Eureka(注册中心)、Nacos(默认 AP)
CA 单点系统才可能,分布式里不存在 单机 MySQL

0.2 BASE:AP 系统的妥协理论

强一致太贵,工程上退一步提出 BASE,描述「最终一致」的系统的特征:

  • BA(Basically Available,基本可用):故障时不雪崩,允许响应时间变长或部分功能降级。
  • S(Soft State,软状态):允许系统中间状态存在,不同节点数据暂时不一致。
  • E(Eventually Consistent,最终一致):没有新写入后,经过一段时间所有副本数据会趋于一致。

一句话:BASE 是 CAP 中 AP 路线的哲学落地——不追求时时刻刻一致,但保证最终能对上。后面 Seata AT、本地消息表、SAGA 都是 BASE 思想的实现。

0.3 一致性的几个层次

面试常混淆,这里厘清:

  • 强一致:写后读立刻可见(如单库事务)。
  • 最终一致:副本异步同步,短暂不一致后收敛(如 MySQL 主从、Redis 主从)。
  • 弱一致:写入后不保证何时可见,甚至不保证最终可见(如单纯写缓存不回源)。
  • 因果一致 / 会话一致 / 单调读一致:更强于最终一致、弱于强一致的变种,按需选用。

1. 什么时候拆微服务

先单体,后拆分。判断信号:团队 ≥ 3 个小组频繁互相阻塞、模块部署节奏互相牵制、单模块流量需要独立扩容。拆分粒度以业务能力为界(用户、订单、商品、支付),两个服务间的调用与一个服务内的方法调用,运维成本差一个数量级。

微服务拓扑

1.1 拆分原则(康威定律 + DDD)

  • 康威定律:系统结构会映射组织结构——团队怎么划分,服务就怎么拆。
  • 单一职责 / 高内聚低耦合:一个服务只干一类事,数据库私有,不共享表。
  • 限界上下文(DDD):用领域驱动划分边界,订单域、库存域、用户域各自独立。
  • 避免分布式单体:拆了却还要一起部署、一个挂全挂,等于白拆。

1.2 不拆的代价 vs 拆的代价

维度 单体 微服务
开发部署 简单,一次打包 复杂,CI/CD/多仓库
技术栈 统一 可异构
故障隔离 差,一处崩全崩 好,熔断降级隔离
数据一致 本地事务 分布式事务难题
运维 低 高(需注册/配置/网关/监控)

经验:日活万级以内、团队 < 10 人,单体 + 模块化(module 分包)往往更高效;等真遇到「扩展瓶颈 / 组织瓶颈」再拆。

2. 组件全景(Spring Cloud Alibaba)

组件 角色 说明
Nacos 注册中心 + 配置中心 服务发现、动态配置、命名空间隔离环境
Spring Cloud Gateway API 网关 统一入口:路由、鉴权、限流、灰度
OpenFeign 服务间调用 声明式 HTTP 客户端,接口+注解即调用
LoadBalancer 客户端负载均衡 轮询/随机/权重
Sentinel 流量治理 限流、熔断、降级、热点参数限流
Seata 分布式事务 AT 模式为主,TCC/SAGA 兜底
SkyWalking 链路追踪 TraceId 串联全链路,拓扑与慢调用分析
xxl-job 分布式调度 定时任务统一管理、分片广播

为什么选 Spring Cloud Alibaba 而非纯 Netflix? Netflix 系(Eureka/Ribbon/Hystrix/Zuul)已进入维护模式(Hystrix、Ribbon、Zuul 1 相继停止活跃开发),Spring Cloud Alibaba 由阿里开源、社区活跃、与云原生兼容好,Nacos 一个组件同时解决注册 + 配置两件事,Sentinel 比 Hystrix 功能更全(规则动态、热点、系统保护),所以新项目基本都选 SCA。

3. Nacos:注册与配置

3.1 注册中心原理(心跳 + 推送)

Nacos 同时支持 AP(Distro 协议) 与 CP(Raft 协议) 两种模式:

  • 临时实例(默认,AP):服务实例用客户端心跳保活,默认每 5 秒发一次心跳;服务端 15 秒没收到心跳就标记为不健康,30 秒剔除。注册表在节点间异步复制,牺牲强一致换高可用。
  • 持久实例(CP):注册信息写入后由 Raft 保证各节点强一致,适合需要稳定不丢的节点。

服务发现的两类推送:

  1. Pull(拉):客户端本地缓存服务列表,定时(默认 10s)向 Nacos 轮询 getAllInstances 拉取最新。
  2. Push(推):当服务端实例列表变更,Nacos 通过长连接(UDP / gRPC 双向流)主动把变更推给订阅的客户端,客户端更新本地缓存。这是 Eureka 只靠 Pull 的升级点——变更感知更实时。

对比 Eureka:Eureka 客户端每 30s 发心跳、每 30s 拉一次注册表,服务端 90s 无心跳剔除,靠纯 Pull + 自我保护。Nacos 心跳更频繁 + 支持 Push,感知更快,且同时提供配置中心能力,所以「注册 + 配置」一套搞定。

3.2 配置中心原理

  • 配置存 Nacos Server(底层可接 MySQL 持久化),客户端启动时拉取 dataId/group/namespace 对应配置。
  • 配置变更时,Nacos 通过长轮询(客户端 30s 阻塞请求,服务端有变更立即返回)通知客户端。
  • 客户端收到变更 → 发布 RefreshEvent → 标注 @RefreshScope 的 Bean 被销毁重建,注入新值。
spring:
  cloud:
    nacos:
      discovery:
        server-addr: nacos:8848
        namespace: prod            # 环境隔离:dev/test/prod 各一个命名空间
      config:
        server-addr: nacos:8848
        file-extension: yaml
        shared-configs:            # 多服务共享配置
          - data-id: common-redis.yaml

配置动态刷新:

@RestController
@RefreshScope                 // 配置变更时自动重建 Bean
public class BizController {
    @Value("${biz.switch.order:false}")
    private boolean orderSwitch;
}

namespace / group / dataId 三层模型:

  • namespace:环境隔离(dev/test/prod),互相不可见。
  • group:同一环境内分组(如按业务域 user-group、order-group)。
  • dataId:具体配置文件,通常 {服务名}-{profile}.yaml。

4. OpenFeign 声明式调用

4.1 原理

OpenFeign 在编译期基于接口和 @RequestMapping 注解生成动态代理(JDK Proxy / CGLIB)。调用方法时:

  1. 解析注解拼出 HTTP 请求(URL、方法、参数、Header);
  2. 通过 LoadBalancer 选一个实例(把服务名 stock-service 解析成具体 IP:Port);
  3. 用底层 HTTP 客户端(默认 HttpURLConnection,可换 OkHttp/Apache HttpClient)发出请求;
  4. 把响应体按返回类型反序列化。

本质是「接口即契约」,比 Ribbon + RestTemplate 手写 URL 优雅得多。

// mall-api 模块:定义接口(生产者实现,消费者引用)
@FeignClient(name = "stock-service", fallback = StockClientFallback.class)
public interface StockClient {

    @PostMapping("/api/stock/deduct")
    R<Boolean> deduct(@RequestBody DeductReq req);
}

// 消费方像调本地方法一样调用
public void createOrder(CreateOrderReq req) {
    R<Boolean> ok = stockClient.deduct(new DeductReq(req.skuId(), req.count()));
    if (!ok.getData()) throw new BizException("库存不足");
}

// 降级:依赖服务挂掉时的兜底
@Component
public class StockClientFallback implements StockClient {
    public R<Boolean> deduct(DeductReq req) {
        return R.fail(503, "库存服务暂不可用,请稍后重试");
    }
}

超时与重试务必显式配置(默认很激进):

spring:
  cloud:
    openfeign:
      client:
        config:
          stock-service:
            connect-timeout: 1000
            read-timeout: 3000
            retryer: never        # 写操作禁用自动重试

重试的坑:OpenFeign 默认 retryer 对读失败会重试,但下单、扣库存这类写操作重试可能重复扣款!务必对写接口设 retryer: never,重试只留给幂等查询。幂等设计(唯一订单号 + 去重表)是另一道保险。

5. Gateway 网关

5.1 路由与过滤器链

Gateway 核心三概念:Route(路由)、Predicate(断言/匹配条件)、Filter(过滤器)。一个请求进来后:

  1. 路由匹配:按 Predicate(Path、Header、Method、Host、时间等)找到命中的 Route;
  2. 过滤器链(GatewayFilter + GlobalFilter):在转发前后织入逻辑(鉴权、限流、改写 Header、打日志);
  3. 负载均衡:URI 写成 lb://order-service 时,由 LoadBalancer 选出实例并替换地址;
  4. 转发:把请求打到真实服务。
spring:
  cloud:
    gateway:
      routes:
        - id: order-route
          uri: lb://order-service          # lb:// 走负载均衡
          predicates:
            - Path=/api/order/**
          filters:
            - StripPrefix=1                # 去掉 /api 前缀
            - name: RequestRateLimiter     # 内置令牌桶限流
              args:
                redis-rate-limiter.replenish-rate: 100
                redis-rate-limiter.burst-capacity: 200

5.2 过滤器链执行顺序(深度)

  • GlobalFilter:全局生效(如鉴权、跨域、链路 TraceId 注入),按 Ordered 排序。
  • GatewayFilter:绑定到具体 Route(如 StripPrefix、限流)。

典型顺序:请求 → NettyWebServer → GatewayReactiveWebFilter → 全局鉴权 Filter → 路由匹配 → 路由级 Filter → 负载均衡 → 下游服务;响应原路返回。

网关层典型职责:JWT 统一鉴权 → Header 注入 userId、按 API 维度限流、灰度路由(按 header/权重分流到金丝雀实例)。

为什么网关做鉴权而不是每个服务做? 网关是统一入口(BFF/边缘),一次校验全链路受益;下游服务假设「能进来就是已认证」,只做细粒度授权。这叫「边缘认证、内部信任」。若没有网关(服务直接暴露),则每个服务都要接 JWT,重复且易漏。

5.3 灰度/金丝雀路由示例

spring:
  cloud:
    gateway:
      routes:
        - id: user-canary
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
            - Header=X-Canary, true   # 带该 Header 的流量走金丝雀
          filters:
            - StripPrefix=1

6. Sentinel 限流熔断

6.1 限流算法

  • 计数器/滑动窗口:固定窗口有临界突刺问题,滑动窗口更平滑。Sentinel 默认用滑动窗口统计 QPS。
  • 漏桶:以恒定速率出水,超量直接丢弃,平滑但无突发。
  • 令牌桶:匀速放令牌,桶里有余量就能突发,兼顾限流与突发——Gateway 的 RequestRateLimiter 用的就是令牌桶。

6.2 熔断降级

Sentinel 支持三种熔断策略:

策略 触发条件 适用
慢调用比例 RT 超过阈值的比例达标即熔断 依赖变慢
异常比例 异常数/总数 超过阈值 错误率升高
异常数 单位时间异常数超阈值 偶发抖动
// 资源定义 + 降级逻辑
@SentinelResource(value = "getProduct", blockHandler = "blocked", fallback = "fallback")
public ProductVO getProduct(Long id) {
    return productService.getProduct(id);
}

// 被 Sentinel 规则限流/熔断时
public ProductVO blocked(Long id, BlockException ex) {
    throw new BizException("当前访问过于频繁,请稍后再试");
}

// 业务异常时的兜底
public ProductVO fallback(Long id, Throwable t) {
    return ProductVO.defaultProduct();      // 返回默认数据/降级数据
}

规则可通过 Nacos 持久化:QPS 限流、慢调用比例熔断(RT>200ms 比例超阈值即熔断 N 秒)、热点参数限流。

blockHandler vs fallback 区别:blockHandler 只在被 Sentinel 流控/熔断/系统保护拦截时触发;fallback 是业务方法本身抛异常(如 NPE、DB 挂)时的兜底。两者职责不同,生产往往一起配。

6.3 系统保护(自适应)

Sentinel 还能按整体入口 QPS、线程数、Load、CPU做系统级保护(SystemRule),当本机 Load 过高时自动拒绝部分请求,防止被自己压垮——这是 Hystrix 没有的能力。

7. 分布式事务(Seata AT 模式)

7.1 为什么难

单体靠本地事务 ACID 一把梭;微服务里「下订单 + 扣库存 + 扣余额」跨三个库,本地事务管不到别人家,网络还可能超时,无法保证全部成功或全部回滚——这就是分布式事务问题。CAP 已告诉我们只能要最终一致,所以方案都是「补偿」而非「强一致原子提交」。

7.2 四种主流方案对比

方案 一致性 侵入性 性能 适用场景
2PC(两阶段提交) 强一致 中 差(全程锁资源) 传统银行核心、XA 接口,强一致但慢
TCC 最终一致 高(手写三接口) 好 高性能核心链路,Try/Confirm/Cancel 手工编排
SAGA 最终一致 中(写补偿) 好 长事务、跨多服务编排,正向+补偿
本地消息表 / 消息最终一致 最终一致 低 好 可异步的动作(发短信、积分),首推

2PC(Two-Phase Commit)

  • 阶段一(Prepare):协调者问所有参与者「能不能提交」,参与者锁资源、写 undo/redo 并返回 Yes/No。
  • 阶段二(Commit/Rollback):全 Yes 才发 Commit,否则发 Rollback。
  • 缺点:Prepare 到 Commit 之间全程持锁,协调者单点,任一参与者阻塞会拖垮整体。实际用得少。

TCC(Try-Confirm-Cancel)

  • Try:预留资源(如冻结库存而非直接扣)。
  • Confirm:真正提交,幂等。
  • Cancel:释放预留,幂等。
  • 优点:不长期持锁、性能好;缺点:每个业务要手写三个接口,开发重。

SAGA

把一个长事务拆成一系列本地事务,每个都有对应的补偿事务;某步失败就反向执行前面各步的补偿。适合跨多个服务的长流程(如旅行订票:订机票→订酒店→租车,失败就依次退订)。

本地消息表(最常用、最推荐)

  1. 本地事务里「写业务数据 + 写消息表(状态=待发送)」同一库事务,保证原子;
  2. 独立线程/定时任务扫消息表,发到 MQ;
  3. 消费方处理成功后回执;失败重试(消费方需幂等);
  4. 消息表状态更新为「已发送/已完成」。

核心思想:用本地事务保证「业务 + 消息」一致,再用 MQ 异步送达,靠消费幂等兜底最终一致。比 Seata 轻,不侵入业务框架,是中小团队首选。

7.3 Seata AT 模式(代码层)

AT 是「无侵入的最终一致」:对业务代码零改动,框架自动在前后生成 SQL 快照写入 undo_log,异常时反向补偿。

// 全局事务注解在发起方;各参与方只需准备 undo_log 表
@GlobalTransactional(rollbackFor = Exception.class)
public void createOrder(CreateOrderReq req) {
    orderService.create(req);        // 分支事务 1
    stockClient.deduct(...);         // 分支事务 2(Feign 调用)
    accountClient.debit(...);        // 分支事务 3
    // 任一分支异常:TC 通知所有分支用 undo log 反向补偿
}
模式 一致性 适用
AT 最终一致 大多数 CRUD 场景,零侵入
TCC 最终一致 高性能核心链路(Try/Confirm/Cancel 手写)
消息最终一致 最终一致 可异步的跨服务动作,优先考虑

能用"业务流程重新设计 + 消息最终一致"解决的,就不用强一致的分布式事务。

8. 可观测性三件套

分布式系统「看不见就管不了」,三大支柱:

  1. 日志:Logback JSON 输出 + TraceId MDC → Filebeat → Kafka → ELK;
  2. 指标:Micrometer + Actuator → Prometheus 抓取 → Grafana 面板(QPS/RT/错误率/JVM);
  3. 链路:SkyWalking Agent 无侵入接入(-javaagent 启动参数),全链路 TraceId、慢接口定位。

SLO 参考面板:接口 P99 延迟、错误率(4xx/5xx 分开)、服务间依赖拓扑、GC/线程池水位。

8.1 链路追踪原理(深度)

一次请求跨 5 个服务,怎么把日志串起来?靠 TraceId + SpanId:

  • TraceId:一次调用链全局唯一 ID,贯穿所有服务。
  • SpanId:调用链中每一段(一个 RPC / 一个本地方法)的唯一 ID,并有 parentSpanId 指向父 Span。

传播方式:

  1. 入口(网关)生成 TraceId,放入 MDC(日志上下文)和 HTTP Header(traceparent / X-Trace-Id);
  2. 服务间调用(OpenFeign / RestTemplate)通过 ClientHttpRequestInterceptor 把 TraceId 透传到下游 Header;
  3. 下游从 Header 取出 TraceId 继续用,并生成自己的 SpanId,记录起止时间;
  4. 各服务把带 TraceId 的 Span 上报给 SkyWalking OAP / Jaeger Collector;
  5. 后端按 TraceId 聚合出瀑布图,看到每个环节的耗时与依赖关系。

无侵入接入关键:SkyWalking 用 Java Agent 字节码增强,自动拦截 Servlet、JDBC、Feign、Redis 等,业务代码零改动。TraceId 要打通日志(MDC),否则「监控里看到慢调用,却去日志里捞不到对应记录」。

9. 服务容错与雪崩防护总览

把前面 Sentinel、Hystrix、限流降级串起来看,微服务防雪崩的三板斧:

  • 限流(Limitation):入口/接口级限制并发,令牌桶/滑动窗口,超了直接拒。
  • 熔断(Circuit Breaker):依赖方故障率超阈值就「开路」,快速失败,给下游喘息,半开探测恢复。
  • 降级(Degrade):资源紧张时关掉非核心功能(返回兜底/默认值),保核心链路。
  • 隔离(Bulkhead):线程池隔离 / 信号量隔离,避免一个慢依赖占满所有线程把整个服务拖死。

关系:限流挡在最前,熔断保护调用方,降级保住核心,隔离防扩散。四者配合才能扛住流量洪峰与依赖故障。

10. 落地建议与踩坑小结

  1. 注册中心选 AP 还是 CP:互联网业务要「永远能注册能发现」→ Nacos(AP)/ Eureka;金融强一致选 Zookeeper(CP),但接受选举期间不可用。
  2. 配置中心别和注册中心混用坏:Nacos 一个组件搞定两者很省事,但生产要给它配高可用集群(至少 3 节点)+ 持久化 MySQL。
  3. Feign 超时 < 网关超时 < 上游超时:层层超时要对齐,否则上层已超时下层还在跑,造成资源浪费与重复执行。建议:Feign readTimeout 3000ms < Gateway 全局超时 5000ms。
  4. 分布式事务能避则避:优先「同库 + 本地事务」「消息最终一致」,实在要跨库强一致再上 Seata,绝不滥用 @GlobalTransactional(有全局锁开销)。
  5. 链路追踪尽早接:等线上排查慢调用再接就晚了,项目初期就把 SkyWalking Agent 挂上。

小结

微服务不是银弹,它用「运维复杂度」换「扩展性与故障隔离」。理解 CAP/BASE 后,Nacos 的 AP、Eureka 的 AP、Zookeeper 的 CP 之争就不再是背结论;理解注册发现的心跳/推送、网关的过滤器链、分布式事务的四种补偿方案、链路的 TraceId 传播,才能在选型与排障时心里有底。记住:先单体后拆分、强一致能避就避、可观测性从第一天就做。

posted @ 2026-09-19 05:41  Stellan7  阅读(25)  评论(0)    收藏  举报