Skip to content

分布式系统基础

分布式系统不是“把项目拆成多个服务”这么简单。真正的难点是:原来一次进程内方法调用,现在变成跨网络调用;原来一个数据库事务,现在变成多个服务各自提交;原来一个日志文件,现在分散到多台机器。

零基础可以先记住一句话:

分布式系统的核心问题不是框架,而是网络不可靠、状态分散、失败不可避免。

本页是基础导读。要深入理解微服务为什么拆、怎样确定数据边界,以及一次远程调用在框架内部怎样执行,请按顺序继续:

  1. 单体到微服务:架构演进、服务拆分与数据边界
  2. 一次完整微服务调用链:从客户端代理到事务响应
  3. 微服务架构、拆分与调用链面试题

单机调用和远程调用的区别

单机方法调用:

java
inventoryService.deduct(productId, count);

如果方法抛异常,调用方基本能确认失败;如果正常返回,基本能确认成功。

远程调用:

java
POST http://inventory-service/api/inventory/deduct

可能出现:

  1. 请求没发出去。
  2. 请求发到了,下游没处理。
  3. 下游处理成功,但响应丢了。
  4. 下游处理很慢,调用方超时。
  5. 调用方重试,导致下游收到两次。
mermaid
flowchart TD
    A["订单服务发送扣减库存请求"] --> B["库存服务执行本地事务"]
    B --> C["库存扣减已经提交"]
    C --> D["响应在网络中丢失或超过调用方超时"]
    D --> E["订单服务只知道没有按时收到结果"]
    E --> F["使用业务流水查询事实并保证重试幂等"]

这就是为什么分布式系统一定要有查询、重试和幂等。

分布式系统的失败类型

失败类型例子如果不处理会怎样
网络超时调库存服务 3 秒无响应调用线程堆积,订单接口变慢
部分失败订单成功,库存失败数据不一致
重复请求支付回调来了两次重复改状态、重复发货
乱序消息先收到取消,再收到创建状态倒退
下游雪崩库存服务慢,订单服务线程被拖死整个系统不可用
节点故障某个实例宕机请求失败或任务中断

必须设置超时

远程调用不能无限等待。

mermaid
flowchart TD
    A["订单服务请求库存服务"] --> B{"是否设置超时"}
    B -- "没有" --> C["线程一直等待"]
    C --> D["线程池耗尽"]
    D --> E["订单服务也不可用"]
    B -- "有" --> F["超时后快速失败"]
    F --> G["重试、降级或进入补偿"]

Java 11+/17+ 标准 HTTP Client 示例;JDK 8 项目请使用调用链专题中的 HttpURLConnection Demo,或沿用项目已有成熟客户端:

java
HttpRequest request = HttpRequest.newBuilder()
        .uri(URI.create("http://inventory-service/api/deduct"))
        .timeout(Duration.ofSeconds(3))
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build();

超时时间不是越长越好。下游已经慢了,上游还长时间等待,只会让线程堆积,把故障扩大。

重试必须配合幂等

重试可以解决临时失败,也可能制造重复写入。

错误思路:

text
调用支付服务超时 -> 立刻重试 -> 支付服务收到两次扣款请求

正确思路:

  1. 每次业务操作都有幂等号。
  2. 下游按幂等号去重。
  3. 重试只会得到同一个业务结果。

支付幂等表:

sql
create table t_payment_request (
    id bigint primary key,
    idempotent_key varchar(128) not null,
    order_no varchar(64) not null,
    status varchar(32) not null,
    created_at datetime not null,
    unique key uk_idempotent_key (idempotent_key)
);

支付接口伪代码:

java
@Transactional
public PaymentResult createPayment(CreatePaymentCommand command) {
    PaymentRequest existed = paymentRequestRepository.findByKey(command.idempotentKey());
    if (existed != null) {
        return PaymentResult.from(existed);
    }

    PaymentRequest request = PaymentRequest.create(command.idempotentKey(), command.orderNo());
    paymentRequestRepository.save(request);
    return PaymentResult.from(request);
}

状态机比布尔字段更可靠

分布式业务不要只用一个 true/false 表示状态。订单、支付、退款、发券都应该设计状态机。

订单状态示例:

mermaid
flowchart TD
    A["WAIT_PAY 待支付"] --> B["PAID 已支付"]
    A --> C["CLOSED 已关闭"]
    B --> D["SHIPPED 已发货"]
    D --> E["FINISHED 已完成"]
    B --> F["REFUNDING 退款中"]
    F --> G["REFUNDED 已退款"]

状态机的好处:

  1. 明确哪些状态可以流转。
  2. 防止重复消息导致状态倒退。
  3. 便于排查业务卡在哪一步。
  4. 更新时可以带状态条件,天然支持幂等。

状态更新 SQL:

sql
update t_order
set status = 'PAID',
    paid_at = now()
where order_no = ?
  and status = 'WAIT_PAY';

如果更新行数是 0,说明订单可能已经支付、关闭或不存在,不能盲目继续处理。

服务调用链路

mermaid
flowchart TD
    A["用户请求"] --> B["网关"]
    B --> C["订单服务"]
    C --> D["库存服务"]
    C --> E["优惠券服务"]
    C --> F["支付服务"]
    C --> G["发送订单事件到 MQ"]
    G --> H["积分服务"]
    G --> I["通知服务"]

这条链路越长,失败概率越高。所以要尽量缩短同步链路,把非核心动作异步化。

下单主链路通常只保留必须立即完成的动作:

  1. 校验商品和价格。
  2. 创建订单。
  3. 锁定库存。
  4. 创建支付单或返回支付参数。

积分、短信、推荐、报表、ES 同步通常可以通过 MQ 异步处理。

同步调用和异步消息怎么选

对比同步调用异步消息
调用方是否等待结果等待不等待或只等待投递成功
适合下单必须校验库存发短信、加积分、同步 ES
优点结果直接,流程清晰解耦、削峰、隔离故障
风险下游慢会拖慢上游有延迟,需要幂等和补偿

选择原则:

  1. 用户当前操作必须知道结果的,用同步。
  2. 不影响当前响应的副作用,用异步。
  3. 异步不是不管结果,必须有重试、死信、补偿和告警。

可观测性

分布式系统必须能回答四个问题:

  1. 请求经过了哪些服务。
  2. 哪个服务慢。
  3. 哪个服务失败。
  4. 影响了哪些业务数据。

最基础做法是全链路传递 traceId

java
String traceId = request.getHeader("X-Trace-Id");
if (traceId == null || traceId.isBlank()) {
    traceId = UUID.randomUUID().toString();
}
MDC.put("traceId", traceId);

日志示例:

text
[traceId=7f3a] create order start, orderNo=O1001
[traceId=7f3a] deduct inventory success, productId=9001
[traceId=7f3a] send order created event success, topic=order.created

没有 traceId,线上排查只能靠时间猜日志,效率很低。

常见设计原则

原则解释
超时所有远程调用必须有超时
限流防止突发流量压垮服务
熔断下游持续失败时快速失败
降级非核心能力失败时不影响主流程
幂等重复请求结果一致
补偿失败后可以重新修复
对账定期发现数据不一致
可观测日志、指标、链路追踪齐全

本章小结

分布式系统不是靠某个框架变可靠的,而是靠一组工程习惯:超时、重试、幂等、状态机、异步解耦、补偿、对账、可观测。

这些基础学明白后,再学 CAP、BASE、分布式事务、Seata、TCC、Saga,才不会只停留在概念表面。

继续学习:架构演进与服务拆分 · 一次完整调用链 · 独立面试题