一.设计初衷
目前系统系统job任务是每10分钟重试一次,总共重试7次。所有需要重试的请求都在10分钟之内堆积,如果请求量过大,这个时间点压力都有可能落到数据库上面,从而造成数据库崩溃。
此外时间配置不够智能化。如果两小时之后所依赖的第三方系统稳定,而目前的重发机制相当于失效了。 基于上面两个原因,提出了改进方案,使用延时job实现功能!
二.功能原理
通过redis的zset数据类型来存储数据,score是过期时间,value可以是封装的记录流水号。使用mysql来确保数据只能被消费一次。
三.流程介绍
3.1 插入记录
实现值的插入很简单。只需要将待重发数据先插入数据库,库里的状态设置为待重发,然后在存入redis中,记录初次过期的时间。
3.2 执行延时job
3.2数据补偿
如果redis突然挂了,需要做补偿措施,可以取表里面近3天需要重试的记录放到redis中,让其再次重发。
四.异常情况
1).如果在【插入记录】数据的时候,插入数据库报错,这样这条数据也无法进入redis中,需要人工处理数据库异常。
2.) 如果在【插入记录】插入数据库成功,插入redis失败,这个时候通过补偿机制,将mysql中的记录同步到redis中(同步频率待确认)。
3).在【执行延时job】时,如果redis直接报错,执行停止。执行停止肯定不可取。解决方案:可以使用一个状态量来判断redis是否正常工作,如果正常工作就执行job,否则,让线程sleep2个小时。
4).在【执行延时job】时,如果调用第三方接口网络不通,目前考虑将该次重试记录入库(这样是否妥当?还是说这次请求不算,数据不入库。),状态是失败,出错消息提示为网络异常。
5).在【执行延时job】时,删除redis值失败,由于redis中还有本来应该删除的值,会再发一次请求(由于理房通那边会校验,再发一次也没问题)。
6).在【执行延时job】时,入库异常,库里面的数据一直存在。在数据补偿的时候数据会放进redis中,就算数据库挂的时间很长也没问题。会用数据里面存的时间加上一个定值在放到redis中,作为下次要过期的时间。在redis删除值时,只会删除那些时间小于当前时间的记录。
注意:添加数据采用异步的方式。
五.库表设计
5.1 sql
CREATE TABLE `t_trade_time_delay` (
`id` BIGINT (20) PRIMARY KEY COMMENT '主键ID',
`request_id` BIGINT(20) NOT NULL DEFAULT 0 COMMENT '请求id',
`current_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '当次时间',
`current_level` INT NOT NULL DEFAULT 0 COMMENT '当次级别',
`is_finished` TINYINT (1) NOT NULL DEFAULT '0' COMMENT '是否完成,1 完成,以后不需要处理,0未完成,还需要处理',
KEY `idx_request_id` (`procurement_request_id`) USING BTREE COMMENT '索引:请求id'
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4 COMMENT = '延时处理表';
5.2 关键字段说明
current_time 该字段存每次需要过期的时间
current_level 改字段存每次需要过期时间的等级。
举例说明, 延时job设置的值为 :
level time(秒)
1 5
2 30
3 60
4 600(十分钟)
5 1800(30分钟)
6 3600(1小时)
7 6*3600(6小时)
8 24*3600(1天)
9 2*24*3600(2天)
在今天10:00:00 将一个请求编号3003,存入表中,此时
request_id为3003,current_time 为10:00:05 ,current_level为1,is_finished为 0。
如果时间到了10:00:05,执行了这条记录,结果还是失败的,这时
request_id为3003,current_time 为10:00:35 ,current_level为2,is_finished为 0。
依次类推。。。
如果在level为3第三方接口通了。此时
request_id为3003,current_time 为10:01:35 ,current_level为3,is_finished为 1。
六.注意点
1.在生产环境中我们都是集群部署的,所以需要使用分布式锁来实现资源的控制。可以你们会说,不是使用了redis吗?它不是天然的适用于redis吗?没错,往redis里面插入中是没问题的,但是考虑到我们的业务场景:第一步:从redis里面获取值。第二步:通过上一步拿到的值,调用下游接口。那么问题就来了,在第一步的时候是没有问题的,但是到了第二步,如果没有锁住,多个客户端可能针对于同一个请求体发多次请求,在下游没有控制好幂等的情况下就gg了。加了分布式锁控住第一步和第二步,这个问题就不存在了。
2.在生产中,我们目前的是在程序系统启动完成之后起一个线程死循环消费redis里面的数据,这里就需要考虑了,如果网络抖动或者redis先挂后好,我们的线程可能就挂了,也就是说虽然项目是一直启动着,但是redis里面的数据就是消费不了。我们采用了一种最笨的方案来解决这个问题。做一个定时任务监控这个线程是否一直都是活的,如果线程挂了,就新建一个再次去消费redis。线程的监控工具也有很多,jstack,jvisualvm等。