分布式系统中的常见错误类型
在微服务架构普及的今天,一个用户请求往往会经过多个服务协同完成。比如你下单买咖啡,订单服务要调用库存、支付、物流三个服务。任何一个环节出问题,整个流程就卡住了。网络抖动、服务宕机、数据库锁表,这些都不是稀罕事。
和单体应用不同,分布式环境里没有“绝对可靠”的链路。你不能指望每次调用都成功,必须默认失败是常态。这时候,错误处理不是锦上添花,而是系统能跑起来的基本前提。
超时控制:别让请求无限等下去
最常见的问题是某个服务响应太慢,导致调用方线程被占满。比如支付服务卡住,订单服务几十个线程都在等,很快整个服务就不可用了。
给每个远程调用设置超时时间是最基本的操作。例如使用 HTTP 客户端时:
HttpClient.newBuilder()
.connectTimeout(1, TimeUnit.SECONDS)
.readTimeout(2, TimeUnit.SECONDS)
.build();
连接超过 1 秒或读取超过 2 秒就直接放弃。虽然会损失一次请求,但能保住整体可用性。就像地铁闸机不会为一个人一直开着,超时就是系统的“自动关门”机制。
重试机制:不是所有失败都得立刻报错
网络抖动可能只持续几百毫秒,重试几次说不定就通了。但盲目重试反而会雪崩。比如服务已经彻底挂了,你还不断发请求,等于火上浇油。
合理的做法是结合指数退避。第一次失败等 100ms 重试,第二次等 200ms,第三次 400ms,逐步拉开间隔。Java 中可以用 Resilience4j 实现:
RetryConfig config = RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(100))
.intervalFunction(IntervalFunction.ofExponentialBackoff())
.build();
同时要区分错误类型。数据库唯一键冲突这种业务错误,重试一万次也没用,只有网络超时、服务不可达才适合重试。
熔断器:及时止损,防止连锁崩溃
当某个下游服务连续失败达到阈值,熔断器会直接拒绝后续请求,避免资源耗尽。这就像家里的保险丝,电流过大就自动断开。
Hystrix 是经典实现之一。配置如下:
@HystrixCommand(fallbackMethod = "getFallbackUser")
public User getUser(Long id) {
return userService.findById(id);
}
public User getFallbackUser(Long id) {
return new User("默认用户");
}
当失败率超过 50%,接下来的请求会直接走 fallback 方法,不再发起远程调用。过一段时间再尝试半开状态,试探服务是否恢复。
日志与追踪:出了问题要知道哪里坏了
没有清晰的日志,排查问题就像在黑屋子里找开关。每个服务都要记录关键步骤,并带上统一的请求 ID(traceId),方便跨服务串联。
比如用 MDC 记录上下文:
MDC.put("traceId", UUID.randomUUID().toString());
log.info("开始创建订单");
结合 Zipkin 或 SkyWalking 这类工具,能画出完整的调用链,一眼看出是哪个环节卡住了。
异步补偿:有些事可以回头再做
不是所有操作都必须实时完成。比如发通知失败,可以先记下来,后台任务定期扫描重发。电商系统里的对账、积分发放,很多都是靠异步补偿兜底。
消息队列在这里很管用。下单成功后发一条消息到 Kafka,通知服务消费失败也不怕,消息还在,能反复投递。
关键是设计好幂等性。同一条消息处理十次,结果也得一样。比如加积分前先查是否已到账,避免重复赠送。