场景
一般我们的项目都是微服务架构,微服务之前的通讯有同步Fegin调用,这种本身就是同步机制;
通过调用的HTTP状态码或者业务返回状态码就可以确定失败或者成功,直接同步处理;
但是有些场景我们一个业务调用对方系统不是返回处理成功或者失败,而是告诉你请求已受理。
这时候获取结果一般有两种方式:主动轮询去查询结果状态,银行业务很常见这种机制
另一种就是被动接受对方系统的回调通知;
如果我们是被调用系统:我们如何保证业务受理之后通知请求方能尽最大可能收到通知呢?最大可能不影响本系统性能呢?
方案
微服务都是内部系统,可以考虑重量级的分布式事务!
不考虑分布式事务,就靠补偿和重试和幂等等机制实现最终一致性!
消息的状态切换有两种方式: 请求接受处理后,第一种是被动轮训查询到最终状态,业务侧自动幂等控制,以及及时解除轮询。第二种就是被动通知可靠:通知采用异步通知,通知失败记录补偿记录表,定时任务调度去完成补偿处理,补偿机制使用阶梯式最大努力策略,设置最大通知次数,防止通知不成功一直死循环式去调度不会成功的业务接口地址。
实现细节
数据库补偿机制表
CREATE TABLE `tx_order_push_info` (
`order_no` varchar(64) DEFAULT NULL COMMENT '交易单号',
`order_id` decimal(11,0) DEFAULT NULL COMMENT '交易单Id',
`push_status` decimal(2,0) DEFAULT NULL COMMENT '推送状态',
`push_count` decimal(2,0) DEFAULT NULL COMMENT '推送次数',
`push_address` varchar(255) DEFAULT NULL COMMENT '推送地址',
`request_body` varchar(1000) DEFAULT NULL COMMENT '推送报文',
`next_push_time` datetime DEFAULT NULL COMMENT '下次通知时间',
`mid` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`create_time` datetime DEFAULT NULL COMMENT '录入时间',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`mid`),
KEY `tx_order_push_info_order_no_IDX` (`order_no`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=406 DEFAULT CHARSET=utf8mb4 COMMENT='推送记录';
代码实现XXL定时任务具体处理策略
@XxlJob("retryPayNotifyExceptionMsg")
public ReturnT<String> retryPayNotifyExceptionMsg(String params) {
String[] timeSplit = params.split("/");
// 当前服务器索引
int shardIndex = XxlJobContext.getXxlJobContext().getShardIndex();
// 总服务器数
int shardTotal = XxlJobContext.getXxlJobContext().getShardTotal();
OrderPushInfo orderPushInfo = new OrderPushInfo();
orderPushInfo.setPushStatus(PushStatusEnum.InitLoading.getCode());
// 分页参数
PageParameter pageParameter = new PageParameter();
// 本次任务需要处理的数据等于默认分页大小*调度任务服务器数量
pageParameter.setPagesize(20);
pageParameter.setCurrentPage(1);
List<OrderPushInfo> orderPushInfos = orderPushInfoService.listByPage(orderPushInfo, pageParameter);
if (CollectionUtils.isNotEmpty(orderPushInfos)) {
for (OrderPushInfo pushInfo : orderPushInfos) {
// 分片处理
if (shardIndex != (pushInfo.getMid() % shardTotal)) {
continue;
}
handlerRetrySendMsg(pushInfo, timeSplit);
}
}
return ReturnT.SUCCESS;
}
private void handlerRetrySendMsg(OrderPushInfo pushInfo, String[] timeSplit) {
Integer pushCount = pushInfo.getPushCount();
if (pushCount >= timeSplit.length) {
// 大于最大通知次数---丢弃
pushInfo.setPushStatus(PushStatusEnum.MaxRetryGiveUp.getCode());
orderPushInfoService.update(pushInfo);
return;
}
// 发送时间是否符合预期
Date nextPushTime = pushInfo.getNextPushTime();
if (nextPushTime.before(new Date())) {
// 重新发送消息
pushCount++;
try {
Map<String, String> header = new HashMap<>();
header.put("token", PBECoder.encrypt(token));
HttpUtil.doPost(pushInfo.getPushAddress(), pushInfo.getRequestBody(), header);
pushInfo.setPushStatus(PushStatusEnum.Success.getCode());
} catch (Exception e) {
log.error(pushInfo.getOrderNo() + "消息通知失败重试---次数" + pushCount);
}
pushInfo.setNextPushTime(DateUtil.addDateSecond(new Date(), Integer.parseInt(timeSplit[pushCount - 1])));
pushInfo.setPushCount(pushCount);
orderPushInfoService.update(pushInfo);
}
}
最终分析
xxl-job在执行定时任务的时候会丢入参数:10/30/60/300/600/1200/1800/3600 阶梯式通知的时间间隔就和他有关系
定时任务还是正常的固定每段时间间隔去执行,但是查询需要执行的数据会有当前时间是否大于下次执行时间的条件过滤,只有满足的数据才会被取出来执行。不满足的就不执行
补偿记录表越来越大怎么办:定期删除已经补偿成功的数据