java计算时间间隔-excel计算间隔天数
1、为什么要引入重试机制
在分布式系统中,在日常的开发和维护中,我们经常会涉及到调用外部接口或者通过RPC访问其他业务系统。 在这个过程中java计算时间间隔,我们经常会遇到这样的问题:被调用的第三方或者对外接口的稳定性有问题,经常会出现请求失败或者超时等现象。 出现这种现象的原因可能是由于网络波动或系统升级维护造成的短暂不可用。 这些都可能导致我们自己的系统处于不稳定的状态。 不仅在测试和产品上会经常被抱怨系统实现问题,还会影响到自身业务的正常运行。
类似这样的场景:发送消息失败、调用远程服务失败、竞争锁失败等;
重试机制可以保护系统免受网络波动和依赖服务暂时不可用的影响,是一种让系统运行更稳定的保护机制。 因此,重试机制在一些必要的业务中是实用有效的。
2.重试机制需要具备的特点 3.重试机制需要考虑的问题
当然,在引入重试机制时,我们需要考虑以下问题:
1.重试几次比较合适
一般来说,单次重试我们面临的结果是非常不确定的,那么需要重试多少次才是最合理的呢? 这个需要根据具体的业务来分析,要看业务的重要性和异常告警分类处理的及时性。 但一般来说,3次重试基本可以满足大部分业务需求。 当然这也需要和重试间隔一起考虑。 为什么说3次基本就够了,因为如果系统长期处于不可用的状态,我们重试多少次是没有意义的,只会增加系统的压力。
2、每次重试间隔多少时间合适?
如果重试间隔设置得太小,可能会造成这样一种情况:我们在被调用系统来不及恢复之前就发起了调用,结果肯定还是失败了,相当于调用的很快,失败了N次。 无实际意义;
如果重试间隔设置过大,可能会造成这样一种情况:牺牲大量数据的时效性;
因此,重试间隔的设置应该根据被调用系统的平均恢复时间来正确估计。 一般来说平均恢复时间没有完整的大数据分析系统是很难统计的,所以这个数值一般需要根据经验(一般经验值为3-5min)来设定,并根据实际情况不断修正。
3、重试机会还是失败怎么办?
当设置的重试次数全部用完后,系统仍然返回失败。 这个时候,这里的生意就相当于中断了。 这时就需要采取一定的补偿措施来保证系统进程的正常进行,保证数据的准确性和及时性。
四、重试机制的解决方案 1、Spring-Retry
spring-retry是spring本身提供的一种重试机制,可以帮助我们以一种标准的方式处理特定操作的重试。 在spring-retry中,所有的配置都是基于简单的注解。
/**
* value: 仅在抛出指定异常时重试
* include:同value,默认为空,当exclude也为空时,默认所有异常
* exclude: 指定不处理异常
* maxAttempts:最大重试次数,默认3次
*退避:重试等待策略,
* 默认使用@Backoff,@Backoff的值默认为1000L,我们设置为2000; 以毫秒为单位的延迟(默认 1000)
* multiplier(指定延迟倍数)默认为0,表示固定停顿1秒后重试。 如果 multiplier 设置为 1.5,则第一次重试为 2 秒,第二次为 3 秒,第三次为 4.5 秒。
* Spring-Retry还为@Retryable重试失败后处理方法提供了@Recover注解。
* 如果不需要回调方法,可以直接不写回调方法,那么效果就是重试次数结束后,如果仍然不满足业务判断,则抛出异常。
* 可以看到传入的参数中写了Exception e,作为回调的连接器代码(重试次数用完,或者失败,我们抛出这个Exception e通知来触发回调方法)。
* 防范措施:
* 方法的返回值必须和@Retryable方法一致
* 方法的第一个参数必须是Throwable类型。 建议与@Retryable配置的异常保持一致。 其他参数,需要哪个参数,写进去即可(有的在@Recover方法中)
* 回调方法和重试方法写在同一个实现类中
*
* 由于是基于AOP实现,不支持类中的自调用方法
* 如果重试失败,需要后面跟@Recover注解的方法,那么重试方法不能有返回值,只能为void
* 方法中不能用try catch,只能在外面抛出异常
* @Recover注解用于开启重试失败后调用的方法(注意必须和重新处理方法在同一个类)。 该注解注解的方法参数必须是@Retryable抛出的异常,否则无法识别。 日志处理方法。
*/
spring-retry工具虽然基于注解,优雅的实现了重试,但是也有几个不友好的设计:
2. Guava-重试
-- 可以设置任意任务单次执行的时间限制,超时则抛出异常;
-- 可以设置一个重试监听器来执行额外的处理
-- 可以设置任务阻塞策略,可以设置本次重试完成,下次重试开始前的时间段内做什么
--可以通过停止重试策略和等待策略组合设置更灵活的策略,如指数等待时长和最多10次调用,随机等待时长和永不停止等待
3. Spring-Aop
自己造轮子:使用AOP为target设置aspect,可以在target调用前后添加一些额外的逻辑。
4.使用MQ进行消息重试
以卡夫卡为例:
kafka consumer采用手动异步ack确认机制。 如果消费失败,则重新消费该消息。 可以设置重试次数,让要消费的消息简单重试即可。 流行的解决方案:设置重试主题
基本流程如下:
retry-topics带来的问题和思考
5.其他解决方案
但是:如果被代理的类没有其他依赖类,直接创建是没有问题的; 如果被代理的类依赖于spring容器管理的其他类,这个方法会抛出异常,因为被代理的实例没有被注入到创建的代理实例中。 在这种情况下java计算时间间隔,它更复杂。 需要从Spring容器中获取需要代理的组装实例,然后为其创建代理类实例,交给Spring容器管理,这样就不用每次都重新创建. 创建一个新的代理类实例。
同时还要考虑容器中的bean类型是Singleton还是Prototype。 如果是Singleton,则同上操作。 如果是Prototype,每次都新建一个代理类对象。
另外,这里使用了JDK动态代理,自然有缺陷。 如果要被代理的类没有实现任何接口,那么就不可能为它创建代理对象。 这种方法行不通。
6.一个消息重试解决方案案例背景
服务器Svr、数据采集中间件Svr、车载Svr进行指令交互
2.重试要求
3.解决方案
使用redis记录命令、命令索引、远程启动命令、命令重试; 使用naocs配置最大命令重试次数; 使用 xxl-job 设置命令重试的间隔;
指令缓存(String类型):key:dispatch:vehicle:retry:instruction:instructionId(指令id) value:json字符串(本次发出的指令json)
指令重试次数(字符串类型):key:dispatch:vehicle:retry:count:instructionId value:0(默认为0)
[远程启动] 指令缓存(设置类型):key:dispatch:vehicle:retry:remotestart value:instructionId
注意:这两个缓存会在消息返回成功或重试N次后被删除; 默认缓存个数用nacos配置,重试的执行间隔用xxl-job设置,可以是每分钟一次,也可以是间隔的倍数等。
消息指令索引集(Set type):key:dispatch:vehicle:retry:failmsgindex value:instructionId
获取缓存指令索引集合,判断集合集合是否为空。 如果为空,则【没有需要重试的指令】; 如果集合set不为空,则遍历集合中的每一个值。
由于是分布式系统,这里需要分布式锁,我们这里使用redission架构进行加锁。此时,还是需要判断具体的业务:有一个特殊的命令叫【远程启动】命令。 由于这条命令下发之前没有创建任务,所以无法关联到任务,所以如果是这条命令,直接重试。 如果不是,则需要判断任务的状态。 我们只重试正在执行的任务。 已完成、暂停和放弃的任务不会重试指令。
// 核心代码:
@XxlJob("doRetryHandle")
public void doRetryHandle() {
log.info("...[消息重试]正在进行消息重试...");
// 获取执行结果为失败的消息索引
Set msgSet = commonHandle.getRedisFailMsgIndex();
log.info("...[消息重试]需要进行消息重试的条数为:{},消息索引为:{}", msgSet.size(), msgSet);
if (EmptyUtil.isEmpty(msgSet)) {
log.info("[没有需要进行重试的指令]:doRetryHandle:[{}]", JSON.toJSONString(msgSet));
} else {
msgSet.forEach(instructionId -> {
RLock lock = redissonClient.getLock(DispatchRedisConstants.DISPATCH_MSG_RETRY_LOCK + instructionId);
log.info("...[消息重试]获取到锁{}", DispatchRedisConstants.DISPATCH_MSG_RETRY_LOCK + instructionId);
try {
lock.lock();
//ADD:前置条件,只对执行中的任务进行重试;若下发指令时,验证任务为【作废/完成/暂停=非执行中】状态,则删除该指令重试的缓存,同时更新该指令状态为失败
// todo:问题:1.远程启动命令时,尚未进行创建任务关联 2.需要做指令跟任务关联的缓存(同时注意删除时机)
// 判断指令是否为远程启动的指令,如果是直接进行重试,如果不是则进行判断任务的状态
if (commonHandle.getRedisRetryRemoteStart(instructionId)) {
doRetryExcute(instructionId);
} else {
String dispatchId = commonHandle.getRedisInstrctionIdDispatchId(instructionId);
log.info("...[消息重试]根据指令id:{}获取到该指令所属的任务id:{}", instructionId, dispatchId);
if (EmptyUtil.isNotEmpty(dispatchId)) {
// 获取任务的当前状态
DispatchTask dispatchTask = dispatchTaskRepository.getById(dispatchId);
log.info("...[消息重试]消息为:{},任务状态为{}", JSONObject.toJSONString(dispatchTask), dispatchTask.getTaskState());
if (EmptyUtil.isNotEmpty(dispatchTask)) {
//只针对执行状态的任务进行重试
if (dispatchTask.getTaskState().equals(EnumTaskState.EXECUTING.getCode())) {
log.info("...[消息重试]正在进行消息重试,任务状态为:{}", EnumTaskState.EXECUTING.getDesc());
//执行重试
doRetryExcute(instructionId);
} else {
// 非执行状态:则删除该指令重试的缓存,同时更新该指令状态为失败
log.info("...[消息重试]任务状态为:{},不是执行中的任务不进行重试", dispatchTask.getTaskState());
commonHandle.doRetryFailExcute(instructionId, OperateExecuteResultEnum.FAIL.getCode());
}
} else {
log.info("...[消息重试]查询不到任务,不进行重试", dispatchTask.getTaskState());
// 查询不到任务
commonHandle.doRetryFailExcute(instructionId, OperateExecuteResultEnum.FAIL.getCode());
}
}
}
} catch (Exception e) {
log.error("doRetryHandle消息重试异常:{}", e.getMessage());
e.printStackTrace();
} finally {
lock.unlock();
}
});
}
}
/**
* 执行重试逻辑
*/
private void doRetryExcute(String instructionId) {
// 根据指令索引获取指令已经进行的重试次数
String stringCount = commonHandle.getRedisRetryCount(instructionId);
if (EmptyUtil.isNotEmpty(stringCount)) {
Integer retryCount = Integer.valueOf(stringCount);
if (retryCount >= RETRY_COUNT) {
log.info("...[消息重试]doRetryExcute:重试次数为{},不再进行重试", retryCount);
// 删除缓存:指令索引缓存、指令缓存、指令重试次数缓存
commonHandle.deleteRetryCaches(instructionId);
return;
} else {
// 根据指令索引获取需要进行重试的消息
String messageJson = commonHandle.getRedisInstrction(instructionId);
if (EmptyUtil.isNotEmpty(messageJson)) {
log.info("...[消息重试]doRetryExcute:需要进行重试的消息为{}", messageJson);
// 发送重试消息
kafkaSendMessage.retrySendMessage(messageJson);
// 重试次数+1
commonHandle.addRedisRetryCount(instructionId, String.valueOf(retryCount + 1));
// 写日志文件(暂时:正在进行第N次重试,**秒后进行第N+1次重试)
IntructionLogUpdateResultRequestDTO intructionLogUpdateResultRequestDTO = new IntructionLogUpdateResultRequestDTO()
.setOperateId(Long.parseLong(instructionId))
.setExecuteResult(OperateExecuteResultEnum.RERTY.getCode())
.setLogContext("进行第" + (retryCount + 1) + "次重试");
dispatchTaskOperateLogRepository.ModifyResultByInstructionId(intructionLogUpdateResultRequestDTO);
}
}
}
}
总结:没有更好的方案,只有最合适的方案,一定要根据自己的业务来选择,不断优化。