前言

定时任务在系统中并不少见,主要目的是用于需要定时处理数据或者执行某个操作的情况下,如定时关闭订单,或者定时备份。而常见的定时任务分为2种,第一种:固定时间执行,如:每分钟执行一次,每天执行一次。第二种:延时多久执行,就是当发生一件事情后,根据这件时间发生的时间定时多久后执行任务,如:15分钟后关闭订单付款状态,24小时候后关闭订单并且释放库存,而由于第二种一般都是单一数据的处理(主要是指数据量不大,一般情况下只有一个主体处理对象,如:一个订单以及订单中的N个商品),所以一般情况下第二种出现性能问题的几率不大(不代表没有),所以本文主要是针对第一种定时任务来进行优化,而且主要是针对数据同步或者传递数据来进行优化,而优化的方式也是根据实际项目中的情况在不同阶段进行优化的

第一阶段 ~

第一阶段属于原始阶段,逻辑也最为简单,由于同步分为数据同步和传递数据,而且2种的需求各不一致(主要是在于是否允许丢失),所以分开分析

第一种类型:传递数据

由于传递数据可以允许丢失,常见的场景如调用凭证推送(常见于接口需要暴露给第三方,为了安全性,可以定时推送调用凭证来保证接口安全性),消息推送(订单消费成功后推送消息,由于可能推送失败,所以需要进入定时任务进行重试,但是因为消息实时性,所以重试到一定次数后放弃重试)

传递数据在第一阶段设计非常简单,定时推送,有限的错误次数,同步成功后修改状态,同步失败后对失败次数+1,一旦超过错误次数,就不在继续尝试

第二种类型:同步数据

同步数据跟传递数据不同点在于同步数据一定需要保证数据能投递成功,否则就要一直进行重试,比如2个系统间的订单同步,会员信息同步等

同步数据再第一阶段也非常简单,定时同步数据,失败就设置同步状态为同步失败,每次同步就只查询状态为未同步和同步失败记录

第二阶段

一开始需要传递或者同步的系统很少,数据也少,所以没有什么问题,但是第二阶段不一样了,数据量稍微有所新增,但是增量不大,主要是需要同步的系统多了,打个比方,连锁商店,总部需要把数据下传到所有门店去,这样门店就不用每次去总部获取数据,这样太耗费时间了,当然门店每次从总部获取到数据可以缓存到本地,不过跟本文内容关系不大,所以这里不再讨论。由于需要同步的系统太多,所以延伸出另外一个问题,一旦一个系统的网络环境不好,会影响其他系统数据同步,所以在第二阶段,引入了黑名单机制,由于黑名单机制对于传递数据和同步数据大致相同,所以这里就不分开描述,有差异性的地方也会指出

黑名单具体处理机制

黑名单分二级:

第一级用于控制本次定时任务,当本次运行定时任务时,不同的接受数据服务器可能有0-N条数据需要同步,所以一旦进入第一级黑名单后,本次后面都不会向接受数据服务器发起请求,而是直接失败;

第二级用于控制多长时间内不进入重试,是控制整个的,从查询需要同步的数据时候就直接过滤并且设置为同步失败状态(传递消息需要对失败次数加1)

首先第一级,当请求不到接受数据的服务器的时候(链接失败,或者链接超时),会再重试2次(传递数据由于及时性要求,所以不会重试,并且超时时间也会合理的减少),如果2次都同步失败,这判断本条数据同步失败,并且进入第一级黑名单,并且判断一定时间内进入了几次第一级黑名单,具体使用redis控制,首先是否进入第一级黑名单直接程序中存储就好,一定时间段内进入了几次黑名单,就使用有序集合保存,排序的分值就存储当前时间戳

进入第一级黑名单后,使用一定时间内进入几次的限制条件,来判断是否进入第二级黑名单,比如5分钟进入3次第一级黑名单,就进入第二级黑名单,那么就查询分值大于5分钟前时间戳的数据集合,如果集合结果有3条或以上数据了,那么就进入第二级黑名单,同时清理掉redis中关于第一级黑名单存储的数据,如果没有3条数据,那么就删除分值小于5分钟前的时间戳的数据,避免垃圾数据过多

使用黑名单机制,可以有效避免一些因为服务本来不可访问导致一直还重试的问题,并且由于有二级黑名单,所以也一定程度上避免了因为暂时网络波动,导致数据长久无法同步的问题

第三阶段

由于需要传递的数据和需要同步数据的服务越来越多,并且由于各种问题导致很多数据不能一次性同步成功,所以每次定时任务都需要同步大量数据,这样就导致及时性很差了,比如几千条数据同步下来,就算一条只需要几十毫秒,从开始到最后一条数据同步成功也是几十秒之后了,所以需要再次对定时任务进行优化,数据量大而导致同步慢原因很简单,是由于单个线程串行同步的,也就是说必须要上一条数据处理了才能处理下一条数据,所以可以使用多线程来优化,提高硬件使用率

多线程的定时任务

当然肯定不可能给每条数据创建一个线程,先不说得创建多少条线程,仅仅是创建线程的消耗就已经很大了,而且线程数量太多,频繁切换线程上下文也会导致性能损耗,所以最合适的就是将数据分配到机器CPU核心数量的线程,或者核心数量*2的线程上去处理更合适,当然具体情况具体分析,最好还是具体测试得出合适的线程数量,同时由于肯定是会存在多个定时任务,所以可以多个定时任务使用同一个线程池,但是每个任务只使用合适线程数量来处理

线程数据分配原则

同一个被接受调用的数据的服务器的数据肯定是分配到一个线程中去处理,比如要分配8个线程来处理,那么可以创建8个集合,先保存查询出来需要被同步的数据,同时查询出来的数据根据被接受数据的服务器标识排序,用接受数据的服务器标识的hash值来%8来确定放入哪个集合,或者使用轮询的方式放入指定集合,分配好之后则创建8个runable放入线程池中去执行

防止定时任务叠加

开启多线程处理后,由于主线程在把任务放入线程池中运行的时候就会返回了,所以一定需要防止定时任务叠加,比如任务是10秒执行一次的,每次定时任务本身的线程只执行了1秒,下次定时任务的时候会发现定时任务已经处理完成,但是实际上真正同步数据的8个线程都没有执行完成,就会出现一条数据重复同步,或者把数据累加到上次任务的集合中去(看具体的处理方式导致不同的结果),最后就跟滚雪球一样,整个服务就算不崩溃,也会出现各种问题,或者就是浪费大量资源去做重复同步,所以为了防止任务叠加,需要使用闭锁来防止定时任务本身返回的情况,同时使用闭锁也要注意处理异常的情况,防止发生异常后,闭锁没有执行操作,导致定时任务一直不能返回

闭锁

使用闭锁防止定时任务返回,8个线程的情况下创建闭锁

CountDownLatch latch = new CountDownLatch(8);

每个线程执行完数据后需要countDown方法来通知,或者叫关闭一个栅栏吧,创建闭锁的传入的8我们可以看成创建了8个栅栏

latch.countDown();

同时在定时任务的线程中,需要等待所有栅栏关闭才能继续执行,所以需要调用方法

latch.await();

这样只有所有线程执行完成后,定时任务的线程才会继续执行,防止任务叠加

使用多线程了,一定要注意多线程的一些线程安全以及其他的一些问题,如果对闭锁和多线程本身不够了解的话,可以自行去查阅一些相关资料

第四阶段

数据量非常大,接受数据的服务也非常多

一台服务器的硬件资源始终有限,尤其是网络资源,由于接受数据的服务不一定是内网服务,加上各种问题导致链接失败,所以数据量大的情况下,就算使用了多线程,还是会造成数据延迟很久才同步成功(主要延迟原因是网络问题),这时候就需要使用多台服务器了,而使用多台服务器定时执行就存在一个问题,数据分片,简单来说怎么保证一条数据只能被一台服务器处理,数据分片有2种方式,第一种:不同服务器处理不同的表的数据。第二种:数据本身主键或者某种标志分配处理

2种处理方式有各自的优缺点

第一种:

优点:简单,只需要简单拆分或者配置即可

缺点:无法扩展更多,最多只能可能扩展到数据表数量台服务器,并且对于热点数据无法更优处理,比如订单这些热点数据,始终都在一台服务器

第二种:

优点:理论上可无限扩展,可以针对热点数据专门扩展

缺点:配置麻烦,每次新增服务器需要重新配置

实现分片定时任务

由于第一种配置简单,而且扩展性不强,所以本文主要讲述第二种方式的实现;

如果所有数据有生成都有自增型主键id,那么最简单也最公平的就是给每台服务器配置一个从0开始连续的服务器id,每台服务器查询数据的时候加一个条件id%服务器台数=当前服务器id,注意这样会导致id列的索引可能无法命中(根据数据库不同,是否命中情况不一致),这样配置的好处就是绝对公平,每台服务器分配到的数据量是平等的,坏处就是一台服务器可能会给所有接受数据服务发起请求,无法更好的利用链接复用,另外也无法针对服务器配置来增加或者降低权重(当然可以一个服务器配置2个id的方式来实现,但是这样也不友好)

如果为了更好的利用链接复用,可以使用先计算出接受数据服务标志的hashcode值,然后跟进hashcode值%服务器台数=当前服务器id的形式,这样就可以将接受数据服务分组式的配置到某个服务器上去处理,当然如果接受数据服务本身存在很大的数据量差异,就不推荐这种方式了,毕竟这样容易把大量数据堆积到某台服务器上去处理

当然还有其他多种分片的配置方式,比如采用表配置的方式来配置哪台服务器处理哪些数据,也可以使用上面种方式的结合体,可以根据具体情况分析到底怎么样才能更适合的进行数据分片处理,当然常规情况下,采用id%服务器的台数是能满足大部分需求的

其他优化

当系统针对性能优化到一定程度的时候,就可以考虑从业务或者其他方面进行优化了,比如一旦有系统进入二级黑名单了,就发出警告通知,或者没有进入二级黑名单,但是却经常进入一级黑名单,也提出一个报警,这样可以让人去排查原因,确认是程序问题还是网络本身的问题。另外也可以设置一个阈值,某个接受数据的服务一直响应很慢,或者经常响应时间超过某个阈值的时候,可以考虑进行降权处理,或者排查程序已经网络相关的原因

作者:佚名

来源:https://sohu.gg/KsBGgl