在日常开发中,经常需要跟对接第三方,一般场景下,失败了我们会直接把异常抛出,某些业务场景,失败了需要进行重试,而不是直接抛出异常,影响用户体验
1.使用Spring-Retry框架RetryTemplate重试,多次失败则记录,后续补偿
RetryTemplate是Spring自带的一个重试模板,支持模板的方式还有注解的方式,模板的方式比较灵活,暂使用该方式作为例子
- 引入maven依赖
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
- 直接上代码
以下是一个通过http请求调用第三方系统接口的例子
@Slf4j
public class Demo {
private static final String SUCCESS = "S";
private final String regex = "\\$(\\w+)\\$";
private final OAConfig oaConfig;
private final CompensationOperator compensationOperator;
public void receiveTodoRequestByJson(String json) {
/**
* dev,sit,uat OA无对接环境,默认返回0
*/
if (StrUtil.isBlank(oaConfig.getUrl())) {
return;
}
RetryTemplate retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy());//默认重试3次
FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
//fixedBackOffPolicy.setBackOffPeriod(1000 * 60 * 60);//重试时间间隔:1h
retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
retryTemplate.execute(
//普通业务实现,调用第三方系统接口,结果不符合则手动抛出异常
context -> {
String response = HttpUtil.post(oaConfig.getUrl() + "/rest/ofs/ReceiveTodoRequestByJson", json);
Integer status = JSON.parseObject(response).getIntValue("operResult");
if (Objects.isNull(status) || (Objects.nonNull(status) && status != 1)) {
log.error("receiveTodoRequestByJson报错,重试次数:{},请求:{},响应:{}", context.getRetryCount(), json, response);
throw new BizException("500", "receiveTodoRequestByJson报错", "receiveTodoRequestByJson报错");
}
log.info("receiveTodoRequestByJson成功,重试次数:{},请求:{},响应:{}", context.getRetryCount(), json, response);
return status;
},
//多次失败后的兜底实现,记录当前的入参,类名,方法,后续通过补偿机制重试
context -> {
Compensation compensation = new Compensation();
compensation.setJson(json);
compensation.setType(CompensationTypeEnum.OA_RECEIVE_TODO_REQUEST.getCode());
compensation.setClassName(this.getClass().getSimpleName());
StackTraceElement stackTraceElements = Thread.currentThread().getStackTrace()[1];
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(stackTraceElements.getMethodName());
if (matcher.find()) {
compensation.setMethodName(matcher.group(1));
} else {
compensation.setMethodName(stackTraceElements.getMethodName());
}
compensationOperator.save(compensation);
log.error("创建OA统一待办失败", context.getLastThrowable());
throw new BizException("", "", "创建OA统一待办失败");
});
}
}
2.补偿机制的实现
Compensation类
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.io.Serializable;
/**
* 补偿表
*
* @TableName compensation
*/
@TableName(value = "compensation")
@Data
public class Compensation implements Serializable {
@TableField(exist = false)
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 是否删除,0否1是,完成后删除
*/
@TableLogic
private Integer isDelete;
/**
* 补偿类型
*/
private Integer type;
/**
* json字符串,入参
*/
private String json;
/**
* 全限定类名
*/
private String className;
/**
* 方法名
*/
private String methodName;
}
对应DDL
create table compensation
(
id bigint unsigned auto_increment comment '主键'
primary key,
type int(2) unsigned not null comment '补偿类型',
json text null comment 'json字符串,入参',
class_name varchar(255) default '' not null comment '全限定类名',
method_name varchar(255) default '' not null comment '方法名',
is_delete tinyint(1) default 0 not null comment '是否删除,0否1是,完成后删除'
)
comment '补偿表';
补偿类型建议写一个枚举类来描述
3.通过xxl-job调用指定记录来重试实现补偿
可以xxl-job,可以直接搞个http接口,可以自动遍历表,可以手动指定重试,看自己喜欢
下面主要是通过获取上下文中的实例来重试,如果通过反射,newInstance的时候容易出问题
import cn.hutool.extra.spring.SpringUtil;
import com.alibaba.fastjson.JSON;
import com.dsl.promotion.svc.innerpart.entity.Compensation;
import com.dsl.promotion.svc.innerpart.operator.CompensationOperator;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import com.xxl.job.core.util.DateUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
import java.util.stream.Collectors;
/**
* @description
* @date 2023/8/17 11:14
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CompensationTask {
private final CompensationOperator compensationOperator;
private final ApplicationContext applicationContext;
/**
* 人工补偿,传id
*/
@XxlJob("manualCompensation")
public void manualCompensation() {
String param = XxlJobHelper.getJobParam();
XxlJobHelper.log("人工补偿,入参:{},开始时间:{}", param, DateUtil.formatDateTime(new Date()));
try {
Long id = Long.parseLong(param);
Compensation compensation = compensationOperator.getById(id);
String className = compensation.getClassName();
String methodName = compensation.getMethodName();
Method method = Arrays.stream(SpringUtil.getBean(className).getClass().getMethods())
.filter(m -> m.getName().equals(methodName)).collect(Collectors.toList()).get(0);
Object result = ReflectionUtils.invokeMethod(method, SpringUtil.getBean(className), compensation.getJson());
compensationOperator.removeById(id);
XxlJobHelper.log("人工补偿成功,结果:{},结束时间:{}", JSON.toJSONString(result), DateUtil.formatDateTime(new Date()));
} catch (Exception e) {
log.error("人工补偿失败", e);
XxlJobHelper.log("人工补偿失败,异常原因:{}", e.getMessage());
XxlJobHelper.handleFail();
return;
}
}
}
调用job的时候,我们可以把id传进入就行