手写一个企业级重试组件:从编码式 Retry 到 Spring Boot Starter
一、为什么需要重试组件?
在分布式系统中,服务之间调用非常频繁。
例如:
订单服务调用库存服务:
订单服务 库存服务
| |
| ---- 扣库存请求 ----> |
| |
| <---- timeout ------- |
这次失败并不一定代表库存服务不可用。
可能原因:
- 网络瞬时抖动
- 服务 GC
- 数据库连接池暂时不足
- 下游服务负载过高
如果直接返回失败:
用户支付失败
订单创建失败
业务异常
但是如果等待一段时间再次请求:
第一次失败
等待
第二次成功
用户体验会更好。
因此需要:
当接口调用失败时,根据一定策略重新执行。
这就是:
重试机制(Retry)
二、最简单的重试方式有什么问题?
很多人第一反应:
for(int i=0;i<3;i++){
try{
remoteCall();
}catch(Exception e){
}
}
虽然可以工作,但是存在大量问题。
1. 重试策略无法扩展
比如:
固定等待:
失败
等待1秒
重试
但是有些场景需要:
指数退避:
失败
等待1秒
失败
等待2秒
失败
等待4秒
不同业务需要不同策略。
2. 不知道什么情况下需要重试
例如:
接口返回:
{
"code":500
}
可能需要重试。
但是:
{
"code":401
}
代表权限错误。
继续重试没有意义。
异常也是一样:
需要:
TimeoutException 重试
IllegalArgumentException 不重试
所以:
重试条件也应该抽象。
3. 同步异步无法统一
同步:
调用线程
失败
等待
重新调用
异步:
调用线程
失败
提交线程池
后台继续重试
应该由组件控制。
三、整体设计
最终设计如下:
Retryer
|
-------------------------
| |
重试条件判断 重试执行策略
Predicate RetryStrategy
|
|
Listener
|
|
Executor
核心组成:
| 模块 | 作用 |
|---|---|
| Retryer | 负责整体流程 |
| RetryerStrategy | 控制等待策略 |
| Predicate | 判断是否需要重试 |
| Listener | 监听重试过程 |
| Executor | 支持异步 |
| Builder | 组装对象 |
| AOP | 实现注解调用 |
四、重试策略设计
1. 抽象父类
所有重试策略都有:
- 最大次数
- 等待时间
- 时间单位
因此抽象:
public abstract class RetryerStrategy {
protected int maxAttempts;
protected long time;
protected TimeUnit timeUnit;
abstract boolean exec();
}
这里使用:
模板模式
父类负责公共能力:
参数保存
监听执行
子类负责:
具体等待算法
五、固定时间重试策略
例如:
配置:
最大次数 3
等待 1秒
执行:
第一次失败
sleep 1秒
第二次
sleep 1秒
第三次
sleep 1秒
代码:
public class TimeRetryerStrategy
extends RetryerStrategy{
@Override
boolean exec(){
while(maxAttempts-->0){
timeUnit.sleep(time);
return true;
}
return false;
}
}
六、指数退避策略
在真实生产环境更加常见。
例如:
配置:
baseTime=1秒
执行:
第一次失败
等待1秒
第二次失败
等待2秒
第三次失败
等待4秒
公式:
等待时间 =
基础时间 × 2^(当前次数-1)
代码:
long waitTime =
(long)(time*Math.pow(2,count-1));
为什么不用固定时间?
因为:
如果下游服务挂了:
大量请求同时重试。
例如:
10000个请求:
0秒失败
1秒后全部重试
再次失败
2秒后全部重试
形成:
惊群效应。
指数退避可以降低压力。
七、为什么需要 RetryListener?
重试过程中可能需要:
记录日志:
订单接口第3次重试
监控:
retry_count++
报警:
接口连续失败
如果直接写死:
Retryer越来越臃肿。
因此:
定义监听器:
@FunctionalInterface
public interface RetryerLister {
void exec(RetryerStrategy strategy);
}
使用:
.retryerLister(strategy -> {
System.out.println("发生重试");
})
这就是:
观察者模式
八、重试条件设计 Predicate
核心问题:
什么时候重试?
例如:
返回值:
false
500
error
异常:
TimeoutException
IOException
因此:
抽象:
Predicate
本质:
输入:
结果
输出:
是否重试
例如:
result -> result.equals(false)
九、多个重试条件
实际业务:
可能:
返回false
或者
返回500
或者
TimeoutException
所以:
需要组合。
设计:
Predicates.or(
p1,
p2,
p3
)
内部:
for(Predicate p:list){
if(p.apply(input)){
return true;
}
}
return false;
只要一个条件满足:
执行重试。
十、Retryer核心流程
用户:
retryer.exec(()->{
return api.call();
});
内部流程:
执行用户代码
|
|
获取结果
|
|
Predicate判断
|
是否需要重试?
|
Strategy控制等待
|
再次执行
代码:
public <T>T exec(RetryerSupplier supplier){
try{
T result=supplier.exec();
if(shouldRetry(result)){
retry();
}
return result;
}catch(Exception e){
if(shouldRetry(e)){
retry();
}
}
}
十一、为什么使用 Builder?
Retryer需要:
策略
结果判断
异常判断
监听器
线程池
如果构造:
new Retryer(
strategy,
predicate,
exception,
listener,
executor
)
参数越来越多。
所以:
使用建造者模式。
调用:
RetryerBuilder
.newBuilder()
.withRetryerStrategy(strategy)
.retryerIfResult(predicate)
.retryerLister(listener)
.build();
优点:
- 可读性高
- 扩展方便
- 避免构造函数爆炸
十二、实现注解方式 Retry
前面是编码方式:
retryer.exec()
但是:
Spring项目更希望:
@Retryer
public Boolean createOrder(){
}
自动拥有重试能力。
怎么实现?
核心:
Spring Bean 生命周期。
十三、BeanPostProcessor实现自动增强
Spring创建Bean:
流程:
实例化
↓
属性注入
↓
初始化
↓
BeanPostProcessor
↓
放入IOC
我们插入:
postProcessAfterInitialization()
扫描:
method.isAnnotationPresent(Retryer.class)
找到:
@Retryer
public void test()
然后:
生成代理对象。
十四、代理模式实现增强
原本:
用户
|
真实对象
|
test()
变成:
用户
|
代理对象
|
Retryer
|
真实对象.test()
代理里面:
自动执行:
重试逻辑
用户完全无感。
这就是:
Spring AOP核心思想。
十五、自动装配 Starter
最终希望:
用户只需要:
@EnableRetryer
即可。
实现:
resources:
META-INF/spring.factories
配置:
EnableAutoConfiguration=
RetryerAutoConfiguration
启动:
Spring自动加载。
这就是:
Spring Boot Starter开发流程。
十六、设计过程中遇到的问题
问题1:RetryStrategy状态污染
错误设计:
RetryerStrategy
{
maxAttempts
}
执行:
3
2
1
0
第二次调用:
直接:
0
为什么?
因为:
策略对象保存了状态。
正确:
配置和状态分离。
例如:
配置:
RetryConfig{
maxAttempts=3
}
执行:
每次创建:
new RetryStrategy(config)
避免污染。
十七、最终架构总结
完整结构:
Spring Bean
|
BeanPostProcessor
|
Proxy对象
|
Retryer
---------------------
| |
Predicate Strategy
| |
判断是否重试 等待多久
|
Listener
|
Executor
十八、从这个组件可以学到什么?
这个项目真正价值不是重试。
而是学习:
1. 模板模式
抽象公共流程。
2. 策略模式
不同重试算法自由替换。
3. 建造者模式
解决复杂对象创建。
4. 代理模式
实现无侵入增强。
5. 观察者模式
监听扩展。
6. Spring Bean 生命周期
实现 Starter。
总结
一个成熟的重试组件,并不是:
while循环 + sleep
而应该考虑:
- 什么情况下重试?
- 重试多久?
- 重试几次?
- 是否异步?
- 如何监控?
- 如何无侵入接入业务?
- 如何扩展?
通过这个组件,我们实际上完成了一次:
从工具类 → 框架组件 → Spring Boot Starter 的设计演进。
这也是很多开源框架的核心思想。