这个问题听上去有点奇怪,一般面试也不会这么问,但是这个问题我见过很多次,甚至很多我大厂的同事写代码也会这么写。
最近一次就发生在上周,我们做 CR 的时候,一个 P7级别的同事写的代码也出现了这个问题,所以单独拿出来讲讲。帮大家避坑。
假如我们有一张表case_event,其中有一个字段state,它有三个值,分布是INIT、SUCCESS、以及 FAILED。
那么在定时任务中,我们需要把 INIT 的数据扫描出来进行执行,一般来说是这么写的 SQL:
SELECT * FROM case_event
WHERE STATE = 'INIT'
ORDER BY ID
LIMIT 200;
这个 SQL 看上去没啥问题,其实就是每次扫描200条记录处理。
但是这个SQL其实是一个典型的 bad case,因为他会出现一个致命的问题,那就是可能会导致扫描任务一直无法执行。
因为上述的 SQL 相当于默认了每一条记录执行之后,都能把状态推进到 SUCCESS 或者 FAILED。但是事实上并不一定的,尤其是在一些有很复杂的业务逻辑,或者一些外部调用的时候,这个地方就变成了一个分布式事务,我们没办法保证最后的 INIT->SUCCESS 或者 INIT->FAILED 一定能成功。
那如果不能成功,就会导致一部分失败的状态一直处于 INIT 状态,那么他就会每次都会被扫描起来(因为他还在前200条之内),然后还是不成功,下次还会被扫描出来。
这样一方面会大大降低任务的效率,一直在重复执行这些不断失败的任务,另一方面,一旦失败的条数达到了200条,那么就意味着每次扫出来的数据都是这200条,导致后面的任务永远无法被执行到。
而如果你的SQL 是这么写的,那么这个问题就更大了:
SELECT * FROM case_event
WHERE STATE in ('INIT' ,'FAILED')
ORDER BY ID
LIMIT 200;
相当于你在不断的重复执行那些固定的任务,而后面的很多任务一直无法被执行。
如何解决这个问题呢,有一个方式,那就是增加一个游标,让你的每次查询都往后移动,如:
SELECT * FROM case_event
WHERE STATE = 'INIT' and id > #{maxId}
ORDER BY ID
LIMIT 200;
这里每次查询的时候,都把上一次的查询结果中的最大id 带过来,然后就可以避免再次扫描到重复的任务了,就可以让本次任务调度正常完成执行。