微服务核心问题解决方案:服务保护与分布式事务

在微服务远程调用的世界里,就像一条串联着多个站点的列车线路 —— 只要其中一个站点出问题,整条线路都可能瘫痪。比如你在购物 APP 里查购物车,背后其实是购物车服务在 “拜托” 商品服务拿最新商品信息。可要是商品服务突然 “罢工”,原本只是 “查商品” 的小问题,很可能让 “查购物车” 也跟着报错;更糟的是,要是商品服务因为人太多 “堵死了”,还会像多米诺骨牌一样,把购物车服务、甚至更多关联服务都拖垮。除此之外,下单时 “扣库存、存订单、清购物车” 这一系列操作,怎么保证要么全成功、要么全失败,也是个让人头疼的难题。不过别担心,今天我们就带着这些问题,一步步找到解决办法!

1. 微服务保护

要让微服务像 “打不死的小强” 一样稳健,避免级联失败引发的 “雪崩灾难”,这就是微服务保护的核心目标。接下来我们会拆解常见的保护方案,再用实际工具落地这些方案。

1.1 服务保护方案

微服务保护的思路,其实和我们应对生活中的 “风险” 很像 —— 比如节假日景区限流、家里装防火门、电路装保险丝,都是通过 “牺牲一点便利,换整体安全”。这些方案本质上都属于 “服务降级”,虽然可能让部分功能体验打折扣,但能保住整个服务不崩溃。

1.1.1 请求限流

你有没有过这种经历:热门奶茶店突然排长队,店员会说 “今天限购 100 杯”?这就是现实中的 “限流”。微服务里的请求限流也是一个道理 —— 服务故障的头号元凶,往往是 “突发的高并发”,比如某商品突然爆火,一瞬间几万用户查购物车,直接把服务器 “冲垮”。

请求限流就像给服务装了一个 “智能闸门”:不管上游冲过来多少请求,闸门只会按照我们设定的 “速度” 放行。比如设定每秒最多处理 6 个查购物车请求,哪怕来了 10 个,也只会让 6 个通过,剩下的 4 个先 “排队” 或 “礼貌拒绝”。这就像水电站的大坝,既能拦住突发的洪水,又能让下游水流始终平稳,避免下游被冲毁。

1.1.2 线程隔离

假设你家有两个卧室,一个卧室漏水了,如果没有墙壁隔开,水会流到另一个卧室;但有了墙壁,漏水只会局限在一个房间里。线程隔离的思路,就来自轮船的 “舱壁模式”—— 轮船的船舱被隔板分成一个个独立空间,就算某部分进水,也不会让整艘船沉没。

在微服务里,这个 “隔板” 就是 “线程资源限制”。比如查购物车时需要调用商品服务,我们给 “查商品” 这个操作单独分配 20 个线程 —— 就算商品服务卡得要死,最多也只会占用这 20 个线程,不会把购物车服务的所有线程都耗光。这样一来,你用 “加购物车”“改商品数量” 这些功能时,依然能快速响应,不会被 “查购物车” 的故障拖累。

1.1.3 服务熔断

线程隔离能阻止故障扩散,但如果商品服务一直 “慢吞吞” 或 “报错”,每次调用它还是会浪费时间、消耗资源 —— 就像你明知某条路堵车,还非要硬闯,结果只是白白浪费时间。这时候就需要 “服务熔断”,像电路里的保险丝一样:一旦电流过大,立刻断开,避免电器烧毁。

服务熔断要做两件关键的事:

  1. 准备 “备用方案”(降级逻辑):比如查商品失败时,不直接报错,而是返回 “商品信息暂时无法获取”,或者展示购物车里的历史商品信息 —— 至少让用户能看到购物车,而不是一个空白页面。
  2. 智能 “断连” 与 “恢复”:统计商品服务的故障情况(比如慢请求太多、报错太多),一旦超过阈值,就暂时 “断开” 对它的调用,所有请求直接走备用方案;等过一会儿,再试探性地发一个请求,如果成功了,就恢复正常调用;如果还是失败,就继续 “断连”。

1.2 Sentinel

讲完了方案,我们来认识一个 “实战工具”——Sentinel。它就像微服务的 “保安队长”,既能帮我们实现限流、隔离、熔断,还能实时监控服务状态。Sentinel 分为两部分:

  • 核心库(Jar 包:埋在我们的微服务项目里,负责执行限流、熔断逻辑;
  • 控制台(Dashboard):一个可视化界面,能看到服务的实时状态,还能手动配置保护规则。

1.2.1 介绍和安装

  1. 下载 “保安队长的控制台”
    去 Sentinel 的 GitHub Releases 页面(https://github.com/alibaba/Sentinel/releases)下载最新的 Jar 包,或者用课前提供的版本,把它重命名为 sentinel-dashboard.jar(方便记忆),放在一个没有中文、没有特殊符号的文件夹里。

  2. 启动控制台
    打开命令行,进入 Jar 包所在的文件夹,输入下面的命令:

    1
    java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar

    这条命令的意思是:让控制台跑在 8090 端口,并且把自己也纳入监控范围,名字叫 “sentinel-dashboard”。

  3. 登录控制台
    打开浏览器,访问 http://localhost:8090,输入默认账号和密码(都是 sentinel),就能看到控制台界面了 —— 默认会显示控制台自己的监控数据,就像保安队长先给自己做了个体检。

1.2.2 微服务整合

接下来,我们让购物车服务(cart-service)“认” 这个保安队长,步骤很简单:

  1. 引入 Sentinel 依赖
    在 cart-service 的 pom.xml 里加一段依赖,相当于给购物车服务装了 “保安队员”:

    1
    2
    3
    4
    5
    <!--sentinel 核心库(保安队员)-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
  2. 告诉 “队员” 控制台在哪
    修改 cart-service 的 application.yaml,添加控制台地址:

    1
    2
    3
    4
    5
    spring:
    cloud:
    sentinel:
    transport:
    dashboard: localhost:8090 # 保安队长的地址
  3. 触发监控
    重启 cart-service,然后随便访问一个购物车接口(比如查购物车)—— 这时候 “队员” 会主动把购物车服务的信息上报给控制台。刷新控制台的 “簇点链路” 页面,就能看到购物车服务的接口列表了。

这里有个小细节:我们的接口是 Restful 风格,比如 /carts 既能查购物车(GET),又能删购物车(DELETE)。默认情况下,Sentinel 会把 “/carts” 当成一个资源,没法区分请求方式。解决办法很简单,在 application.yaml 里加一行配置,让 Sentinel 把 “请求方式 + 路径” 当成资源名(比如 GET:/cartsDELETE:/carts):

1
2
3
4
5
6
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090
http-method-specify: true # 开启请求方式前缀,区分GET/DELETE等

重启后再访问接口,控制台的 “簇点链路” 就会显示不同请求方式的接口了,再也不会 “张冠李戴”。

1.3 请求限流

现在我们给 “查购物车”(GET:/carts)接口装个 “限流闸门”,比如每秒最多允许 6 个请求通过。

  1. 配置限流规则
    在控制台的 “簇点链路” 页面,找到 GET:/carts,点击后面的 “流控” 按钮,弹出配置框:
    • 阈值类型选 “QPS”(每秒请求数);
    • 阈值填 “6”;
      其他默认,点击 “确定”。
  2. 测试限流效果
    用 Jemeter 模拟 “每秒 10 个请求” 的场景(就像 10 个人同时查购物车)。跑一会儿后看控制台监控:
    • 通过的请求每秒稳定在 6 个左右;
    • 被拒绝的请求每秒大概 4 个;
      完全符合我们设定的规则,就像闸门精准地控制着水流速度,不会让服务器 “超负荷工作”。

1.4 线程隔离

限流能防 “突发流量”,但如果商品服务本身出了问题(比如响应很慢),查购物车时调用商品服务,还是会占用购物车服务的线程。这时候就需要线程隔离,给 “调用商品服务” 这个操作划一个 “专属线程池”。

1.4.1 OpenFeign 整合 Sentinel

我们调用商品服务用的是 Feign(就像购物车服务给商品服务 “打电话”),所以要先让 Feign 支持 Sentinel,步骤如下:

  1. 开启 Feign 的 Sentinel 支持
    修改 cart-service 的 application.yaml,加一行配置:

    1
    2
    3
    feign:
    sentinel:
    enabled: true # 让Feign调用支持Sentinel保护
  2. 调整 Tomcat 线程数(方便测试)
    默认情况下,Tomcat 的最大线程数是 200,单机测试很难 “打满” 线程。我们把它调小一点,让隔离效果更明显:

    1
    2
    3
    4
    5
    6
    7
    server:
    port: 8082
    tomcat:
    threads:
    max: 50 # 最多50个工作线程
    accept-count: 50 # 最多50个排队请求
    max-connections: 100 # 最多100个连接
  3. 查看 Feign 资源
    重启 cart-service,访问一次查购物车接口(触发 Feign 调用商品服务)。再看控制台的 “簇点链路”,会发现多了一个 Feign 相关的资源(比如 GET:http://item-service/items)—— 这就是调用商品服务的接口,我们要给它做线程隔离。

1.4.2 配置线程隔离

  1. 设置隔离规则
    在 “簇点链路” 里找到 Feign 资源(比如 GET:http://item-service/items),点击 “流控” 按钮:
    • 阈值类型选 “并发线程数”;
    • 阈值填 “5”(最多用 5 个线程调用商品服务);
      点击 “确定”。
  2. 测试隔离效果
    用 Jemeter 模拟 “每秒 100 个查购物车请求”—— 这时候:
    • 调用商品服务的请求,每秒最多通过 10 个左右(因为 5 个线程,每个线程每秒处理 2 个请求);
    • 但购物车服务的其他接口(比如 POST:/carts 加购物车),响应依然很快,完全不受影响。
      这就像给 “查商品” 这个操作装了一道 “防火墙”,就算它出问题,也不会占用其他操作的资源。

1.5 服务熔断

线程隔离解决了 “故障扩散”,但如果商品服务一直 “慢吞吞”(比如每次响应要 500 毫秒),就算只占 5 个线程,也会让查购物车的响应时间变长,用户体验还是不好。这时候就需要 “熔断”—— 既然这条路走不通,不如暂时不走,直接用备用方案。

1.5.1 编写降级逻辑

降级逻辑就是 “调用失败时的备用方案”。比如查商品失败时,不报错,而是返回空列表(让购物车只显示商品 ID,至少用户能看到自己加了什么);而扣库存失败时,要报错(因为下单必须扣库存成功)。

给 Feign 写降级逻辑有两种方式,我们选更灵活的 “FallbackFactory”(能处理异常信息):

  1. 定义降级处理类
    在 hm-api 模块里,新建 ItemClientFallback 类,实现 FallbackFactory<ItemClient>

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    package com.hmall.api.client.fallback;

    import com.hmall.api.client.ItemClient;
    import com.hmall.api.dto.ItemDTO;
    import com.hmall.api.dto.OrderDetailDTO;
    import com.hmall.common.exception.BizIllegalException;
    import com.hmall.common.utils.CollUtils;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.cloud.openfeign.FallbackFactory;

    import java.util.Collection;
    import java.util.List;

    @Slf4j
    public class ItemClientFallback implements FallbackFactory<ItemClient> {
    @Override
    public ItemClient create(Throwable cause) {
    return new ItemClient() {
    @Override
    public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
    log.error("远程调用ItemClient#queryItemByIds方法出现异常,参数:{}", ids, cause);
    // 查购物车允许失败,返回空列表(用户至少能看到购物车结构)
    return CollUtils.emptyList();
    }

    @Override
    public void deductStock(List<OrderDetailDTO> items) {
    // 扣库存失败必须报错,触发事务回滚
    throw new BizIllegalException(cause);
    }
    };
    }
    }
  2. 注册降级 Bean
    在 hm-api 的 DefaultFeignConfig 类里,把 ItemClientFallback 注册成 Spring Bean:

    1
    2
    3
    4
    @Bean
    public ItemClientFallback itemClientFallback() {
    return new ItemClientFallback();
    }
  3. 绑定 Feign 接口
    ItemClient 接口上,用 @FeignClientfallbackFactory 属性绑定降级类:

    1
    2
    3
    4
    @FeignClient(value = "item-service", fallbackFactory = ItemClientFallback.class)
    public interface ItemClient {
    // 接口方法...
    }
  4. 测试降级效果
    重启服务后,用 Jemeter 压测查购物车接口 —— 被限流的请求不会报错,而是返回空的商品列表,购物车依然能正常显示,用户体验好了很多。

1.5.2 服务熔断

现在我们配置 “慢调用熔断”:如果调用商品服务的请求,超过 200 毫秒还没返回,就算 “慢调用”;最近 1 秒内至少有 5 次请求,且慢调用比例超过 50%,就触发熔断,20 秒内不再调用商品服务,直接走降级逻辑。

  1. 配置熔断规则
    在控制台 “簇点链路” 里,找到 Feign 资源(GET:http://item-service/items),点击 “熔断” 按钮:
    • 熔断策略选 “慢调用比例”;
    • 最大 RT(慢调用阈值)填 “200”(超过 200 毫秒算慢);
    • 比例阈值填 “0.5”(50% 以上慢调用);
    • 最小请求数填 “5”(至少 5 次请求才统计);
    • 熔断时长填 “20”(熔断后 20 秒不调用);
      点击 “确定”。
  2. 测试熔断效果
    用 Jemeter 压测查购物车接口:
    • 刚开始,请求会正常调用商品服务,但因为商品服务慢,很快就达到 “50% 慢调用比例”;
    • 触发熔断后,调用商品服务的请求直接被拦截,通过 QPS 降到 0,所有请求走降级逻辑;
    • 此时查购物车的平均响应时间很短(不用等慢调用了),购物车服务整体依然稳健。

等 20 秒后,熔断进入 “半开状态”,会放行一个请求试探:如果商品服务恢复了,就回到 “关闭状态”,正常调用;如果还是慢,就继续 “打开状态”,避免浪费资源。