通过定时任务扫表,是我们在业务中经常会做的事情,一般是直接用xxl-job等定时任务去分页查询数据库,然后进行业务操作,这个方案,一般是最简单的,也是最有效的。
但是,他还是有一些缺点的,如:
随着需要扫描的表中的数据量越来越大,通过定时任务扫表的方式会越来越慢,那么想要解决这个问题,
但是这样做,会带来一个问题,那就是多个线程之间如何做好隔离,如何确保不会出现并发导致同一条记录被多个线程执行多次呢?
Long minId = messageService.getMinInitId();
for(int i=1;i<= threadPool.size();i++){
Long maxId = minId + segmentSize()*i;
List<Message> messages = messageService.scanInitMessages(minId,maxId);
proccee(messages);
minId = maxId + 1;
}
像上面的例子中,假设有10个线程,那么第一个线程就扫描ID处于0-1000的数据,第二个线程扫描1001-2000的数据,第三个线程扫描2001-3000的数据。这样以此类推,线程之间通过分段的方式就做好了隔离,可以避免同一个数据被多个线程扫描到。
这个做法,有个小问题,那就是数据的ID可能不是连续的,那么就需要考虑其他的分段方式,比如在时间表中增加一个业务ID,然后根据这个biz_id做分片也可以。
比如:
for(int i=1;i<= threadPool.size();i++){
List<Message> messages = messageService.scanInitMessages(i);
proccee(messages);
}
这样在SQL中:
SELECT * FROM RETRY_MESSAGE WHERE
STATE = "1"
AND BIZ_ID LIKE "${frontNumber}%"
那么,不同的线程执行的SQL就不一样了分别是:
SELECT * FROM RETRY_MESSAGE WHERE
STATE = "1"
AND BIZ_ID LIKE "1%"
SELECT * FROM RETRY_MESSAGE WHERE
STATE = "1"
AND BIZ_ID LIKE "2%"
SELECT * FROM RETRY_MESSAGE WHERE
STATE = "1"
AND BIZ_ID LIKE "3%"
SELECT * FROM RETRY_MESSAGE WHERE
STATE = "1"
AND BIZ_ID LIKE "4%"
这样也是可以做分段的。
如果业务量比较大的话,集中式的扫描数据库势必给数据库带来一定的压力,那么就会影响到正常的业务。
那么想要解决这个问题,
当然,这里还要考虑一个问题,那就是备库扫描数据之后的执行,执行完该如何同步到主库,这里可以直接修改主库,主备库数据ID一致的,直接去修改主库的就行了。不建议直接在备库上修改。
但是不管怎么样,备库还是可以分担扫表的这个大量高峰请求的。
除了扫备库,还有一个方案,
因为多个数据库的话,每个库提供的连接数就会多,并且多个实例的话,CPU、IO、LOAD这些指标也可以互相分担。
定时任务都是集中式的定时执行的,那么就会存在延迟的问题。随着数据库越来越大,延时会越来越长。
想要降低延迟,那就要抛弃定时任务的方案,可以考虑延迟消息,基于延迟消息来做定时执行。
用了延迟消息之后,还可以缓解数据库的压力。也能比定时扫表的性能要好,实时性也更高。
当然,引入另外一个中间件也需要考虑成本的。