做另一棵树
2023/05/03阅读:16主题:萌绿
《SpringBoot中间件设计与实战》第2章 服务治理,超时熔断
需求背景
在流量较大的场景下,举个例子,用户在电商平台下单后开始跳转到在线收银台进行支付。由于支付渠道和网络环境随时都有可能发生问题,那么你该怎么保证支付系统的可靠性呢?
保证支付系统的可靠性需要考虑的点非常多,但这里有一个最直接和重点的内容就支付响应时长,如果支付时间过长,那么暴增的支付请求可能会把整个服务拖垮,最终导致所有服务瘫痪。
这时你可能会想到一个功能组件,超时熔断hystrix。这也是大多数支付系统中必用的组件,但怎么用呢,我们是在所有的接口上都加一个这样的功能组件吗?显然这样做是不合适的,一般类似这样的组件可能会嵌入到你的RPC接口或者自研的网关上,也可能是在整个服务治理层的功能编排上。总之,它不会轻易的暴漏给你,让你硬编码到业务逻辑实现中。
那么,本章我们就抽丝剥茧把组件包装使用的最核心实现方式展示给你。记住任何实现方案都是以当前系统环境最适合方式设计的,并不一定非得拘泥于某种形式。
方案设计
面对复杂的场景问题,市面上基本上都有应对的方案。。就像我们本章节所需要的调用超时要熔断保护系统,就有相应的技术组件hystrix,它是 Netflix公司开源的一款容错框架,在大部分RPC服务中也都有引入使用。
那如果我们只是想方便、简单并且不需要关心如何创建和返回结果的使用这样一个服务,就可以把hystrix的框架包装在中间里,屏蔽调用逻辑。整体的设计方案如图4-1超时熔断中间件框架设计。

大致思路如下:
-
使用自定义注解和切面技术,拦截需要被熔断保护的方法。 -
拦截后到方法后,就可以通过hystrix给方法设定已配置好的超时熔断处理。
技术实现
工程结构如下:

工程源码地址
https://gitee.com/lldlld/spring-boot-middleware-demo/tree/master/02-熔断
熔断类直接的UML关系图如下

在这个中间件的实现中也没有额外的yml配置,仅是对切面的运用和hystrix的包装,包含如下信息:
-
1.DoHystrix自定义注解中需要配置方法的超时时长和拦截后的返回信息。 -
2.DoHystrixPoint自定义切面的实现比较简单,只是拦截方法和处理相应的操作。 -
3.HystrixValvelmpl是熔断的具体实现,这里之所以添加接口的实现方式,是为了可以扩展更多的插件包服务。
自定义注解
/**
* 类描述:
* 博客 :https://blog.csdn.net/weixin_42329623
* 公众号 安前码后
* @author LiLiDong
* @date 2023/5/3 15:03
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DoHystrix {
/**
* 失败结果
*/
String returnJson() default "";
/**
* 超时熔断
*/
int timeoutValue() default 0;
}
-
@Retention(RetentionPolicy.RUNTIME)标记注解在JVM运行时可见 -
@Target(ElementType.METHOD)标记注解的作用域在方法层面ElementType.METHOD -
returnJson,熔断保护下返回的结果 -
timeoutValue,方法的调用安全范围时长,一般指的是 TP99、TP999的调用稳定值
熔断实现
/**
* 类描述: 服务熔断包装
* 博客 :https://blog.csdn.net/weixin_42329623
* 公众号 安前码后
* @author LiLiDong
* @date 2023/5/3 15:03
*/
public class HystrixValveImpl extends HystrixCommand<Object> implements IValveService {
private ProceedingJoinPoint jp;
private Method method;
private DoHystrix doHystrix;
public HystrixValveImpl() {
/**
* 置HystrixCommand的属性
* GroupKey: 该命令属于哪一个组,可以帮助我们更好的组织命令。
* CommandKey: 该命令的名称
* ThreadPoolKey: 该命令所属线程池的名称,同样配置的命令会共享同一线程池,若不配置,会默认使用GroupKey作为线程池名称。
* CommandProperties: 该命令的一些设置,包括断路器的配置,隔离策略,降级设置,以及一些监控指标等。
* ThreadPoolProperties:关于线程池的配置,包括线程池大小,排队队列的大小等
*/
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GovernGroup"))
.andCommandKey(HystrixCommandKey.Factory.asKey("GovernKey"))
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("GovernThreadPool"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionIsolationStrategy(HystrixCommandProperties.ExecutionIsolationStrategy.THREAD))
.andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter().withCoreSize(10))
);
}
@Override
public Object access(ProceedingJoinPoint jp, Method method, DoHystrix doHystrix, Object[] args) throws Throwable {
this.jp = jp;
this.method = method;
this.doHystrix = doHystrix;
// 设置熔断超时时间
Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("GovernGroup"))
.andCommandPropertiesDefaults(HystrixCommandProperties.Setter()
.withExecutionTimeoutInMilliseconds(doHystrix.timeoutValue()));
return this.execute();
}
@Override
protected Object run() throws Exception {
try {
return jp.proceed();
} catch (Throwable throwable) {
return null;
}
}
@Override
protected Object getFallback() {
return JSON.parseObject(doHystrix.returnJson(), method.getReturnType());
}
}
-
本文主要是关于中间件对现有解决方案组件的包装,如果对熔断保护的内容感兴趣可以深入学习Hystrix -
HystrixValvelmpl主要是对Hystrix熔断保护的封装,通过继承HystrixCommand构造函数中配置启动参数 -
在切面接口方法调用中,设置超时熔断时间,withExecutionTimeoutInMilliseconds(doHystrix.timeoutValue())这个时间信息就是自定义注解中的配置信息 -
另外这里有一个抽象方法run()和重写的方法 getFallback(),它们分别是返回正确方法调用结果和熔断保护时候返回的对象信息。
切面逻辑实现
/**
* 类描述:
* 博客 :https://blog.csdn.net/weixin_42329623
* 公众号搜索:安前码后
* @author LiLiDong
* @date 2023/5/3 15:03
*/
@Aspect
@Component
public class DoHystrixPoint {
@Pointcut("@annotation(com.aqmh.middleware.hystrix.annotation.DoHystrix)")
public void aopPoint() {
}
@Around("aopPoint() && @annotation(doGovern)")
public Object doRouter(ProceedingJoinPoint jp, DoHystrix doGovern) throws Throwable {
IValveService valveService = new HystrixValveImpl();
return valveService.access(jp, getMethod(jp), doGovern, jp.getArgs());
}
private Method getMethod(JoinPoint jp) throws NoSuchMethodException {
Signature sig = jp.getSignature();
MethodSignature methodSignature = (MethodSignature) sig;
return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes());
}
}
-
@Aspect定义切面、@Component定义组件以求被实例化、@Pointcut定义切点,这些与我们之前的内容一样。 -
·@Around("aopPoint()&&@annotation(doGovern)"),这块的处理是本章节新增的,一般在方法入参中并没有直接提供自定义注解的获取,而是通过类和方法再反向找出来。而直接通过方法入参的方式可以更加方便的拿到注解,处理起来也更优雅。 -
接下来就是方法内容的调用逻辑,valveService.access(ip, getMethod(jp), doGovern,jp.getArgs());,这步调用的就是我们包装好的熔断保护服务。
测试验证
项目结构如下:

源码地址
https://gitee.com/lldlld/spring-boot-middleware-demo/tree/master/02-%E7%86%94%E6%96%AD/aqmh-hystrix-spring-boot-starter-test
引入依赖
<!-- 服务治理,熔断 -->
<dependency>
<groupId>com.aqmh</groupId>
<artifactId>aqmh-hystrix-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
接口使用中间件定义的注解

测试结果
情景一,执行事件1000ms返回的结果:


情景二,执行时间为250ms返回的结果:


总结
-
本章节主要体现的对已有组件的使用封装,通过中间件的设计屏蔽掉底层应用的复杂性,也让整个功能服务更加符合自己的业务特性,同时可以让使用此功能的研发不会过多的参与到插件的使用中,把更多的关心放在业务逻辑开发中。 -
类似这样的中间件使用并没有涉及到SpringBoot 的加载和配置以及 Bean 的处理,这看上去更加简单即使是运用在 Spring 中也可以非常方便的迁移过去。其实大多数依赖SpringBoot开发的starter都是对 Spring 原有组件的包装,后面我们会涉及到 -
·技术的使用更多的实际场景的考虑和运用,并不非得拘泥于某一种实现形式。在学习的过程中也可以把各项你需要的功能不断的扩展进去,体会依赖实际场景开发的具体实现结果。这样可以让你学到更多。
作者介绍