1、背景
数据库中存在一个预约时间表(t_reserve),和一个正在生效的时间表(t_time)
根据业务需求,在t_reserve表中配置多个不同的时间,然后定时去更新t_time表
2、问题
因为应用是集群部署,需要考虑
1)、多进程的任务锁抢占
2)、当抢占到任务锁的应用挂了,宕机的情况,没有释放任务锁,造成死锁
3)、集群部署,无法保证每台应用同时启动定时任务,多个定时任务的触发点会被错开,定时任务的间隔执行时间无法保证
3、实现
需要设计一个定时计划表(t_job)
job_name | job__status | start_time | end_time | interval |
---|---|---|---|---|
任务 | 运行状态 0-未运行 1-运行中 |
开始时间 | 结束时间 | 任务锁过期时间 |
job | 0 | 60 |
3.1)、任务锁的实现(列举当时想到的三种实现)
3.1.1)、本来打算使用forupdate实现悲观锁
a)、查询t_job,看是否有任务,任务是否在执行中
select * from t_job where job_name='job' forupdate; #行锁,会锁住t_job表的job这一行
forupdate锁表规则:
当有明确指定唯一行(如主键),那么就是行锁,其他行的数据,别的事务还是可以操作该表的
指定行不明确(如不等于某个主键值,会返回好多数据),那么就是表锁了,这时候锁了整个表,其他事务无法操作该表
b)、这儿又需要分两种情况 肯定会更新成功,悲观锁,同个只有同个时间可以操作
b.1)、当前查询到没有任务在执行
直接更新t_job表,将job_status置为1,表示抢占到任务锁
update t_job set job_status=1, start_time=now() where job_name='job' and job_status=0 and end_time='上一步查询结果';
b.2)、当前查询到任务在执行中
(这种情况需要注意判断是否锁没有释放掉,如果没有释放掉死锁,会导致之后的定时任务无法运行(包括其他应用))
需要自己设定一个任务锁过期值,到时间了,直接就把任务锁释放掉,会有一可能就是当你定时任务跑的时间太长了,导致应用误判为死锁,所以这个过期值需要考虑,取一个合适值
update t_job set job_status=1, start_time=now() where job_name='job' and job_status=1 and TIMESTAMPDIFF(SECOND,end_time,now())>=interval;
b.3)、如果update执行成功,则代表成功的获取任务锁,其他情况都是没有获取到
c)、正在实现业务逻辑
d)、释放任务锁
update t_job set job_status=0,, end_time=now() where job_name='job' and job_stauts=0 and end_time='上一步查询结果';
为什么释放任务锁时做更新操作,where要使用end_time?
因为必须控制版本,只能释放当前任务获取到的任务锁(误判死锁,其实本身任务还在执行,可能由于某个原因导致执行时间过长,超过任务锁的过期值)
注:这儿还会有一个问题,就是在select … from t_job where … forupdate锁表的时候,因为是悲观锁,其他事务无法访问到t_job表的这一行,所以会抛异常,需要进行异常捕获
3.1.2)、使用悲观锁,将获取任务锁、逻辑处理、释放任务锁放在同一个事务中(常见的一种处理方式)
a)、获取任务锁
update t_job set job_status=1
where job_name='job' and job_status=0;
b)、逻辑处理
c)、释放任务锁
update t_job set job_status=0
where job_name='job' and job_status=1;
也是存在行锁,其他事务无法操作t_job表该行数据
因为在同个事务中,当发生异常,则该任务的所有数据库操作都会被回滚,包括t_job表的job_status,也会被回滚成0,为未运行状态。执行成功,则事务会自动提交所有的数据库操作
优点 不会造成死锁
缺点 如果逻辑处理时间太长,事务没有释放,可能会导致其他逻辑不能操作对应的业务表
3.1.3)、使用乐观锁。主要是对3.1.1的优化,不用forupdate来锁表,直接查询t_job的任务状态后,去更新t_job表,更新成功则获取到任务锁,更新失败则获取不到(可能被其他应用先更新t_job表了,所以更新失败了)
3.2)、多进程(应用)的任务锁抢占、死锁的释放,3.1已经有了三种方案。但是还是存在了一个问题,集群部署的时候,无法保证启动时间的相同,这样就会存在定时任务启动的时间差问题
例如在程序中实现了一个定时任务,隔间是1分钟执行一次,部署在A、B两台机器上
A应用在50分11秒启动,定时任务也启动了
B应用在50分31秒启动,定时任务也启动了
这时候A的定时任务执行时间是 50分11秒 51分11秒 52分11秒 53分11秒
这时候B的定时任务执行时间是 50分31秒 51分31秒 52分31秒 53分31秒
这样的话,定时任务的执行间隔,就不是1分钟了,A执行一次,30秒后B执行一次,30秒后A又执行一次(真实场景时间不确定,无法把控),任务锁的抢占设计也就没有意义了。
为了保证定时任务在多台应用的执行频率相同,可以去控制任务锁获取的间隔时间
解决方案:
3.2.1)、在update语句的where中加上条件TIMESTAMPDIFF(SECOND,end_time,now())>=60
才能去获取锁
3.2.2)、可以先查询t_job,然后应用再根据查询出来的end_time和当前时间比较,看是否超过间隔时间1分钟,超过了间隔时间,则更新t_job获取锁
3.3)、定时任务随着应用发布而启动
3.3.1)、实现spring的监听器接口ApplicationListener,监听ContextRefreshedEvent事件,并重写对应的onApplicationEvent,去创建线程池执行定时任务
注:
1、监听器不归spring容器管理,所以无法使用@Autowired去注入bean。如果需要拿到bean,需要去实现ApplicationContextAware接口,拿到spring容器,然后再利用getBean得到对应的bean。
2、定时任务的处理逻辑是一个继承Runnable的类,不归spring容器管理,该类如果需要拿到spring容器中的bean,则使用有参构成方法传入即可。