Spring Boot实现第一次启动时自动初始化数据库流程详解

随着互联网的发展项目中的业务功能越来越复杂,有一些基础服务我们不可避免的会去调用一些第三方的接口或者公司内其他项目中提供的服务,但是远程服务的健壮性和网络稳定性都是不可控因素。

在测试阶段可能没有什么异常情况,但上线后可能会出现调用的接口因为内部错误或者网络波动而出错或返回系统异常,因此我们必须考虑加上重试机制

重试机制 可以提高系统的健壮性,并且减少因网络波动依赖服务临时不可用带来的影响,让系统能更稳定的运行

1. 手动重试

手动重试:使用 while 语句进行重试:

@Service
public class OrderServiceImpl implements OrderService {public void addOrder() {int times = 1;while (times <= 5) {try {// 故意抛异常int i = 3 / 0;// addOrder} catch (Exception e) {System.out.println("重试" + times + "次");Thread.sleep(2000);times++;if (times > 5) {throw new RuntimeException("不再重试!");}}}}
}

运行上述代码:

图片

上述代码看上去可以解决重试问题,但实际上存在一些弊端:

  1. 由于没有重试间隔,很可能远程调用的服务还没有从网络异常中恢复,所以有可能接下来的几次调用都会失败
  2. 代码侵入式太高,调用方代码不够优雅
  3. 项目中远程调用的服务可能有很多,每个都去添加重试会出现大量的重复代码

2. 静态代理

上面的处理方式由于需要对业务代码进行大量修改,虽然实现了功能,但是对原有代码的侵入性太强,可维护性差。所以需要使用一种更优雅一点的方式,不直接修改业务代码,那要怎么做呢?

其实很简单,直接在业务代码的外面再包一层就行了,代理模式在这里就有用武之地了。

@Service
public class OrderServiceProxyImpl implements OrderService {@Autowiredprivate OrderServiceImpl orderService;@Overridepublic void addOrder() {int times = 1;while (times <= 5) {try {// 故意抛异常int i = 3 / 0;orderService.addOrder();} catch (Exception e) {System.out.println("重试" + times + "次");try {Thread.sleep(2000);} catch (InterruptedException ex) {ex.printStackTrace();}times++;if (times > 5) {throw new RuntimeException("不再重试!");}}}}
}

这样,重试逻辑就都由代理类来完成,原业务类的逻辑就不需要修改了,以后想修改重试逻辑也只需要修改这个类就行了

代理模式虽然要更加优雅,但是如果依赖的服务很多的时候,要为每个服务都创建一个代理类,显然过于麻烦,而且其实重试的逻辑都大同小异,无非就是重试的次数和延时不一样而已。如果每个类都写这么一长串类似的代码,显然,不优雅!

3. JDK 动态代理

这时候,动态代理就闪亮登场了。只需要写一个代理处理类就 ok 了

public class RetryInvocationHandler implements InvocationHandler {private final Object subject;public RetryInvocationHandler(Object subject) {this.subject = subject;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {int times = 1;while (times <= 5) {try {// 故意抛异常int i = 3 / 0;return method.invoke(subject, args);} catch (Exception e) {System.out.println("重试【" + times + "】次");try {Thread.sleep(2000);} catch (InterruptedException ex) {ex.printStackTrace();}times++;if (times > 5) {throw new RuntimeException("不再重试!");}}}return null;}public static Object getProxy(Object realSubject) {InvocationHandler handler = new RetryInvocationHandler(realSubject);return Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler);}}

测试:

@RestController
@RequestMapping("/order")
public class OrderController {@Qualifier("orderServiceImpl")@Autowiredprivate OrderService orderService;@GetMapping("/addOrder")public String addOrder() {OrderService orderServiceProxy = (OrderService)RetryInvocationHandler.getProxy(orderService);orderServiceProxy.addOrder();return "addOrder";}}

动态代理可以将重试逻辑都放到一块,显然比直接使用代理类要方便很多,也更加优雅。

这里使用的是JDK动态代理,因此就存在一个天然的缺陷,如果想要被代理的类,没有实现任何接口,那么就无法为其创建代理对象,这种方式就行不通了

4. CGLib 动态代理

既然已经说到了 JDK 动态代理,那就不得不提 CGLib 动态代理了。使用 JDK 动态代理对被代理的类有要求,不是所有的类都能被代理,而 CGLib 动态代理则刚好解决了这个问题

@Component
public class CGLibRetryProxyHandler implements MethodInterceptor {private Object target;@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {int times = 1;while (times <= 5) {try {// 故意抛异常int i = 3 / 0;return method.invoke(target, objects);} catch (Exception e) {System.out.println("重试【" + times + "】次");try {Thread.sleep(2000);} catch (InterruptedException ex) {ex.printStackTrace();}times++;if (times > 5) {throw new RuntimeException("不再重试!");}}}return null;}public Object getCglibProxy(Object objectTarget){this.target = objectTarget;Enhancer enhancer = new Enhancer();enhancer.setSuperclass(objectTarget.getClass());enhancer.setCallback(this);Object result = enhancer.create();return result;}}

测试:

@GetMapping("/addOrder")
public String addOrder() {OrderService orderServiceProxy = (OrderService) cgLibRetryProxyHandler.getCglibProxy(orderService);orderServiceProxy.addOrder();return "addOrder";
}

这样就很棒了,完美的解决了 JDK 动态代理带来的缺陷。优雅指数上涨了不少。

但这个方案仍旧存在一个问题,那就是需要对原来的逻辑进行侵入式修改,在每个被代理实例被调用的地方都需要进行调整,这样仍然会对原有代码带来较多修改

5. 手动 Aop

考虑到以后可能会有很多的方法也需要重试功能,咱们可以将重试这个共性功能通过 AOP 来实现:使用 AOP 来为目标调用设置切面,即可在目标方法调用前后添加一些重试的逻辑

<dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId>
</dependency>

自定义注解:

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyRetryable {// 最大重试次数int retryTimes() default 3;// 重试间隔int retryInterval() default 1;}
@Slf4j
@Aspect
@Component
public class RetryAspect {@Pointcut("@annotation(com.hcr.sbes.retry.annotation.MyRetryable)")private void retryMethodCall(){}@Around("retryMethodCall()")public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {// 获取重试次数和重试间隔MyRetryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(MyRetryable.class);int maxRetryTimes = retry.retryTimes();int retryInterval = retry.retryInterval();Throwable error = new RuntimeException();for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){try {Object result = joinPoint.proceed();return result;} catch (Throwable throwable) {error = throwable;log.warn("调用发生异常,开始重试,retryTimes:{}", retryTimes);}Thread.sleep(retryInterval * 1000L);}throw new RuntimeException("重试次数耗尽", error);}}

给需要重试的方法添加注解 @MyRetryable

@Service
public class OrderServiceImpl implements OrderService {@Override@MyRetryable(retryTimes = 5, retryInterval = 2)public void addOrder() {int i = 3 / 0;// addOrder}}

这样即不用编写重复代码,实现上也比较优雅了:一个注解就实现重试。

6. spring-retry

<dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId>
</dependency>

开启重试功能:在启动类或者配置类上添加 @EnableRetry 注解

在需要重试的方法上添加 @Retryable 注解

@Slf4j
@Service
public class OrderServiceImpl implements OrderService {@Override@Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2))public void addOrder() {System.out.println("重试...");int i = 3 / 0;// addOrder}@Recoverpublic void recover(RuntimeException e) {log.error("达到最大重试次数", e);}}

该方法调用后会进行重试,最大重试次数为 3,第一次重试间隔为 2s,之后以 2 倍大小进行递增,第二次重试间隔为 4 s,第三次为 8s

Spring 的重试机制还支持很多很有用的特性,由三个注解完成:

@Retryable
@Backoff
@Recover

查看 @Retryable 注解源码:指定异常重试、次数

public @interface Retryable {// 设置重试拦截器的 bean 名称String interceptor() default "";// 只对特定类型的异常进行重试。默认:所有异常Class<? extends Throwable>[] value() default {};// 包含或者排除哪些异常进行重试Class<? extends Throwable>[] include() default {};Class<? extends Throwable>[] exclude() default {};// l设置该重试的唯一标志,用于统计输出String label() default "";boolean stateful() default false;// 最大重试次数,默认为 3 次int maxAttempts() default 3;String maxAttemptsExpression() default "";// 设置重试补偿机制,可以设置重试间隔,并且支持设置重试延迟倍数Backoff backoff() default @Backoff;// 异常表达式,在抛出异常后执行,以判断后续是否进行重试String exceptionExpression() default "";String[] listeners() default {};
}

@Backoff 注解: 指定重试回退策略(如果因为网络波动导致调用失败,立即重试可能还是会失败,最优选择是等待一小会儿再重试。决定等待多久之后再重试的方法。通俗的说,就是每次重试是立即重试还是等待一段时间后重试)

@Recover 注解: 进行善后工作:当重试达到指定次数之后,会调用指定的方法来进行日志记录等操作

注意:

@Recover 注解标记的方法必须和被 @Retryable 标记的方法在同一个类中
重试方法抛出的异常类型需要与 recover()方法参数类型保持一致
recover() 方法返回值需要与重试方法返回值保证一致
recover() 方法中不能再抛出Exception,否则会报无法识别该异常的错误

这里还需要再提醒的一点是,由于 Spring Retry 用到了 Aspect 增强,所以就会有使用 Aspect 不可避免的坑——方法内部调用,如果被 @Retryable 注解的方法的调用方和被调用方处于同一个类中,那么重试将会失效

通过以上几个简单的配置,可以看到 Spring Retry 重试机制考虑的比较完善,比自己写AOP实现要强大很多

弊端:
但也还是存在一定的不足,Spring的重试机制只支持对 异常 进行捕获,而无法对返回值进行校验

@Retryable
public String hello() {long current = count.incrementAndGet();System.out.println("第" + current +"次被调用");if (current % 3 != 0) {log.warn("调用失败");return "error";}return "success";
}

因此就算在方法上添加 @Retryable,也无法实现失败重试

除了使用注解外,Spring Retry 也支持直接在调用时使用代码进行重试:

@Test
public void normalSpringRetry() {// 表示哪些异常需要重试,key表示异常的字节码,value为true表示需要重试Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();exceptionMap.put(HelloRetryException.class, true);// 构建重试模板实例RetryTemplate retryTemplate = new RetryTemplate();// 设置重试回退操作策略,主要设置重试间隔时间FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();long fixedPeriodTime = 1000L;backOffPolicy.setBackOffPeriod(fixedPeriodTime);// 设置重试策略,主要设置重试次数int maxRetryTimes = 3;SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);retryTemplate.setRetryPolicy(retryPolicy);retryTemplate.setBackOffPolicy(backOffPolicy);Boolean execute = retryTemplate.execute(//RetryCallbackretryContext -> {String hello = helloService.hello();log.info("调用的结果:{}", hello);return true;},// RecoverCallBackretryContext -> {//RecoveryCallbacklog.info("已达到最大重试次数");return false;});
}

此时唯一的好处是可以设置多种重试策略:

NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

7. guava-retry

和 Spring Retry 相比,Guava Retry 具有更强的灵活性,并且能够根据 返回值 来判断是否需要重试

<dependency><groupId>com.github.rholder</groupId><artifactId>guava-retrying</artifactId><version>2.0.0</version>
</dependency>
@Override
public String guavaRetry(Integer num) {Retryer<String> retryer = RetryerBuilder.<String>newBuilder()//无论出现什么异常,都进行重试.retryIfException()//返回结果为 error时,进行重试.retryIfResult(result -> Objects.equals(result, "error"))//重试等待策略:等待 2s 后再进行重试.withWaitStrategy(WaitStrategies.fixedWait(2, TimeUnit.SECONDS))//重试停止策略:重试达到 3 次.withStopStrategy(StopStrategies.stopAfterAttempt(3)).withRetryListener(new RetryListener() {@Overridepublic <V> void onRetry(Attempt<V> attempt) {System.out.println("RetryListener: 第" + attempt.getAttemptNumber() + "次调用");}}).build();try {retryer.call(() -> testGuavaRetry(num));} catch (Exception e) {e.printStackTrace();}return "test";
}

先创建一个Retryer实例,然后使用这个实例对需要重试的方法进行调用,可以通过很多方法来设置重试机制:

retryIfException():对所有异常进行重试
retryIfRuntimeException():设置对指定异常进行重试
retryIfExceptionOfType():对所有 RuntimeException 进行重试
retryIfResult():对不符合预期的返回结果进行重试

还有五个以 withXxx 开头的方法,用来对重试策略/等待策略/阻塞策略/单次任务执行时间限制/自定义监听器进行设置,以实现更加强大的异常处理:

withRetryListener():设置重试监听器,用来执行额外的处理工作
withWaitStrategy():重试等待策略
withStopStrategy():停止重试策略
withAttemptTimeLimiter:设置任务单次执行的时间限制,如果超时则抛出异常
withBlockStrategy():设置任务阻塞策略,即可以设置当前重试完成,下次重试开始前的这段时间做什么事情

总结

从手动重试,到使用 Spring AOP 自己动手实现,再到站在巨人肩上使用特别优秀的开源实现 Spring Retry 和 Google guava-retrying,经过对各种重试实现方式的介绍,可以看到以上几种方式基本上已经满足大部分场景的需要:

如果是基于 Spring 的项目,使用 Spring Retry 的注解方式已经可以解决大部分问题
如果项目没有使用 Spring 相关框架,则适合使用 Google guava-retrying:自成体系,使用起来更加灵活强大

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/40494.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

设计HTML5文本

网页文本内容丰富、形式多样&#xff0c;通过不同的版式显示在页面中&#xff0c;为用户提供最直接、最丰富的信息。HTML5新增了很多文本标签&#xff0c;它们都有特殊的语义&#xff0c;正确使用这些标签&#xff0c;可以让网页文本更严谨、更符合语义。 1、通用文本 1.1、标…

算法竞赛备赛之搜索与图论训练提升,暑期集训营培训

目录 1.DFS和BFS 1.1.DFS深度优先搜索 1.2.BFS广度优先搜索 2.树与图的遍历&#xff1a;拓扑排序 3.最短路 3.1.迪杰斯特拉算法 3.2.贝尔曼算法 3.3.SPFA算法 3.4.多源汇最短路Floy算法 4.最小生成树 4.1.普利姆算法 4.2.克鲁斯卡尔算法 5.二分图&#xff1a;染色法…

7. CSS(四)

目录 一、浮动 &#xff08;一&#xff09;传统网页布局的三种方式 &#xff08;二&#xff09;标准流&#xff08;普通流/文档流&#xff09; &#xff08;三&#xff09;为什么需要浮动&#xff1f; &#xff08;四&#xff09;什么是浮动 &#xff08;五&#xff09;浮…

OpenAI全球招外包大军,手把手训练ChatGPT取代码农 ; 码农:我自己「杀」自己

目录 前言 OpenAI招了一千多名外包人员&#xff0c;训练AI学会像人类一样一步步思考。如果ChatGPT「学成归来」&#xff0c;码农恐怕真的危了&#xff1f; 码农真的危了&#xff01; 当时OpenAI也说&#xff0c;ChatGPT最合适的定位&#xff0c;应该是编码辅助工具。 用Cha…

计算机竞赛 opencv 图像识别 指纹识别 - python

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于机器视觉的指纹识别系统 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;3分工作量&#xff1a;3分创新点&#xff1a;4分 该项目较为新颖&#xff0c;适…

Vue引入Echarts报错 import * as echarts from “echarts“;

项目场景&#xff1a; 已经下载好echarts cnpm i echarts Vue引入Echarts import echarts from echarts mounted() {this.myChart echarts.init(document.querySelector(.right))this.myChart.setOption({title: {text: 消费列表,left: center},...问题描述 原因分析&#…

【100天精通python】Day38:GUI界面编程_PyQT从入门到实战(中)

目录 专栏导读 4 数据库操作 4.1 连接数据库 4.2 执行 SQL 查询和更新&#xff1a; 4.3 使用模型和视图显示数据 5 多线程编程 5.1 多线程编程的概念和优势 5.2 在 PyQt 中使用多线程 5.3 处理多线程间的同步和通信问题 5.3.1 信号槽机制 5.3.2 线程安全的数据访问 Q…

日常BUG——通过命令行创建vue项目报错

&#x1f61c;作 者&#xff1a;是江迪呀✒️本文关键词&#xff1a;日常BUG、BUG、问题分析☀️每日 一言 &#xff1a;存在错误说明你在进步&#xff01; 一、问题描述 在使用vue命令行创建一个vue项目时&#xff0c;出现一下的错误&#xff1a; vue create my…

UDP数据报结构分析(面试重点)

在传输层中有UDP和TCP两个重要的协议&#xff0c;下面将针对UDP数据报的结构进行分析 UDP结构图示 UDP报头结构的分析 UDP报头有4个属性&#xff0c;分别是源端口&#xff0c;目的端口&#xff0c;UDP报文长度&#xff0c;校验和&#xff0c;它们都占16位2个字节&#xff0c;所…

【java面向对象中static关键字】

提纲 static修饰成员变量static修饰成员变量的应用场景static修饰成员方法static修饰成员方法的应用场景static的注意事项static的应用知识&#xff1a;代码块static的应用知识&#xff1a;单例设计模式 static静态的意思&#xff0c;可以修饰成员变量&#xff0c;成员方法&a…

FPGA_学习_14_第一个自写模块的感悟和ila在线调试教程与技巧(寻找APD的击穿偏压)

前一篇博客我们提到了&#xff0c;如果要使用算法找到Vbr&#xff0c;通过寻找APD采集信号的噪声方差的剧变点去寻找Vbr是一个不错的方式。此功能的第一步是在FPGA中实现方差的计算&#xff0c;这个我们已经在上一篇博客中实现了。 继上一篇博客之后&#xff0c;感觉过了很久了…

【Image captioning】ruotianluo/self-critical.pytorch之1—数据集的加载与使用

【Image captioning】ruotianluo/self-critical.pytorch之1—数据集的加载与使用 作者&#xff1a;安静到无声 个人主页 数据加载程序示意图 使用方法 示例代码 #%%from __future__ import absolute_import from __future__ import division from __future__ import print_…

开源,微信小程序 美食便签地图(FoodNoteMap)的设计与开发

目录 0 前言 1 美食便签地图简介 2 美食便签地图小程序端开发 2.1技术选型 2.2前端UI设计 2.3主页界面 2.4个人信息界面 2.5 添加美食界面 2.6美食便签界面 2.8 美食好友界面 2.9 美食圈子界面 2.10 子页面-店铺详情界面 2.11 后台数据缓存 2.12 订阅消息通知 2.1…

探索区块链世界:去中心化应用(DApp)的崭新前景

随着科技的不断发展&#xff0c;区块链技术逐渐引领着数字时代的潮流。在这个充满创新和变革的领域中&#xff0c;去中心化应用&#xff08;DApp&#xff09;成为了备受瞩目的焦点。DApp 不仅改变了传统应用程序的范式&#xff0c;还在金融、社交、游戏等多个领域展现出了广阔的…

GRPC 链接 NODE 和 GOLANG

GRPC 链接 NODE 和 GOLANG GRPC 了解 什么是GRPC gRPC 采用了 Protocol Buffers 作为数据序列化和反序列化的协议&#xff0c;可以更快速地传输数据&#xff0c;并支持多种编程语言的跨平台使用gRPC 提供“统一水平层”来对此类问题进行抽象化。 开发人员在本机平台中编写专…

打造专属照片分享平台:快速上手Piwigo网页搭建

文章目录 通过cpolar分享本地电脑上有趣的照片&#xff1a;部署piwigo网页前言1.Piwigo2. 使用phpstudy网页运行3. 创建网站4. 开始安装Piwogo 总结 &#x1f340;小结&#x1f340; &#x1f389;博客主页&#xff1a;小智_x0___0x_ &#x1f389;欢迎关注&#xff1a;&#x…

深度学习1:通过模型评价指标优化训练

P(Positive)表示预测为正样本&#xff0c;N(negative)表示预测为负样本&#xff0c;T(True)表示预测正确,F(False)表示预测错误。 TP&#xff1a;正样本预测正确的数量&#xff08;正确检测&#xff09; FP&#xff1a;负样本预测正确数量&#xff08;误检测&#xff09; TN…

卷积神经网络CNN

卷积神经网络CNN 1 应用领域1 检测任务2 分类和检索3 超分辨率重构4 医学任务5 无人驾驶6 人脸识别 2 卷积的作用3 卷积特征值计算方法4 得到特征图表示5 步长和卷积核大小对结果的影响1 步长2 卷积核 6 边缘填充方法7 特征图尺寸计算与参数共享8 池化层的作用9 整体网络架构10…

【GitLab私有仓库】如何在Linux上用Gitlab搭建自己的私有库并配置cpolar内网穿透?

文章目录 前言1. 下载Gitlab2. 安装Gitlab3. 启动Gitlab4. 安装cpolar5. 创建隧道配置访问地址6. 固定GitLab访问地址6.1 保留二级子域名6.2 配置二级子域名 7. 测试访问二级子域名 前言 GitLab 是一个用于仓库管理系统的开源项目&#xff0c;使用Git作为代码管理工具&#xf…

软考笔记——10.项目管理

进度管理 进度管理就是采用科学的方法&#xff0c;确定进度目标&#xff0c;编制进度计划和资源供应计划&#xff0c;进行进度控制&#xff0c;在与质量、成本目标协调的基础上&#xff0c;实现工期目标。 具体来说&#xff0c;包括以下过程&#xff1a; (1) 活动定义&#…