场景
工作中我们经常遇到遍历表的场景,查询表数据,处理表数据,当表的数据量多的时候,如何保证性能呢?
我们假设表结构如下:
-- 源表结构示例
CREATE TABLE source_table (
id INT AUTO_INCREMENT PRIMARY KEY,
data VARCHAR(255),
create_date DATE,
processed TINYINT DEFAULT 0 -- 可选:标记是否已处理
);
-- 确保排序字段有索引
CREATE INDEX idx_id ON source_table(id);
使用传统的游标式分页,LIMIT offset, size,在大数据量下,效率极低,每次都需要扫描前 N 行。
直接全表查询更不可行,大概率查询超时,或者直接把内存打爆。
解决方案
确定一个递增的字段,就可以使用游标式分页查询的方式,伪代码如下:
-- 每次循环执行以下查询(应用程序中通过变量传递last_id)
SELECT id, data
FROM source_table
WHERE id > @last_id
ORDER BY id
LIMIT 1000;
这里不一定要是主键 id,只要能保证字段单调递增就行。如果数据允许的话,可以创建覆盖索引(id, data),避免回表查询。
如果想进一步提升性能,可以使用多线程并发处理。这里有个注意点,并发处理的时候,不是并发查 last_id,而是在内存中并发处理数据。流程如下:
相比串行处理,并发的优势在于虽然多了一次回表查询,但是极大提升了处理效率,因为大部分时候,数据处理才是最耗时的。伪代码如下:
List<CompletableFuture<Integer>> futures = Lists.newArrayList();
Long lastId = -1L;
do {
futures.add(batchJob(maxTaskFlowId, PAGE_SIZE);
lastId = queryLastId(lastId, PAGE_SIZE);
} while (lastId != null);
public CompletableFuture<Integer> batchJob(Long lastId, int pageSize) {
return CompletableFuture.supplyAsync(() -> {
List<Data> datas = queryData(lastId, pageSize);
//deal data
}, executorService);
}
有个风险点是因为同步查完 last_id,就丢到线程池阻塞队列中去了,在极端情况下,可能会导致阻塞队列很长,但是数据上我们只是存了 id 值,并没有存具体的数据,所以不会占用太多内存。如果确实有这方面的担忧,那就设置下阻塞队列为数组,更新下线程池拒绝策略。
总结
这里是基于单机的做法,如果系统支持分布式调度的话,那方案就不一样了。我们可以将每个分片作为一个单独的任务,根据调度策略分发到其他机器上执行。在确定技术方案的时候没有什么最优的方案,充分利用当前系统能力,能解决问题、不留坑的就是好方案。