重试为什么可能放大故障?怎么避免雪崩?

1. 概述

1.1 重试机制的双刃剑

重试机制是提高系统可靠性的重要手段,但如果使用不当,可能放大故障,甚至导致系统雪崩。

重试的风险

  • 放大故障:重试可能将小故障放大为大故障
  • 雪崩效应:重试风暴可能导致整个系统崩溃
  • 资源耗尽:大量重试可能耗尽系统资源
  • 级联故障:一个服务的故障可能传播到整个系统

1.2 雪崩效应

雪崩效应:当一个服务出现故障时,由于重试机制,大量请求堆积,导致服务完全不可用,进而影响依赖它的其他服务,最终导致整个系统崩溃。

雪崩的特点

  • 快速传播:故障快速传播到整个系统
  • 难以恢复:一旦发生,难以快速恢复
  • 影响范围大:影响整个系统的可用性

1.3 本文内容结构

本文将从以下几个方面全面解析重试机制的风险和避免雪崩的方法:

  1. 重试放大故障的原因:为什么重试可能放大故障
  2. 雪崩效应原理:雪崩是如何发生的
  3. 避免雪崩的方案:如何设计重试策略避免雪崩
  4. 重试策略优化:指数退避、重试限制、熔断等
  5. 实战案例:实际项目中的重试优化

2. 重试放大故障的原因

2.1 重试风暴

2.1.1 什么是重试风暴

重试风暴:当服务出现故障时,大量客户端同时重试,导致请求量急剧增加,进一步加重服务负担,形成恶性循环。

场景示例

1
2
3
4
5
6
正常情况:
- 服务A每秒接收100个请求
- 服务A出现故障,响应变慢
- 100个客户端同时重试
- 服务A每秒接收200个请求(100个新请求 + 100个重试请求)
- 服务A负载翻倍,故障加剧

2.1.2 重试风暴的危害

危害

  • 请求量翻倍:重试导致请求量成倍增加
  • 资源耗尽:大量重试耗尽服务资源(CPU、内存、连接数等)
  • 故障加剧:服务负载增加,故障更加严重
  • 级联故障:故障传播到依赖服务

2.1.3 错误的重试实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误示例:无限制重试
@Service
public class OrderService {

@Autowired
private PaymentClient paymentClient;

/**
* 错误的重试实现:无限制重试
*/
public PaymentResult payOrder(PaymentRequest request) {
while (true) { // 无限重试
try {
return paymentClient.pay(request);
} catch (Exception e) {
log.warn("Payment failed, retrying...", e);
// 没有延迟,立即重试
// 没有重试次数限制
}
}
}
}

问题

  • 无限重试,可能导致服务完全不可用
  • 没有延迟,立即重试,加重服务负担
  • 没有重试次数限制,可能永远重试

2.2 同步重试放大故障

2.2.1 同步重试的问题

同步重试:在同一个线程中重试,阻塞线程资源。

问题

  • 线程阻塞:重试期间线程被阻塞,无法处理其他请求
  • 线程池耗尽:大量线程被重试占用,线程池耗尽
  • 请求堆积:新请求无法处理,请求堆积

2.2.2 错误示例

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
// 错误示例:同步重试,阻塞线程
@Service
public class OrderService {

@Autowired
private InventoryClient inventoryClient;

/**
* 同步重试:阻塞线程
*/
public InventoryResult deductStock(Long skuId, Integer quantity) {
int maxRetries = 10; // 最多重试10次

for (int i = 0; i < maxRetries; i++) {
try {
return inventoryClient.deductStock(skuId, quantity);
} catch (Exception e) {
if (i < maxRetries - 1) {
try {
Thread.sleep(100); // 固定延迟100ms
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new BusinessException("重试被中断", ie);
}
} else {
throw new BusinessException("库存服务调用失败", e);
}
}
}

throw new BusinessException("库存服务调用失败");
}
}

问题

  • 同步重试,阻塞线程
  • 固定延迟,没有指数退避
  • 重试次数过多(10次)

2.3 无延迟重试放大故障

2.3.1 无延迟重试的问题

无延迟重试:失败后立即重试,没有延迟。

问题

  • 请求突增:大量请求同时重试,请求量突增
  • 服务过载:服务还未恢复,又收到大量重试请求
  • 故障持续:服务无法恢复,故障持续

2.3.2 错误示例

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
// 错误示例:无延迟重试
@Service
public class OrderService {

@Autowired
private PaymentClient paymentClient;

/**
* 无延迟重试:立即重试
*/
public PaymentResult payOrder(PaymentRequest request) {
int maxRetries = 5;

for (int i = 0; i < maxRetries; i++) {
try {
return paymentClient.pay(request);
} catch (Exception e) {
if (i < maxRetries - 1) {
// 没有延迟,立即重试
// 这会导致大量请求同时重试
} else {
throw new BusinessException("支付失败", e);
}
}
}

throw new BusinessException("支付失败");
}
}

2.4 重试范围过大

2.4.1 重试范围过大的问题

重试范围过大:对所有异常都重试,包括不应该重试的异常。

问题

  • 无效重试:对业务异常重试,浪费资源
  • 数据不一致:对幂等性操作重试,可能导致数据不一致
  • 资源浪费:对不可恢复的错误重试,浪费资源

2.4.2 错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误示例:对所有异常都重试
@Service
public class OrderService {

@Autowired
private PaymentClient paymentClient;

/**
* 错误:对所有异常都重试
*/
@Retryable(value = Exception.class) // 对所有异常都重试
public PaymentResult payOrder(PaymentRequest request) {
return paymentClient.pay(request);
}
}

问题

  • 对业务异常(如余额不足)也重试,浪费资源
  • 对参数错误也重试,没有意义
  • 对权限错误也重试,不会成功

3. 雪崩效应原理

3.1 雪崩的发生过程

3.1.1 雪崩的触发

雪崩触发条件

  1. 服务故障:某个服务出现故障(响应慢、超时、异常等)
  2. 大量重试:客户端大量重试,请求量急剧增加
  3. 资源耗尽:服务资源(线程、连接、内存等)耗尽
  4. 故障传播:故障传播到依赖服务

3.1.2 雪崩的传播

雪崩传播路径

1
2
3
4
5
6
7
8
9
10
11
12
13
服务A故障

客户端重试(请求量翻倍)

服务A资源耗尽(线程池、连接池等)

服务A完全不可用

依赖服务A的服务B也受影响

服务B也开始重试

整个系统雪崩

3.2 雪崩的数学模型

3.2.1 请求量计算

请求量计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
总请求量 = 新请求量 + 重试请求量

重试请求量 = 新请求量 × 重试次数 × 重试比例

其中:
- 重试次数:每个请求最多重试的次数
- 重试比例:失败请求的比例

示例:
- 新请求量:100 QPS
- 失败率:50%
- 重试次数:3次
- 重试请求量 = 100 × 50% × 3 = 150 QPS
- 总请求量 = 100 + 150 = 250 QPS(增加2.5倍)

3.2.2 资源消耗计算

资源消耗

1
2
3
4
5
6
7
8
9
10
线程消耗 = 并发请求数 × 平均处理时间

如果:
- 并发请求数:250(包括重试)
- 平均处理时间:2秒(服务变慢)
- 线程消耗 = 250 × 2 = 500个线程

如果线程池大小只有100,则:
- 500个线程 > 100个线程池大小
- 线程池耗尽,新请求无法处理

3.3 雪崩的典型案例

3.3.1 案例:支付服务雪崩

场景

  • 支付服务响应变慢(从100ms增加到2秒)
  • 订单服务调用支付服务,超时后重试
  • 100个订单同时创建,每个重试3次
  • 支付服务收到400个请求(100个新请求 + 300个重试请求)
  • 支付服务线程池耗尽,完全不可用
  • 依赖支付服务的其他服务也受影响

4. 避免雪崩的方案

4.1 指数退避(Exponential Backoff)

4.1.1 指数退避原理

指数退避:重试间隔逐渐增加,避免大量请求同时重试。

公式

1
2
3
4
5
6
7
8
延迟时间 = 初始延迟 × 2^重试次数

示例:
- 初始延迟:100ms
- 第1次重试:100ms
- 第2次重试:200ms
- 第3次重试:400ms
- 第4次重试:800ms

4.1.2 指数退避实现

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
34
35
36
37
@Service
public class OrderService {

@Autowired
private PaymentClient paymentClient;

/**
* 指数退避重试
*/
public PaymentResult payOrder(PaymentRequest request) {
int maxRetries = 3;
long initialDelay = 100; // 初始延迟100ms
long maxDelay = 5000; // 最大延迟5秒

for (int i = 0; i < maxRetries; i++) {
try {
return paymentClient.pay(request);
} catch (Exception e) {
if (i < maxRetries - 1) {
// 指数退避:延迟时间 = 初始延迟 × 2^重试次数
long delay = Math.min(initialDelay * (long) Math.pow(2, i), maxDelay);

try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new BusinessException("重试被中断", ie);
}
} else {
throw new BusinessException("支付失败", e);
}
}
}

throw new BusinessException("支付失败");
}
}

4.1.3 带抖动的指数退避

抖动(Jitter):在指数退避的基础上添加随机抖动,避免大量请求同时重试。

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
/**
* 带抖动的指数退避
*/
public PaymentResult payOrderWithJitter(PaymentRequest request) {
int maxRetries = 3;
long initialDelay = 100;
long maxDelay = 5000;
Random random = new Random();

for (int i = 0; i < maxRetries; i++) {
try {
return paymentClient.pay(request);
} catch (Exception e) {
if (i < maxRetries - 1) {
// 指数退避 + 随机抖动
long baseDelay = Math.min(initialDelay * (long) Math.pow(2, i), maxDelay);
long jitter = (long) (baseDelay * 0.1 * random.nextDouble()); // 10%的抖动
long delay = baseDelay + jitter;

try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new BusinessException("重试被中断", ie);
}
} else {
throw new BusinessException("支付失败", e);
}
}
}

throw new BusinessException("支付失败");
}

4.2 限制重试次数

4.2.1 重试次数限制

重试次数限制:限制最大重试次数,避免无限重试。

最佳实践

  • 网络错误:重试3-5次
  • 超时错误:重试2-3次
  • 5xx错误:重试1-2次
  • 4xx错误:不重试(客户端错误)
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@Service
public class OrderService {

@Autowired
private PaymentClient paymentClient;

/**
* 根据异常类型设置不同的重试次数
*/
public PaymentResult payOrder(PaymentRequest request) {
int maxRetries = getMaxRetries(request);

for (int i = 0; i < maxRetries; i++) {
try {
return paymentClient.pay(request);
} catch (TimeoutException e) {
// 超时错误:重试3次
if (i < 2) {
sleepWithBackoff(i);
} else {
throw new BusinessException("支付超时", e);
}
} catch (ConnectException e) {
// 连接错误:重试5次
if (i < 4) {
sleepWithBackoff(i);
} else {
throw new BusinessException("支付服务连接失败", e);
}
} catch (HttpServerException e) {
// 5xx错误:重试2次
if (i < 1) {
sleepWithBackoff(i);
} else {
throw new BusinessException("支付服务错误", e);
}
} catch (HttpClientException e) {
// 4xx错误:不重试
throw new BusinessException("支付请求错误", e);
}
}

throw new BusinessException("支付失败");
}

private int getMaxRetries(PaymentRequest request) {
// 根据请求类型设置不同的重试次数
return 3; // 默认3次
}

private void sleepWithBackoff(int retryCount) {
long delay = 100 * (long) Math.pow(2, retryCount);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

4.3 熔断器保护

4.3.1 熔断器防止重试风暴

熔断器:当服务故障率超过阈值时,快速失败,不进行重试。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Service
public class OrderService {

@Autowired
private PaymentClient paymentClient;

private final CircuitBreaker circuitBreaker;

public OrderService() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 失败率阈值:50%
.waitDurationInOpenState(Duration.ofSeconds(10)) // 打开状态等待时间:10秒
.slidingWindowSize(10) // 滑动窗口大小:10个请求
.build();

this.circuitBreaker = CircuitBreaker.of("payment", config);
}

/**
* 使用熔断器保护的重试
*/
public PaymentResult payOrder(PaymentRequest request) {
return circuitBreaker.executeSupplier(() -> {
// 熔断器打开时,直接失败,不重试
return retryWithBackoff(() -> paymentClient.pay(request));
});
}

private PaymentResult retryWithBackoff(Supplier<PaymentResult> supplier) {
int maxRetries = 3;
long initialDelay = 100;

for (int i = 0; i < maxRetries; i++) {
try {
return supplier.get();
} catch (Exception e) {
if (i < maxRetries - 1) {
long delay = initialDelay * (long) Math.pow(2, i);
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new BusinessException("重试被中断", ie);
}
} else {
throw new BusinessException("支付失败", e);
}
}
}

throw new BusinessException("支付失败");
}
}

4.4 异步重试

4.4.1 异步重试的优势

异步重试:将重试任务放入队列,异步执行,不阻塞主线程。

优势

  • 不阻塞线程:主线程不被阻塞
  • 控制重试速率:通过队列控制重试速率
  • 批量处理:可以批量处理重试任务

4.4.2 异步重试实现

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@Service
public class OrderService {

@Autowired
private PaymentClient paymentClient;

@Autowired
private KafkaTemplate<String, String> kafkaTemplate;

/**
* 异步重试:将重试任务放入消息队列
*/
public PaymentResult payOrder(PaymentRequest request) {
try {
// 第一次尝试
return paymentClient.pay(request);
} catch (Exception e) {
log.warn("Payment failed, sending to retry queue: request={}", request, e);

// 发送到重试队列,异步重试
RetryMessage retryMessage = new RetryMessage();
retryMessage.setRequest(request);
retryMessage.setRetryCount(0);
retryMessage.setMaxRetries(3);
retryMessage.setNextRetryTime(System.currentTimeMillis() + 100); // 100ms后重试

kafkaTemplate.send("payment-retry", JSON.toJSONString(retryMessage));

// 返回处理中状态
return PaymentResult.processing("支付处理中,请稍后查询");
}
}
}

@Component
public class PaymentRetryConsumer {

@Autowired
private PaymentClient paymentClient;

@KafkaListener(topics = "payment-retry", groupId = "payment-retry-group")
public void handleRetry(String message) {
RetryMessage retryMessage = JSON.parseObject(message, RetryMessage.class);

// 检查是否到了重试时间
if (System.currentTimeMillis() < retryMessage.getNextRetryTime()) {
// 还没到重试时间,重新发送到队列
kafkaTemplate.send("payment-retry", message);
return;
}

try {
// 执行重试
PaymentResult result = paymentClient.pay(retryMessage.getRequest());
log.info("Payment retry success: request={}", retryMessage.getRequest());
} catch (Exception e) {
log.warn("Payment retry failed: request={}, retryCount={}",
retryMessage.getRequest(), retryMessage.getRetryCount(), e);

// 检查是否还有重试次数
if (retryMessage.getRetryCount() < retryMessage.getMaxRetries()) {
// 指数退避
long delay = 100 * (long) Math.pow(2, retryMessage.getRetryCount());
retryMessage.setRetryCount(retryMessage.getRetryCount() + 1);
retryMessage.setNextRetryTime(System.currentTimeMillis() + delay);

// 重新发送到队列
kafkaTemplate.send("payment-retry", JSON.toJSONString(retryMessage));
} else {
// 重试次数用完,发送到死信队列
log.error("Payment retry exhausted: request={}", retryMessage.getRequest());
kafkaTemplate.send("payment-dlq", JSON.toJSONString(retryMessage));
}
}
}
}

4.5 重试限流

4.5.1 重试限流原理

重试限流:限制重试请求的速率,避免重试风暴。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@Service
public class OrderService {

@Autowired
private PaymentClient paymentClient;

@Autowired
private RedisTemplate<String, String> redisTemplate;

/**
* 重试限流:限制重试请求的速率
*/
public PaymentResult payOrder(PaymentRequest request) {
int maxRetries = 3;
long initialDelay = 100;

for (int i = 0; i < maxRetries; i++) {
try {
return paymentClient.pay(request);
} catch (Exception e) {
if (i < maxRetries - 1) {
// 检查重试限流
if (!checkRetryRateLimit("payment-retry")) {
throw new BusinessException("重试过于频繁,请稍后再试");
}

// 指数退避
long delay = initialDelay * (long) Math.pow(2, i);
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new BusinessException("重试被中断", ie);
}
} else {
throw new BusinessException("支付失败", e);
}
}
}

throw new BusinessException("支付失败");
}

/**
* 检查重试限流
*/
private boolean checkRetryRateLimit(String key) {
String cacheKey = "retry_rate_limit:" + key;
String countStr = redisTemplate.opsForValue().get(cacheKey);
int count = countStr == null ? 0 : Integer.parseInt(countStr);

// 每秒最多10个重试请求
if (count >= 10) {
return false;
}

redisTemplate.opsForValue().increment(cacheKey);
redisTemplate.expire(cacheKey, 1, TimeUnit.SECONDS);
return true;
}
}

4.6 选择性重试

4.6.1 选择性重试原理

选择性重试:只对特定异常重试,不对所有异常重试。

应该重试的异常

  • 网络错误:连接超时、读取超时等
  • 5xx错误:服务器错误
  • 临时故障:服务暂时不可用

不应该重试的异常

  • 4xx错误:客户端错误(参数错误、权限错误等)
  • 业务异常:余额不足、库存不足等
  • 幂等性错误:重复请求等
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Service
public class OrderService {

@Autowired
private PaymentClient paymentClient;

/**
* 选择性重试:只对特定异常重试
*/
public PaymentResult payOrder(PaymentRequest request) {
int maxRetries = 3;
long initialDelay = 100;

for (int i = 0; i < maxRetries; i++) {
try {
return paymentClient.pay(request);
} catch (TimeoutException e) {
// 超时错误:重试
if (i < maxRetries - 1) {
sleepWithBackoff(i, initialDelay);
} else {
throw new BusinessException("支付超时", e);
}
} catch (ConnectException e) {
// 连接错误:重试
if (i < maxRetries - 1) {
sleepWithBackoff(i, initialDelay);
} else {
throw new BusinessException("支付服务连接失败", e);
}
} catch (HttpServerException e) {
// 5xx错误:重试
if (i < maxRetries - 1) {
sleepWithBackoff(i, initialDelay);
} else {
throw new BusinessException("支付服务错误", e);
}
} catch (HttpClientException e) {
// 4xx错误:不重试
throw new BusinessException("支付请求错误", e);
} catch (BusinessException e) {
// 业务异常:不重试
throw e;
}
}

throw new BusinessException("支付失败");
}

private void sleepWithBackoff(int retryCount, long initialDelay) {
long delay = initialDelay * (long) Math.pow(2, retryCount);
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

5. 综合方案

5.1 完整的重试策略

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@Service
public class OrderService {

@Autowired
private PaymentClient paymentClient;

private final CircuitBreaker circuitBreaker;
private final RateLimiter retryRateLimiter;

public OrderService() {
// 熔断器配置
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofSeconds(10))
.slidingWindowSize(10)
.build();
this.circuitBreaker = CircuitBreaker.of("payment", circuitBreakerConfig);

// 重试限流器配置
this.retryRateLimiter = RateLimiter.of("payment-retry",
RateLimiterConfig.custom()
.limitForPeriod(10) // 每秒最多10个重试
.limitRefreshPeriod(Duration.ofSeconds(1))
.build());
}

/**
* 完整的重试策略
*/
public PaymentResult payOrder(PaymentRequest request) {
// 1. 熔断器保护
return circuitBreaker.executeSupplier(() -> {
// 2. 重试限流
if (!retryRateLimiter.acquirePermission()) {
throw new BusinessException("重试过于频繁,请稍后再试");
}

// 3. 选择性重试 + 指数退避
return retryWithStrategy(() -> paymentClient.pay(request));
});
}

/**
* 重试策略:选择性重试 + 指数退避 + 抖动
*/
private PaymentResult retryWithStrategy(Supplier<PaymentResult> supplier) {
int maxRetries = 3;
long initialDelay = 100;
long maxDelay = 5000;
Random random = new Random();

for (int i = 0; i < maxRetries; i++) {
try {
return supplier.get();
} catch (TimeoutException | ConnectException | HttpServerException e) {
// 只对网络错误和5xx错误重试
if (i < maxRetries - 1) {
// 指数退避 + 抖动
long baseDelay = Math.min(initialDelay * (long) Math.pow(2, i), maxDelay);
long jitter = (long) (baseDelay * 0.1 * random.nextDouble());
long delay = baseDelay + jitter;

try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new BusinessException("重试被中断", ie);
}
} else {
throw new BusinessException("支付失败", e);
}
} catch (HttpClientException | BusinessException e) {
// 4xx错误和业务异常:不重试
throw new BusinessException("支付失败", e);
}
}

throw new BusinessException("支付失败");
}
}

6. 监控和告警

6.1 重试监控

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
34
35
36
@Component
public class RetryMetrics {

private final MeterRegistry meterRegistry;

public RetryMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
}

/**
* 记录重试指标
*/
public void recordRetry(String service, int retryCount, boolean success) {
meterRegistry.counter("retry_count",
"service", service,
"retry_count", String.valueOf(retryCount),
"success", String.valueOf(success))
.increment();
}

/**
* 记录重试延迟
*/
public void recordRetryDelay(String service, long delay) {
meterRegistry.timer("retry_delay", "service", service)
.record(delay, TimeUnit.MILLISECONDS);
}

/**
* 记录重试风暴告警
*/
public void recordRetryStorm(String service) {
meterRegistry.counter("retry_storm", "service", service).increment();
// 发送告警
}
}

7. 总结

7.1 核心要点

  1. 重试放大故障的原因:重试风暴、同步重试、无延迟重试、重试范围过大
  2. 雪崩效应原理:服务故障 → 大量重试 → 资源耗尽 → 故障传播
  3. 避免雪崩的方案:指数退避、限制重试次数、熔断器保护、异步重试、重试限流、选择性重试
  4. 最佳实践:组合使用多种方案,形成完整的重试策略

7.2 关键理解

  1. 重试是双刃剑:合理使用提高可靠性,不当使用放大故障
  2. 指数退避是关键:避免大量请求同时重试
  3. 熔断器是保护:快速失败,防止重试风暴
  4. 选择性重试:只对特定异常重试,不对所有异常重试

7.3 最佳实践

  1. 指数退避 + 抖动:避免大量请求同时重试
  2. 限制重试次数:避免无限重试
  3. 熔断器保护:快速失败,防止重试风暴
  4. 异步重试:不阻塞主线程
  5. 重试限流:限制重试请求的速率
  6. 选择性重试:只对特定异常重试
  7. 监控告警:实时监控重试情况,及时发现问题

相关文章