Java 架构里最容易忽视的“稳定性工程”:限流、熔断、降级与一致性
Java 架构里最容易忽视的“稳定性工程”:限流、熔断、降级与一致性
很多 Java 项目在早期都能跑得很顺:并发不高、链路短、依赖少。等业务增长到一定规模,问题会突然集中爆发:接口偶发超时、某个第三方抖一下全站连锁雪崩、MQ 积压后回调重复执行、数据库压力一上来所有服务一起慢。此时你会发现,真正决定系统上限的,不是你写了多少功能,而是你有没有把“稳定性”当作架构的一部分。
这篇文章从 Java 架构视角,讲一套可落地的稳定性设计:限流、熔断、降级、隔离、以及最终一致性的处理方式。依旧保持工程实战,不堆概念。
1)先承认现实:故障不是“会不会发生”,而是“何时发生”
稳定性设计的第一原则是: 把故障当作常态,把恢复当作能力。
常见故障来源:
- 第三方接口抖动(超时、5xx、限频)
- 网络抖动(瞬时延迟飙升)
- 数据库慢查询或锁竞争
- 缓存击穿、雪崩
- MQ 积压导致重复消费、延迟消费
- 单点资源耗尽(线程池、连接池、CPU、内存)
如果你的架构里只有“重试”,没有“熔断/隔离/降级”,那重试只会把故障放大。
2)限流:先保护自己,再谈服务别人
限流不是“怕用户来”,而是“保证系统能活”。尤其在电商、支付、活动类系统中,限流是最后一道保险。
2.1 令牌桶限流(本地/单机)
public class TokenBucketLimiter {
private final long capacity;
private final long refillPerSecond;
private long tokens;
private long lastRefillTime;
public TokenBucketLimiter(long capacity, long refillPerSecond) {
this.capacity = capacity;
this.refillPerSecond = refillPerSecond;
this.tokens = capacity;
this.lastRefillTime = System.nanoTime();
}
public synchronized boolean tryAcquire() {
refill();
if (tokens > 0) {
tokens--;
return true;
}
return false;
}
private void refill() {
long now = System.nanoTime();
long elapsedNs = now - lastRefillTime;
long add = (elapsedNs / 1_000_000_000L) * refillPerSecond;
if (add > 0) {
tokens = Math.min(capacity, tokens + add);
lastRefillTime = now;
}
}
}
用法:在 Controller 或网关层对关键接口(例如下单、支付、领券)进行保护。 注意:单机限流适合快速兜底,但多实例需要分布式限流。
3)熔断:别让慢依赖拖死你
外部依赖抖动时,如果你继续请求,只会把线程池和连接池耗光,最终形成“全站雪崩”。熔断的意义是:当依赖处于异常状态时,主动停止调用,快速失败,让系统自我保护并等待恢复窗口。
3.1 一个简化版熔断器(可理解原理)
public class SimpleCircuitBreaker {
enum State { CLOSED, OPEN, HALF_OPEN }
private State state = State.CLOSED;
private int failCount = 0;
private final int threshold;
private long openUntilMs = 0;
private final long openDurationMs;
public SimpleCircuitBreaker(int threshold, long openDurationMs) {
this.threshold = threshold;
this.openDurationMs = openDurationMs;
}
public synchronized <T> T call(Supplier<T> supplier, Supplier<T> fallback) {
long now = System.currentTimeMillis();
if (state == State.OPEN) {
if (now < openUntilMs) return fallback.get();
state = State.HALF_OPEN;
}
try {
T res = supplier.get();
onSuccess();
return res;
} catch (Exception e) {
onFail(now);
return fallback.get();
}
}
private void onSuccess() {
failCount = 0;
state = State.CLOSED;
}
private void onFail(long now) {
failCount++;
if (failCount >= threshold) {
state = State.OPEN;
openUntilMs = now + openDurationMs;
}
}
}
实际生产中你会用成熟组件(如 Resilience4j),但理解原理很重要: 熔断不是“让你失败”,而是让你失败得快、失败得可控。
4)降级:不是“不提供服务”,而是“提供可接受的服务”
降级的核心思想:当资源不足或依赖异常时,系统仍然返回一个“可接受的结果”,而不是拖死整个链路。
常见降级策略:
- 返回缓存的旧数据(例如首页推荐)
- 返回简化的数据(少字段、少模块)
- 返回“稍后处理”的状态(异步化)
- 关闭非关键功能(例如统计、埋点、非必要第三方)
4.1 典型降级:读缓存失败 → 直接返回默认结构
public class RecommendationService {
private final CacheClient cache;
private final SimpleCircuitBreaker breaker;
public RecommendationService(CacheClient cache) {
this.cache = cache;
this.breaker = new SimpleCircuitBreaker(3, 10_000);
}
public List<String> getHomeRecs(Long userId) {
return breaker.call(
() -> cache.get("recs:" + userId),
() -> List.of("Hot Books", "New Arrivals", "Editor Picks")
);
}
}
注意:降级不是“硬编码返回”,而是让系统在资源紧张时保住核心路径。
5)隔离:把故障关在房间里
很多系统不是被“错误”打死的,而是被“资源耗尽”打死的:线程池用光,所有请求都堵住。隔离的意义是:不同业务场景用不同资源池,避免互相拖累。
5.1 线程池隔离(示例:支付查询与普通查询分开)
public class ExecutorsConfig {
public static ExecutorService paymentExecutor() {
return new ThreadPoolExecutor(
20, 40,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
new ThreadPoolExecutor.AbortPolicy()
);
}
public static ExecutorService queryExecutor() {
return new ThreadPoolExecutor(
50, 100,
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadPoolExecutor.CallerRunsPolicy()
);
}
}
- 支付相关:队列小、策略更严格,防止堆积
- 普通查询:容忍排队,尽量不拒绝
隔离做得好,局部故障不会扩散。
6)一致性:用“最终一致”取代“强行同步一致”
在分布式系统里,强行同步一致通常意味着:链路长、超时多、失败率高。更稳定的方式是:核心写库成功即可,后续操作通过事件异步完成,保证最终一致。
6.1 Outbox + MQ:避免“写库成功但消息丢失”
事务内:写订单表 + 写 outbox 表 事务外:发布 outbox 到 MQ,成功后标记 published
@Service
public class OrderAppService {
private final OrderRepository orderRepo;
private final OutboxRepository outboxRepo;
@Transactional
public void createOrder(CreateOrderCmd cmd) {
Order order = Order.create(cmd.userId(), cmd.items());
orderRepo.save(order);
OutboxEvent evt = OutboxEvent.of(
"ORDER_CREATED",
"{\"orderId\":" + order.getId() + "}"
);
outboxRepo.save(evt);
}
}
然后用定时任务扫描 outbox 未发布事件,发布成功后更新状态。 这套机制的价值:你不需要把所有事都同步做完,系统会更稳。
7)幂等:稳定性工程的最后一块拼图
当你引入 MQ、重试、回调,你必须接受一件事: 重复一定会发生。
所以所有关键写操作必须幂等,尤其:
- 支付回调处理
- 发货/核销
- 退款处理
- 库存扣减
一个简单的幂等方式:用业务唯一键 + 唯一索引
@Entity
@Table(name="payment_callback",
uniqueConstraints = @UniqueConstraint(columnNames = {"callbackId"}))
public class PaymentCallbackRecord {
@Id
private Long id;
private String callbackId;
private Long orderId;
private LocalDateTime createdAt;
}
处理回调时,先插入记录;插入失败说明处理过,直接返回成功。 这是工程里非常实用的一招:用数据库约束做幂等锁。
结语:稳定性不是“加个组件”,而是架构习惯
限流、熔断、降级、隔离、最终一致、幂等,这些不是“高大上”的选配项,而是你系统走到一定规模后必须具备的生存能力。
你可以按优先级逐步落地:
- 关键接口先做限流(保护入口)
- 外部依赖加熔断(保护线程池)
- 非关键链路做降级(保主链路)
- 大模块做资源隔离(防扩散)
- 写流程异步化(outbox + MQ)
- 全链路幂等(接受重复)
做完这套,你会发现系统并没有“更复杂”,反而更可控: 故障发生时,不再靠运气,而是靠机制。
评论 0