数据库扫表任务如何避免出现死循环

假如我们有一张表case_event,其中有一个字段state,它有三个值,分布是INIT、SUCCESS、以及 FAILED。

那么在定时任务中,我们需要把 INIT 的数据扫描出来进行执行,一般来说是这么写的 SQL:

1
2
3
4
5
SELECT * FROM case_event
WHERE STATE = 'INIT'
ORDER BY ID
LIMIT 200;
//以上的数据拿出来执行 那么这时候状态就会变成 success 或者 Fail

这个 SQL 看上去没啥问题,其实就是每次扫描200条记录处理。

但是这个SQL其实是一个典型的 bad case,因为他会出现一个致命的问题,那就是可能会导致扫描任务一直无法执行。

因为上述的 SQL 相当于默认了每一条记录执行之后,都能把状态推进到 SUCCESS 或者 FAILED。但是事实上并不一定的,尤其是在一些有很复杂的业务逻辑,或者一些外部调用的时候,这个地方就变成了一个分布式事务,我们没办法保证最后的 INIT->SUCCESS 或者 INIT->FAILED 一定能成功。即还是INIT

那如果不能成功,就会导致一部分失败的状态一直处于 INIT 状态,那么他就会每次都会被扫描起来(因为他还在前200条之内),然后还是不成功,下次还会被扫描出来

这样一方面会大大降低任务的效率,一直在重复执行这些不断失败的任务,另一方面,一旦失败的条数达到了200条,那么就意味着每次扫出来的数据都是这200条,导致后面的任务永远无法被执行到。

这里的失败指的是没法推进状态 以及 执行失败

而如果你的SQL 是这么写的,那么这个问题就更大了:

1
2
3
4
SELECT * FROM case_event
WHERE STATE in ('INIT' ,'FAILED')
ORDER BY ID
LIMIT 200;

相当于你在不断的重复执行那些固定的任务,而后面的很多任务一直无法被执行。

一直在重复执行者两百条永远执行失败的任务,而后面的任务就管了一点了

如何解决这个问题呢,有一个方式,那就是增加一个游标,让你的每次查询都往后移动,如:

1
2
select * from task where status = 'init' and id > #{id}
order by id limit 200

这里每次查询的时候,都把上一次的查询结果中的最大id 带过来,然后就可以避免再次扫描到重复的任务了,就可以让本次任务调度正常完成执行。