1.单点定时任务
1)
在jdk1.5以后,可以使用ScheduledExecutorService来进行定时任务,项目中可以对该服务进行封装使用
public class SomeScheduledExecutorService {
public static void main(String[] args) {
// 创建任务队列,一共5个线程
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(5);
// 执行任务: 1秒 后开始执行,每 60秒 执行一次
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("执行任务:" + new Date());
}, 10, 60, TimeUnit.SECONDS);
}
}
2)基于redis的方案
基于Redis的实现可以通过多点来增加定时任务,多点消费。但是要做好防范重复消费的准备。
@Configuration
@EnableScheduling
public class RedisJob {
public static final String JOB_KEY = "redis.job.task";
private static final Logger LOGGER = LoggerFactory.getLogger(RedisJob.class);
@Autowired private StringRedisTemplate stringRedisTemplate;
/**
* 添加任务.
*
* @param task
*/
public void addTask(String task, Instant instant) {
stringRedisTemplate.opsForZSet().add(JOB_KEY, task, instant.getEpochSecond());
}
/**
* 定时任务队列消费
* 每分钟消费一次(可以缩短间隔到1s)
*/
@Scheduled(cron = "0 0/1 * * * ? *")
public void doDelayQueue() {
long nowSecond = Instant.now().getEpochSecond();
// 查询当前时间的所有任务
Set<String> strings = stringRedisTemplate.opsForZSet().range(JOB_KEY, 0, nowSecond);
for (String task : strings) {
// 开始消费 task
LOGGER.info("执行任务:{}", task);
}
// 删除已经执行的任务
stringRedisTemplate.opsForZSet().remove(JOB_KEY, 0, nowSecond);
}
}
适用场景如下:
订单下单之后15分钟后,用户如果没有付钱,系统需要自动取消订单。
红包24小时未被查收,需要延迟执退还业务;
某个活动指定在某个时间内生效&失效;
优势是:
省去了MySQL的查询操作,而使用性能更高的Redis做为代替;
不会因为停机等原因,遗漏要执行的任务;
加粗样式3)redis键空间的方式
实现思路是给所有的定时任务设置一个过期时间,等到了过期之后,我们通过订阅过期消息就能感知到定时任务需要被执行了,此时我们执行定时任务即可。
默认情况下Redis是不开启键空间通知的,需要我们通过config set notify-keyspace-events
Ex的命令手动开启。
场景举例:
订单下单之后15分钟后,用户如果没有付钱,系统需要自动取消订单。
红包24小时未被查收,需要延迟执退还业务;
优劣势是:
被动触发,对于服务的资源消耗更小;
Redis的Pub/Sub不可靠,没有ACK机制等,但是一般情况可以容忍;
键空间通知功能会耗费一些CPU
2.分布式定时任务
什么是分布式定时任务,个人总结为:把分散的,可靠性差的计划任务纳入统一的平台,并实现集群管理调度和分布式部署的一种定时任务的管理方式。叫做分布式定时任务。
单点定时任务的缺点:
功能相对简单,交互性差,任务部署效率低,开发和维护成本比较高,不能很好的满足各系统定时任务的管理和控制,尤其在多系统的环境下更加明显;
许多任务都是单机部署,可用性差;
任务跟踪和告警难以实现。
分布式定时任务的优势:
通过集群的方式进行管理调度,大大降低了开发和维护成本;
分布式部署,保证了系统的高可用性,伸缩性,负载均衡,提高了容错;
可以通过控制台部署和管理定时任务,方便灵活高效;
任务都可以持久化到数据库,避免了宕机和数据丢失带来的隐患,同时有完善的任务失败重做机制和详细的任务跟踪及告警策略。
3.1 分时方案
严格划分时间片,交替运行计划任务,当主系统宕机后,备用系统仍然工作,但是处理初期被拉长了。
缺点:周期延长了。
3.2 HA高可用方案:
正常情况下主系统工作,备用系统守候,心跳检测发现主系统出现故障备用系统启动。
缺点:单一系统,不能做负载均衡,只能垂直扩展,也就是硬件层面的升级,无法做水平扩展。
3.3 多路心跳方案:
采用多路心跳,做服务级,进程级的,IP和端口级别的心跳检测,正常情况是主系统工作,备用系统守候,心跳检测主系统出现故障,备用系统启动,当再次检测到主系统工作,则将执行权交回主系统。
缺点:开发比较复杂,程序健壮性要求高。
3.4 任务抢占方案:
A,B两台服务器同时工作,启动需要存在一前一后,谁先启动谁率先加锁,其他服务器只能等待,他们同时对互斥锁进行监控,一旦发现锁被释放,其他服务那个先抢到,那个运行,运行前加排他锁。
优点:可以进一步实现多服务器横向扩展。
缺点:开发复杂,程序健壮性要求高,有时候会出现不释放锁的问题。
3.5 任务轮询或任务轮询+抢占排队方案
每个服务器首次启动时加入队列;
每次任务运行首先判断自己是否是当前可运行任务,如果是便运行;
如果不是当前运行的任务,检查自己是否在队列中,如果在,便推出,如果不在队列中,便键入队列。
分布式互斥锁:
互斥锁也叫排他锁,用于并发时管理多进程和多进程同一时刻只能有一个进程或者线程操作一个功能。我们将进程,线程中的锁延伸到互联网上,实现对一个节点运行的进程或线程加 锁,解锁操作。这样便能控制节点上的进程或线程的并发。
还可以通过对锁设置最大时间来解决server在加锁的过程中,出现宕机的问题
队列:
在上面的基础上,排队运行任务。
从上图中可以看出,TaskQueue中排队情况,运行是自上而下的,当然这个顺序可以自己设置规则,只需要先进先出的远程即可。另外,Task Queue我们需要做至少两个节点,他们遵循主 从结构的原则,主节点需要实时向从节点同步数据,保证在主节点不可用,从节点可以替代。当然,这里可以使用权重轮询的方式,加上数据异步同步,让所有节点都可以做主从的切换, 根据运行状况来分配,可能会更好,但是这样开发难度也有所提高,但是大大增加了高可用性。
当前比较流行的分布式定时任务框架:
Quartz:
Quartz是Java领域最著名的开源任务调度工具。Quartz提供了极为广泛的特性如持久化任务,集群和分布式任务
特点:
完全由Java写成,同时可以很方便的和java的另外一个框架spring集成;
强大的调度功能:支持丰富多样的调度方法,可以满足各种常规及特殊需求;
灵活的应用方式:支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
分布式和集群能力,负载均衡和高可用性;