作者:陈昌浩
1 背景
随着数据量的增长,发现零碎在与其余零碎交互时,批量接口会呈现超时景象,发现原批量接口在实现时,没有做分片解决,当数据过大时或超过其余零碎阈值时,就会呈现谬误。因为与其余零碎交互比拟多,一个一个接口做分片优化,改变量较大,所以思考通过 AOP 解决此问题。
2 Spring-AOP
AOP(Aspect Orient Programming), 直译过去就是 面向切面编程。AOP 是一种编程思维,是面向对象编程(OOP)的一种补充。面向对象编程将程序形象成各个档次的对象,而面向切面编程是将程序形象成各个切面。
Spring 中的 AOP 是通过动静代理实现的。Spring AOP 不能拦挡对对象字段的批改,也不反对结构器连接点, 咱们无奈在 Bean 创立时利用告诉。
3 性能实现
自定义分片解决分三个局部:自定义注解(MethodPartAndRetryer)、重试器(RetryUtil)、切面实现(RetryAspectAop)。
3.1 MethodPartAndRetryer
源码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MethodPartAndRetryer {
/**
* 失败重试次数
* @return
*/
int times() default 3;
/**
* 失败距离执行工夫 300 毫秒
* @return
*/
long waitTime() default 300L;
/**
* 分片大小
* @return
*/
int parts() default 200;}
@interface 阐明这个类是个注解。
@Target 是这个注解的作用域
public enum ElementType {/** 类、接口 (包含正文类型) 或枚举申明 */
TYPE,
/** 字段申明(包含枚举常量) */
FIELD,
/** 办法申明 */
METHOD,
/** 正式的参数申明 */
PARAMETER,
/** 构造函数申明 */
CONSTRUCTOR,
/** 局部变量申明 */
LOCAL_VARIABLE,
/** 正文类型申明 */
ANNOTATION_TYPE,
/** 程序包申明 */
PACKAGE,
/** 类型参数申明 */
TYPE_PARAMETER,
/** 类型的应用 */
TYPE_USE
}
@Retention 注解的生命周期
public enum RetentionPolicy {
/** 编译器解决完后不存储在 class 中 */
SOURCE,
/** 正文将被编译器记录在类文件中,但不须要在运行时被 VM 保留。这是默认值 */
CLASS,
/** 编译器存储在 class 中,能够由虚拟机读取 */
RUNTIME
}
- times():接口调用失败时,重试的次数。
- waitTime(): 接口调用失败是,距离多长时间再次调用。
- int parts():进行分片时,每个分片的大小。
3.2 RetryUtil
源码
public class RetryUtil<V> {public Retryer<V> getDefaultRetryer(int times,long waitTime) {Retryer<V> retryer = RetryerBuilder.<V>newBuilder()
.retryIfException()
.retryIfRuntimeException()
.retryIfExceptionOfType(Exception.class)
.withWaitStrategy(WaitStrategies.fixedWait(waitTime, TimeUnit.MILLISECONDS))
.withStopStrategy(StopStrategies.stopAfterAttempt(times))
.build();
return retryer;
}
}
阐明
- RetryerBuilder:是用于配置和创立 Retryer 的构建器。
- retryIfException:抛出 runtime 异样、checked 异样时都会重试,然而抛出 error 不会重试。
- retryIfRuntimeException:只会在抛 runtime 异样的时候才重试,checked 异样和 error 都不重试。
- retryIfExceptionOfType:容许咱们只在产生特定异样的时候才重试。
- withWaitStrategy: 期待策略,每次申请距离。
- withStopStrategy: 进行策略,重试多少次后进行。
3.3 RetryAspectAop
源码:
public class RetryAspectAop {public Object around(final ProceedingJoinPoint point) throws Throwable {
Object result = null;
final Object[] args = point.getArgs();
boolean isHandler1 = isHandler(args);
if (isHandler1) {String className = point.getSignature().getDeclaringTypeName();
String methodName = point.getSignature().getName();
Object firstArg = args[0];
List<Object> paramList = (List<Object>) firstArg;
// 获取办法信息
Method method = getCurrentMethod(point);
// 获取注解信息
MethodPartAndRetryer retryable = AnnotationUtils.getAnnotation(method, MethodPartAndRetryer.class);
// 重试机制
Retryer<Object> retryer = new RetryUtil<Object>().getDefaultRetryer(retryable.times(),retryable.waitTime());
// 分片
List<List<Object>> requestList = Lists.partition(paramList, retryable.parts());
for (List<Object> partList : requestList) {args[0] = partList;
Object tempResult = retryer.call(new Callable<Object>() {
@Override
public Object call() throws Exception {
try {return point.proceed(args);
} catch (Throwable throwable) {log.error(String.format("分片重试报错, 类 %s- 办法 %s",className,methodName),throwable);
throw new RuntimeException("分片重试出错");
}
}
});
if (null != tempResult) {if (tempResult instanceof Boolean) {if (!((Boolean) tempResult)) {log.error(String.format("分片执行报错返回类型不能转化 bolean, 类 %s- 办法 %s",className,methodName));
throw new RuntimeException("分片执行报错!");
}
result = tempResult;
} else if (tempResult instanceof List) {if(result ==null){result =Lists.newArrayList();
}
((List) result).addAll((List) tempResult);
}else {log.error(String.format("分片执行返回的类型不反对, 类 %s- 办法 %s",className,methodName));
throw new RuntimeException("不反对该返回类型");
}
} else {log.error(String.format("分片执行返回的后果为空, 类 %s- 办法 %s",className,methodName));
throw new RuntimeException("调用后果为空");
}
}
} else {result = point.proceed(args);
}
return result;
}
private boolean isHandler(Object[] args) {
boolean isHandler = false;
if (null != args && args.length > 0) {Object firstArg = args[0];
// 如果第一个参数是 list 并且数量大于 1
if (firstArg!=null&&firstArg instanceof List &&((List) firstArg).size()>1) {isHandler = true;}
}
return isHandler;
}
private Method getCurrentMethod(ProceedingJoinPoint point) {
try {Signature sig = point.getSignature();
MethodSignature msig = (MethodSignature) sig;
Object target = point.getTarget();
return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
} catch (NoSuchMethodException e) {throw new RuntimeException(e);
}
}
}
阐明:
getCurrentMethod:获取办法信息即要做分片的批量调用的接口。
isHandler1:判断是否要做分片解决,只有第一参数是 list 并且 list 的值大于 1 时才做分片解决。
around:具体分片逻辑。
- 获取要分片办法的参数。
- 判断是否要做分片解决。
- 获取办法。
- 获取重试次数、重试间隔时间和分片大小。
- 生成重试器。
- 依据设置的分片大小,做分片解决。
- 调用批量接口并处理结果。
4 性能应用
4.1 配置文件
4.2 代码示例
@MethodPartAndRetryer(parts=100)
public Boolean writeBackOfGoodsSN(List<SerialDTO> listSerial,ObCheckWorker workerData)
只有在须要做分片的批量接口办法上,加上 MethodPartAndRetryer 注解就能够,重试次数、重试间隔时间和分片大小能够在注解时设置,也能够应用默认值。
5 小结
通过自定义分片工具,能够疾速的对老代码进行分片解决,而且减少了重试机制,进步了程序的可用性,进步了对老代码的重构效率。