一、重试的使用场景
在很多业务场景中,为了排除系统中的各种不稳定因素,以及逻辑上的错误,并最大概率保证获得预期的结果,重试机制都是必不可少的。尤其是调用远程服务,在高并发场景下,很可能因为服务器响应延迟或者网络原因,造成我们得不到想要的结果,或者根本得不到响应。这个时候,一个优雅的重试调用机制,可以让我们更大概率保证得到预期的响应。
通常情况下,我们会通过定时任务进行重试。例如某次操作失败,则记录下来,当定时任务再次启动,则将数据放到定时任务的方法中,重新跑一遍。最终直至得到想要的结果为止。
无论是基于定时任务的重试机制,还是我们自己写的简单的重试器,缺点都是重试的机制太单一,而且实现起来不优雅。
如何优雅地设计重试实现
一个完备的重试实现,要很好地解决如下问题:
什么条件下重试
什么条件下停止
如何停止重试
停止重试等待多久
如何等待
请求时间限制
如何结束
如何监听整个重试过程
并且,为了更好地封装性,重试的实现一般分为两步:
- 使用工厂模式构造重试器
- 执行重试方法并得到结果
一个完整的重试流程可以简单示意为:
二、guava-retrying基础用法
guava-retrying是基于谷歌的核心类库guava的重试机制实现,可以说是一个重试利器。
<!-- https://mvnrepository.com/artifact/com.github.rholder/guava-retrying -->
<dependency>
<groupId>com.github.rholder</groupId>
<artifactId>guava-retrying</artifactId>
<version>2.0.0</version>
</dependency>
1.Maven配置
需要注意的是,此版本依赖的是27.0.1版本的guava。如果你项目中的guava低几个版本没问题,但是低太多就不兼容了。这个时候你需要升级你项目的guava版本,或者直接去掉你自己的guava依赖,使用guava-retrying传递过来的guava依赖。
2.实现Callable
Callable<Boolean> callable = new Callable<Boolean>() {
public Boolean call() throws Exception {
return true; // do something useful here
}
};
Callable的call方法中是你自己实际的业务调用。
3、通过RetryerBuilder构造Retryer
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.<Boolean>isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
4、使用重试器执行你的业务
retryer.call(callable);
5、下面是完整的参考实现。
public Boolean test() throws Exception {
//定义重试机制
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
//retryIf 重试条件
.retryIfException()
.retryIfRuntimeException()
.retryIfExceptionOfType(Exception.class)
.retryIfException(Predicates.equalTo(new Exception()))
.retryIfResult(Predicates.equalTo(false))
//等待策略:每次请求间隔1s
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
//停止策略 : 尝试请求6次
.withStopStrategy(StopStrategies.stopAfterAttempt(6))
//时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))
.build();
//定义请求实现
Callable<Boolean> callable = new Callable<Boolean>() {
int times = 1;
@Override
public Boolean call() throws Exception {
log.info("call times={}", times);
times++;
if (times == 2) {
throw new NullPointerException();
} else if (times == 3) {
throw new Exception();
} else if (times == 4) {
throw new RuntimeException();
} else if (times == 5) {
return false;
} else {
return true;
}
}
};
//利用重试器调用请求
return retryer.call(callable);
}
三、guava-retrying实现原理
guava-retrying的核心是Attempt类、Retryer类以及一些Strategy(策略)相关的类。
1、Attempt
Attempt既是一次重试请求(call),也是请求的结果,并记录了当前请求的次数、是否包含异常和请求的返回值。
/**
* An attempt of a call, which resulted either in a result returned by the call,
* or in a Throwable thrown by the call.
*
* @param <V> The type returned by the wrapped callable.
* @author JB
*/
public interface Attempt<V>
2、Retryer
Retryer通过RetryerBuilder这个工厂类进行构造。RetryerBuilder负责将定义的重试策略赋值到Retryer对象中。
在Retryer执行call方法的时候,会将这些重试策略一一使用。
下面就看一下Retryer的call方法的具体实现。
/**
* Executes the given callable. If the rejection predicate
* accepts the attempt, the stop strategy is used to decide if a new attempt
* must be made. Then the wait strategy is used to decide how much time to sleep
* and a new attempt is made.
*
* @param callable the callable task to be executed
* @return the computed result of the given callable
* @throws ExecutionException if the given callable throws an exception, and the
* rejection predicate considers the attempt as successful. The original exception
* is wrapped into an ExecutionException.
* @throws RetryException if all the attempts failed before the stop strategy decided
* to abort, or the thread was interrupted. Note that if the thread is interrupted,
* this exception is thrown and the thread's interrupt status is set.
*/
public V call(Callable<V> callable) throws ExecutionException, RetryException {
long startTime = System.nanoTime();
//说明: 根据attemptNumber进行循环——也就是重试多少次
for (int attemptNumber = 1; ; attemptNumber++) {
//说明:进入方法不等待,立即执行一次
Attempt<V> attempt;
try {
//说明:执行callable中的具体业务
//attemptTimeLimiter限制了每次尝试等待的时常
V result = attemptTimeLimiter.call(callable);
//利用调用结果构造新的attempt
attempt = new ResultAttempt<V>(result, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
} catch (Throwable t) {
attempt = new ExceptionAttempt<V>(t, attemptNumber, TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime));
}
//说明:遍历自定义的监听器
for (RetryListener listener : listeners) {
listener.onRetry(attempt);
}
//说明:判断是否满足重试条件,来决定是否继续等待并进行重试
if (!rejectionPredicate.apply(attempt)) {
return attempt.get();
}
//说明:此时满足停止策略,因为还没有得到想要的结果,因此抛出异常
if (stopStrategy.shouldStop(attempt)) {
throw new RetryException(attemptNumber, attempt);
} else {
//说明:执行默认的停止策略——线程休眠
long sleepTime = waitStrategy.computeSleepTime(attempt);
try {
//说明:也可以执行定义的停止策略
blockStrategy.block(sleepTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RetryException(attemptNumber, attempt);
}
}
}
}
Retryer执行过程如下。
四、guava-retrying高级用法
基于guava-retrying的实现原理,我们可以根据实际业务来确定自己的重试策略。
下面以数据同步这种常规系统业务为例,自定义重试策略。
如下实现基于Spring Boot 2.1.2.RELEASE版本。
并使用Lombok简化Bean。
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
1、业务描述
当商品创建以后,需要另外设置商品的价格。由于两个操作是有两个人进行的,因此会出现如下问题,即商品没有创建,但是价格数据却已经建好了。遇到这种情况,价格数据需要等待商品正常创建以后,继续完成同步。
我们通过一个http请求进行商品的创建,同时通过一个定时器来修改商品的价格。
当商品不存在,或者商品的数量小于1的时候,商品的价格不能设置。需要等商品成功创建且数量大于0的时候,才能将商品的价格设置成功。
2、实现过程
1、自定义重试阻塞策略
默认的阻塞策略是线程休眠,这里使用自旋锁实现,不阻塞线程
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.strategy;
import com.github.rholder.retry.BlockStrategy;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import java.time.Duration;
import java.time.LocalDateTime;
/**
* 自旋锁的实现, 不响应线程中断
*/
@Slf4j
@NoArgsConstructor
public class SpinBlockStrategy implements BlockStrategy {
@Override
public void block(long sleepTime) throws InterruptedException {
LocalDateTime startTime = LocalDateTime.now();
long start = System.currentTimeMillis();
long end = start;
log.info("[SpinBlockStrategy]...begin wait.");
while (end - start <= sleepTime) {
end = System.currentTimeMillis();
}
//使用Java8新增的Duration计算时间间隔
Duration duration = Duration.between(startTime, LocalDateTime.now());
log.info("[SpinBlockStrategy]...end wait.duration={}", duration.toMillis());
}
}
2、自定义重试监听器
RetryListener可以监控多次重试过程,并可以使用attempt做一些额外的事情。
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.listener;
import com.github.rholder.retry.Attempt;
import com.github.rholder.retry.RetryListener;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class RetryLogListener implements RetryListener {
@Override
public <V> void onRetry(Attempt<V> attempt) {
// 第几次重试,(注意:第一次重试其实是第一次调用)
log.info("retry time : [{}]", attempt.getAttemptNumber());
// 距离第一次重试的延迟
log.info("retry delay : [{}]", attempt.getDelaySinceFirstAttempt());
// 重试结果: 是异常终止, 还是正常返回
log.info("hasException={}", attempt.hasException());
log.info("hasResult={}", attempt.hasResult());
// 是什么原因导致异常
if (attempt.hasException()) {
log.info("causeBy={}" , attempt.getExceptionCause().toString());
} else {
// 正常返回时的结果
log.info("result={}" , attempt.getResult());
}
log.info("log listen over.");
}
}
3、自定义Exception
有些异常需要重试,有些不需要。
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.exception;
/**
* 当抛出这个异常的时候,表示需要重试
*/
public class NeedRetryException extends Exception {
public NeedRetryException(String message) {
super("NeedRetryException can retry."+message);
}
}
4、实现具体重试业务与Callable接口
使用call方法调用自己的业务。
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.math.BigDecimal;
/**
* 商品model
*/
@Data
@AllArgsConstructor
public class Product {
private Long id;
private String name;
private Integer count;
private BigDecimal price;
}
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.repository;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.model.Product;
import org.springframework.stereotype.Repository;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* 商品DAO
*/
@Repository
public class ProductRepository {
private static ConcurrentHashMap<Long,Product> products=new ConcurrentHashMap();
private static AtomicLong ids=new AtomicLong(0);
public List<Product> findAll(){
return new ArrayList<>(products.values());
}
public Product findById(Long id){
return products.get(id);
}
public Product updatePrice(Long id, BigDecimal price){
Product p=products.get(id);
if (null==p){
return p;
}
p.setPrice(price);
return p;
}
public Product addProduct(Product product){
Long id=ids.addAndGet(1);
product.setId(id);
products.put(id,product);
return product;
}
}
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service;
import lombok.extern.slf4j.Slf4j;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.exception.NeedRetryException;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.model.Product;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.repository.ProductRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;
/**
* 业务方法实现
*/
@Component
@Slf4j
public class ProductInformationHander implements Callable<Boolean> {
@Autowired
private ProductRepository pRepo;
private static Map<Long, BigDecimal> prices = new HashMap<>();
static {
prices.put(1L, new BigDecimal(100));
prices.put(2L, new BigDecimal(200));
prices.put(3L, new BigDecimal(300));
prices.put(4L, new BigDecimal(400));
prices.put(8L, new BigDecimal(800));
prices.put(9L, new BigDecimal(900));
}
@Override
public Boolean call() throws Exception {
log.info("sync price begin,prices size={}", prices.size());
for (Long id : prices.keySet()) {
Product product = pRepo.findById(id);
if (null == product) {
throw new NeedRetryException("can not find product by id=" + id);
}
if (null == product.getCount() || product.getCount() < 1) {
throw new NeedRetryException("product count is less than 1, id=" + id);
}
Product updatedP = pRepo.updatePrice(id, prices.get(id));
if (null == updatedP) {
return false;
}
prices.remove(id);
}
log.info("sync price over,prices size={}", prices.size());
return true;
}
}
5、构造重试器Retryer
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service;
import com.github.rholder.retry.*;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.exception.NeedRetryException;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.listener.RetryLogListener;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.strategy.SpinBlockStrategy;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
* 构造重试器
*/
@Component
public class ProductRetryerBuilder {
public Retryer build() {
//定义重试机制
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
//retryIf 重试条件
//.retryIfException()
//.retryIfRuntimeException()
//.retryIfExceptionOfType(Exception.class)
//.retryIfException(Predicates.equalTo(new Exception()))
//.retryIfResult(Predicates.equalTo(false))
.retryIfExceptionOfType(NeedRetryException.class)
//等待策略:每次请求间隔1s
.withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS))
//停止策略 : 尝试请求3次
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
//时间限制 : 某次请求不得超过2s , 类似: TimeLimiter timeLimiter = new SimpleTimeLimiter();
.withAttemptTimeLimiter(AttemptTimeLimiters.fixedTimeLimit(2, TimeUnit.SECONDS))
//默认的阻塞策略:线程睡眠
//.withBlockStrategy(BlockStrategies.threadSleepStrategy())
//自定义阻塞策略:自旋锁
.withBlockStrategy(new SpinBlockStrategy())
//自定义重试监听器
.withRetryListener(new RetryLogListener())
.build();
return retryer;
}
}
6、与定时任务结合执行Retryer
定时任务只需要跑一次,但是实际上实现了所有的重试策略。这样大大简化了定时器的设计。
首先使用@EnableScheduling声明项目支持定时器注解。
@SpringBootApplication
@EnableScheduling
public class DemoRetryerApplication {
public static void main(String[] args) {
SpringApplication.run(DemoRetryerApplication.class, args);
}
}
package net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.task;
import com.github.rholder.retry.Retryer;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service.ProductInformationHander;
import net.ijiangtao.tech.framework.spring.ispringboot.demo.retryer.guava.service.ProductRetryerBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 商品信息定时器
*/
@Component
public class ProductScheduledTasks {
@Autowired
private ProductRetryerBuilder builder;
@Autowired
private ProductInformationHander hander;
/**
* 同步商品价格定时任务
* @Scheduled(fixedDelay = 30000) :上一次执行完毕时间点之后30秒再执行
*/
@Scheduled(fixedDelay = 30*1000)
public void syncPrice() throws Exception{
Retryer retryer=builder.build();
retryer.call(hander);
}
}
执行结果:由于并没有商品,因此重试以后,抛出异常。
2019-二月-28 14:37:52.667 INFO [scheduling-1] n.i.t.f.s.i.d.r.g.l.RetryLogListener - log listen over.
2019-二月-28 14:37:52.672 ERROR [scheduling-1] o.s.s.s.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task.
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
at com.github.rholder.retry.Retryer.call(Retryer.java:174)
你也可以增加一些商品数据,看一下重试成功的效果
完整示例
五、使用中遇到的问题
1、Guava版本冲突
由于项目中依赖的guava版本过低,启动项目时出现了如下异常。
java.lang.NoSuchMethodError: com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor()Lcom/google/common/util/concurrent/ListeningExecutorService;
at org.apache.curator.framework.listen.ListenerContainer.addListener(ListenerContainer.java:41)
at com.bzn.curator.ZkOperator.getZkClient(ZkOperator.java:207)
at com.bzn.curator.ZkOperator.checkExists(ZkOperator.java:346)
at com.bzn.curator.watcher.AbstractWatcher.initListen(AbstractWatcher.java:87)
at com.bzn.web.listener.NebulaSystemInitListener.initZkWatcher(NebulaSystemInitListener.java:84)
at com.bzn.web.listener.NebulaSystemInitListener.contextInitialized(NebulaSystemInitListener.java:33)
at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4939)
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5434)
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:150)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1559)
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1549)
at java.util.concurrent.FutureTask.run(FutureTask.java:266)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:748)
因此,要排除项目中低版本的guava依赖。
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
同时,由于Guava在新版本中移除了sameThreadExecutor方法,但目前项目中的ZK需要此方法,因此需要手动设置合适的guava版本。
果然,在19.0版本中MoreExecutors的此方法依然存在,只是标注为过期了。
@Deprecated
@GwtIncompatible("TODO")
public static ListeningExecutorService sameThreadExecutor() {
return new DirectExecutorService();
}
声明依赖的guava版本改为19.0即可。
<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
2、动态调节重试策略
在实际使用过程中,有时经常需要调整重试的次数、等待的时间等重试策略,因此,将重试策略的配置参数化保存,可以动态调节。
例如在秒杀、双十一购物节等时期增加等待的时间与重试次数,以保证错峰请求。在平时,可以适当减少等待时间和重试次数。
对于系统关键性业务,如果多次重试步成功,可以通过RetryListener进行监控与报警。
https://github.com/rholder/guava-retrying
https://github.com/spring-projects/spring-retry
https://blog.csdn.net/qq_27399407/article/details/78966536
https://www.jianshu.com/p/d56c417d2b97
http://www.voidcn.com/article/p-eoekttzg-dk.html
https://blog.csdn.net/aitangyong/article/details/53894899
https://www.jianshu.com/p/58e753ca0151
六、guava-retrying模块解析
模块提供了一种通用方法, 可以使用Guava谓词匹配增强的特定停止、重试和异常处理功能来重试任意Java代码。
- 优势
guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。
Guava Retryer也是线程安全的,入口调用逻辑采用的是 java.util.concurrent.Callable 的 call() 方法
- 示例:遇到异常之后,重试 3 次停止
public static void main(String[] args) {
Callable<Boolean> callable = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// do something useful here
LOGGER.info("call...");
throw new RuntimeException();
}
};
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(callable);
} catch (RetryException | ExecutionException e) {
e.printStackTrace();
}
}
2018-08-08 17:21:12.442 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
2018-08-08 17:21:12.443 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
2018-08-08 17:21:12.444 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call...
at com.github.rholder.retry.Retryer.call(Retryer.java:174)
at com.github.houbb.retry.guava.HelloDemo.main(HelloDemo.java:53)
Caused by: java.lang.RuntimeException
at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:42)
at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:37)
at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)
at com.github.rholder.retry.Retryer.call(Retryer.java:160)
... 1 more
复制代码
- 重试策略
重试次数:3
重试策略:固定等待 3S
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(callable);
} catch (RetryException | ExecutionException e) {
e.printStackTrace();
}
2018-08-08 17:20:41.653 INFO [main] com.github.houbb.retry.guava.ExponentialBackoff:43 - call...
2018-08-08 17:20:44.659 INFO [main] com.github.houbb.retry.guava.ExponentialBackoff:43 - call...
2018-08-08 17:20:47.664 INFO [main] com.github.houbb.retry.guava.ExponentialBackoff:43 - call...
com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
at com.github.rholder.retry.Retryer.call(Retryer.java:174)
at com.github.houbb.retry.guava.ExponentialBackoff.main(ExponentialBackoff.java:56)
Caused by: java.lang.RuntimeException
at com.github.houbb.retry.guava.ExponentialBackoff$1.call(ExponentialBackoff.java:44)
at com.github.houbb.retry.guava.ExponentialBackoff$1.call(ExponentialBackoff.java:39)
at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)
at com.github.rholder.retry.Retryer.call(Retryer.java:160)
... 1 more
复制代码
1、guava-retrying 简介
1、RetryerBuilder
RetryerBuilder 是一个 factory 创建者,可以定制设置重试源且可以支持多个重试源,可以配置重试次数或重试超时时间,以及可以配置等待时间间隔,创建重试者 Retryer 实例。
RetryerBuilder 的重试源支持 Exception 异常对象和自定义断言对象,通过retryIfException 和 retryIfResult 设置,同时支持多个且能兼容。
1、retryIfException
retryIfException,抛出 runtime 异常、checked 异常时都会重试,但是抛出 error 不会重试。
2、retryIfRuntimeException
etryIfRuntimeException 只会在抛 runtime 异常的时候才重试,checked 异常和error 都不重试。
3、retryIfExceptionOfType
retryIfExceptionOfType 允许我们只在发生特定异常的时候才重试,比如NullPointerException 和 IllegalStateException 都属于 runtime 异常,也包括自定义的error。
retryIfExceptionOfType(Error.class)// 只在抛出error重试
当然我们还可以在只有出现指定的异常的时候才重试,如:
.retryIfExceptionOfType(IllegalStateException.class)
.retryIfExceptionOfType(NullPointerException.class)
或者通过Predicate实现
.retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),
Predicates.instanceOf(IllegalStateException.class)))
4、 retryIfResult
retryIfResult 可以指定你的 Callable 方法在返回值的时候进行重试,如
// 返回false重试
.retryIfResult(Predicates.equalTo(false))
//以_error结尾才重试
.retryIfResult(Predicates.containsPattern("_error$"))
5、RetryListener
当发生重试之后,假如我们需要做一些额外的处理动作,比如log一下异常,那么可以使用RetryListener。
每次重试之后,guava-retrying 会自动回调我们注册的监听。
可以注册多个RetryListener,会按照注册顺序依次调用。
.withRetryListener(new RetryListener {
@Override
public <T> void onRetry(Attempt<T> attempt) {
logger.error("第【{}】次调用失败" , attempt.getAttemptNumber());
}
}
)
2、主要接口
1、StopStrategy
提供三种:
1、StopAfterDelayStrategy
设定一个最长允许的执行时间;比如设定最长执行10s,无论任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException;
2、NeverStopStrategy
不停止,用于需要一直轮训知道返回期望结果的情况;
3、StopAfterAttemptStrategy
设定最大重试次数,如果超出最大重试次数则停止重试,并返回重试异常
2、WaitStrategy
1、FixedWaitStrategy
固定等待时长策略;
2、RandomWaitStrategy
随机等待时长策略(可以提供一个最小和最大时长,等待时长为其区间随机值)
3、IncrementingWaitStrategy
递增等待时长策略(提供一个初始值和步长,等待时间随重试次数增加而增加)
4、ExponentialWaitStrategy
指数等待时长策略;
5、FibonacciWaitStrategy
Fibonacci 等待时长策略;
6、ExceptionWaitStrategy
异常时长等待策略;
7、CompositeWaitStrategy
复合时长等待策略;
七、spring-retry 版本
Spring Retry 为 Spring 应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。
在分布式系统中,为了保证数据分布式事务的强一致性,大家在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作。 大家用的最多的重试方式就是MQ了,但是如果你的项目中没有引入MQ,那就不方便了。
还有一种方式,是开发者自己编写重试机制,但是大多不够优雅
1、注解式使用
重试条件:遇到 RuntimeException
重试次数:3
重试策略:重试的时候等待 5S, 后面时间依次变为原来的 2 倍数
熔断机制:全部重试失败,则调用 recover() 方法。
启动类添加注解:@EnableRetry
@Service
public class RemoteService {
private static final Logger LOGGER = LoggerFactory.getLogger(RemoteService.class);
/**
* 调用方法
*/
@Retryable(value = RuntimeException.class,
maxAttempts = 3,
backoff = @Backoff(delay = 5000L, multiplier = 2))
public void call() {
LOGGER.info("Call something...");
throw new RuntimeException("RPC调用异常");
}
/**
* recover 机制
* @param e 异常
*/
@Recover
public void recover(RuntimeException e) {
LOGGER.info("Start do recover things....");
LOGGER.warn("We meet ex: ", e);
}
}
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
public class RemoteServiceTest {
@Autowired
private RemoteService remoteService;
@Test
public void test() {
remoteService.call();
}
}
2018-08-08 16:03:26.409 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:31.414 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:41.416 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Call something...
2018-08-08 16:03:41.418 INFO 1433 --- [ main] c.g.h.r.spring.service.RemoteService : Start do recover things....
2018-08-08 16:03:41.425 WARN 1433 --- [ main] c.g.h.r.spring.service.RemoteService : We meet ex:
java.lang.RuntimeException: RPC调用异常
at com.github.houbb.retry.spring.service.RemoteService.call(RemoteService.java:38) ~[classes/:na]
...
2、缺陷
spring-retry 工具虽能优雅实现重试,但是存在两个不友好设计:
一个是重试实体限定为 Throwable 子类,说明重试针对的是可捕捉的功能异常为设计前提的,但是我们希望依赖某个数据对象实体作为重试实体, 但 sping-retry框架必须强制转换为Throwable子类。
另一个就是重试根源的断言对象使用的是 doWithRetry 的 Exception 异常实例,不符合正常内部断言的返回设计。
Spring Retry 提倡以注解的方式对方法进行重试,重试逻辑是同步执行的,重试的“失败”针对的是Throwable, 如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。
@Recover 注解在使用时无法指定方法,如果一个类中多个重试方法,就会很麻烦。
3、注解介绍
1、@EnableRetry
表示是否开启重试。
2、@Retryable
标注此注解的方法在发生异常时会进行重试
3、@Backoff
4、@Recover
用于恢复处理程序的方法调用的注释。一个合适的复苏handler有一个类型为可投掷(或可投掷的子类型)的第一个参数 和返回与@Retryable方法相同的类型的值。 可抛出的第一个参数是可选的(但是没有它的方法只会被调用)。 从失败方法的参数列表按顺序填充后续的参数。
4、方法式使用
注解式只是让我们使用更加便捷,但是如果要更高的灵活性。可以使用各种提供的方法。
public class SimpleDemo {
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDemo.class);
public static void main(String[] args) throws Exception {
RetryTemplate template = new RetryTemplate();
// 策略
SimpleRetryPolicy policy = new SimpleRetryPolicy();
policy.setMaxAttempts(2);
template.setRetryPolicy(policy);
String result = template.execute(
new RetryCallback<String, Exception>() {
@Override
public String doWithRetry(RetryContext arg0) {
throw new NullPointerException();
}
}
,
new RecoveryCallback<String>() {
@Override
public String recover(RetryContext context) {
return "recovery callback";
}
}
);
LOGGER.info("result: {}", result);
}
}
16:30:52.578 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=0
16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=1
16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=1
16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=2
16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry failed last attempt: count=2
16:30:52.592 [main] INFO com.github.houbb.retry.spring.commonway.SimpleDemo - result: recovery callback
5、spring-retry 结构
概览
- RetryCallback: 封装你需要重试的业务逻辑(上文中的doSth)
- RecoverCallback:封装在多次重试都失败后你需要执行的业务逻辑(上文中的doSthWhenStillFail)
- RetryContext: 重试语境下的上下文,可用于在多次Retry或者Retry 和Recover之间传递参数或状态(在多次doSth或者doSth与doSthWhenStillFail之间传递参数)
- RetryOperations : 定义了“重试”的基本框架(模板),要求传入RetryCallback,可选传入RecoveryCallback;
- RetryListener:典型的“监听者”,在重试的不同阶段通知“监听者”(例如doSth,wait等阶段时通知)
- RetryPolicy : 重试的策略或条件,可以简单的进行多次重试,可以是指定超时时间进行重试(上文中的someCondition)
- BackOffPolicy: 重试的回退策略,在业务逻辑执行发生异常时。如果需要重试,我们可能需要等一段时间(可能服务器过于繁忙,如果一直不间隔重试可能拖垮服务器), 当然这段时间可以是 0,也可以是固定的,可以是随机的(参见tcp的拥塞控制算法中的回退策略)。回退策略在上文中体现为wait();
- RetryTemplate: RetryOperations的具体实现,组合了RetryListener[],BackOffPolicy,RetryPolicy。
1 、重试策略
-
NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
-
AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
-
SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
-
TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
-
ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
-
CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
-
CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以, 悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行
2、重试回退策略
重试回退策略,指的是每次重试是立即重试还是等待一段时间后重试。
默认情况下是立即重试,如果需要配置等待一段时间后重试则需要指定回退策略BackoffRetryPolicy。
-
NoBackOffPolicy:无退避算法策略,每次重试时立即重试
-
FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒
-
UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒
-
ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier
-
ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数可以实现随机乘数回退