mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6mobile wallpaper 7mobile wallpaper 8mobile wallpaper 9mobile wallpaper 10mobile wallpaper 11mobile wallpaper 12
1386 字
4 分钟
优先队列流式处理 + 多路归并排序:轻松实现DB分表严格有序的分页查询
2025-08-22

一、业务场景#

你正在开发一款短链接生成平台,用户可在该平台上创建多个分组,并在分组内生成大量短链接。

现需开发一个接口,支持用户其创建的所有短链接。该接口需满足以下要求:

  1. 严格排序:查询结果必须严格按修改时间倒序排序。
  2. 高效查询:查询速度必须快。
  3. 数据库限制:仅可使用 MySQL 数据库。

已知短链接表已通过 以分组 ID (gId) 为分片键进行了分表处理。每张分表的数据量均达到万级。同时,当前磁盘空间较为紧张。


二、分析#

该需求初看简单,实则实现难度较高,核心挑战在于避免深分页并确保快速响应。

难点主要源于跨表查询:在的场景下,要精确获取pageSize条严格按修改时间倒序排列的数据非常困难,因为无法预判每个分表需要查询多少数据才能满足全局排序。

此外,还存在以下硬性约束:

  1. 仅限使用 MySQL,排除了引入 Elasticsearch 等外部方案的可能;
  2. 当前磁盘空间紧张,限制了空间换取时间思想的解决方案的应用。

(一)方案一#

既然问题在于跨表查询,那我们就从根本处解决问题,不跨表就好了。

我们可以建立一张映射表,存储短链接id、gId和updateTime,对gid和updateTime建立联合索引避免回表查询。

这样一来我们只要用这条就可以快速查询到下一页数据了:

SELECT id, gid
FROM linkId_and_gid
WHERE updateTime < #{lastUpdateTime}
ORDER BY updateTime DESC
LIMIT #{pageSize}

其中 #{lastUpdateTime} 为上一页最后一条记录的修改时间(游标)。

但是这样的话该表的数据量就会异常庞大,最后还是会陷入深分页问题。

为缓解深分页问题,可考虑在映射表中加入 userId 字段并以其作为新的分片键。然而,此方案需要额外创建一张体量巨大的分片表,显著增加存储开销,与当前磁盘空间紧张的约束严重冲突,故不可行。

(二)方案二#

鉴于无法采用空间换时间的策略,我们转而考虑以一定妥协换取性能提升。

根据用户的每个分组gId,分别到对应的分表里查询PageSize条符合条件的数据返回,具体SQL如下:

SELECT *
FROM links
WHERE updateTime > #{lastUpdateTime}
AND gid = #{gid}
ORDER BY updateTime desc
LIMIT #{pageSize}

然后将查到的数据汇总,通过排序选出前PageSize条给返回即可,注意要给gId和updateTime建立联合索引。

但是当用户分组数量极大,查询返回的总数据量 (分组数 * pageSize) 可能非常庞大,存在风险。

为避免 OOM,可引入优先队列进行流式:

  • 队列容量固定为pageSize + 1。
  • 每次从各分片结果集中取出当前最大值加入队列。
  • 若队列满,则弹出最小值,始终保持队列中为当前已处理数据中的前pageSize + 1大项。
  • 此方法确保内存中仅需维护pageSize  + 1个元素,有效控制内存占用。

什么是流式处理? 流式处理是一种数据持续到达时即时处理的计算范式,与传统的等待数据全部到达后全量处理形成鲜明对比。 维度全量处理流式处理数据视野全量数据加载到内存仅处理当前数据片段处理时机数据完整收集后处理数据到达即时处理内存开销O(全数据集大小) → 可能OOMO(结果集大小) → 恒定安全结果产出结束时统一输出过程持续优化结果延迟特性高延迟(等待所有数据)低延迟(第一条结果快速产出)

当然除此之外,我们还需要通过限制最大Pagesize、用户最大可创建分组数以及每个分组最大可创建短链接数来进行数据规模的把控。

那这里的主要耗时操作就在于DB查询以及排序操作上了。

由于DB的数据量级非常庞大,所以对于DB查询我们可以使用虚拟线程 + structureTaskScope来并行游标查询;排序的话我们使用优先队列进行多路归并排序即可。

若对虚拟线程这类新特性还不了解的请阅读下面这篇博客: 带你轻松学习虚拟线程和StructuredTaskScope-CSDN博客https://blog.csdn.net/2401_88959292/article/details/150288495?spm=1001.2014.3001.5501

流程图如下:

经测试该接口在每个分表均为万级数据、pageSize设置为30的情况下响应时间均为毫秒级。


三、方案实现#

@Slf4j
@Service
@RequiredArgsConstructor
public class LinkServiceImpl implements LinkService {
private final ShortLinkMapper shortLinkMapper;
@Override
public IPage<ShortLinkDO> pageShortLink(List<Long> gidList, Date lastUpdateTime, int pageSize) {
// 创建优先队列,供控制器使用
PriorityQueue<ShortLinkDO> minHeap = new PriorityQueue<>(
Comparator.comparing(ShortLinkDO::getUpdateTime)
.thenComparingLong(ShortLinkDO::getId)
);
// 创建流式处理控制器
try (StreamProcessor processor = new StreamProcessor(gidList, minHeap, pageSize)) {
// 并行处理每个分组
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
List<StructuredTaskScope.Subtask<Void>> futures = gidList.stream()
.map(gid -> scope.fork((Callable<Void>) () -> {
processGroupStream(gid, lastUpdateTime, processor);
return null;
}))
.toList();
// 等待所有任务完成
scope.join();
// 抛异常
scope.throwIfFailed();
}
} catch (Exception e) {
log.error("创建流式控制处理器失败");
throw new RuntimeException(e);
}
// 构建最终结果
List<ShortLinkDO> result = new ArrayList<>(minHeap);
result.sort(Comparator.comparing(ShortLinkDO::getUpdateTime)
.thenComparingLong(ShortLinkDO::getId)
.reversed());
// 构建分页响应
Page<ShortLinkDO> page = new Page<>(1, pageSize);
page.setRecords(result);
page.setTotal(result.size());
return page;
}
// 流式处理每个分组的数据
private void processGroupStream(Long gid, Date lastUpdateTime, StreamProcessor processor) {
int offset = 0;
// 每次查询的批次大小
final int batchSize = 50;
// 游标查询
while (true) {
List<ShortLinkDO> batch = shortLinkMapper.pageShortLink(gid, lastUpdateTime, batchSize, offset);
// 终止条件
if (batch.isEmpty()){
break;
}
// 处理当前批次
for (ShortLinkDO item : batch) {
if (!processor.process(item)) {
return;
}
}
// 通知完成一个分组
offset += batchSize;
}
}
// 流式处理控制器
private static class StreamProcessor implements AutoCloseable {
// 优先队列
private final PriorityQueue<ShortLinkDO> minHeap;
// 堆容量
private final int heapCapacity;
// 分组同步器
private final Phaser phaser = new Phaser(1);
// 是否继续处理
private volatile boolean shouldContinue = true;
// 每个请求单独创建一个控制器,所以不用担心线程安全问题
public StreamProcessor(List<Long> gidList,
PriorityQueue<ShortLinkDO> minHeap,
int heapCapacity) {
this.minHeap = minHeap;
this.heapCapacity = heapCapacity;
phaser.bulkRegister(gidList.size());
}
// 处理单个记录,直接用同步块保障线程安全,因为这里只需要防住每个请求自己创建的线程,与其他线程无关
public synchronized boolean process(ShortLinkDO item) {
// 停止处理
if (!shouldContinue) {
return false;
}
// 添加到堆中
minHeap.offer(item);
// 维持堆大小
if (minHeap.size() > heapCapacity) {
// 移除最小元素
minHeap.poll();
}
return true;
}
@Override
public void close() {
try {
// 等待所有分组完成
phaser.arriveAndAwaitAdvance();
} finally {
// 停止处理新数据
shouldContinue = false;
}
}
}
}

我的水平有限,只能想到这两个方案,如果你有更好的方案,请在评论区中告诉我!


码文不易,留个赞再走吧


原文链接: 优先队列流式处理 + 多路归并排序:轻松实现DB分表严格有序的分页查询 作者: Yilena

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

优先队列流式处理 + 多路归并排序:轻松实现DB分表严格有序的分页查询
https://blog.csdn.net/2401_88959292/article/details/150618375?spm=1001.2014.3001.5501
作者
Yilena
发布于
2025-08-22
许可协议
CC BY 4.0

部分信息可能已经过时

相关文章 智能推荐
1
优化日志分析店铺推荐方案:用户范围的精确度以及ES与MySQL的查询效率差异
业务拆解 本文针对点餐场景下的店铺推荐方案进行了深度优化。首先,通过Redis记录用户月度登录天数,精准筛选出热点用户,解决了旧版方案中推荐用户范围不精确的问题。其次,对比了Elasticsearch与MySQL在十万级数据量下的查询效率,将热数据迁移至MySQL并建立索引以提升查询性能,同时保留ES作为冷数据备份。文章详细展示了优化后的业务流程图及Java代码实现,包括基于Lua脚本的Redis原子操作、多线程并行处理用户日志分析以及加权评分推荐算法,为高并发场景下的日志分析与个性化推荐提供了高效的工程实践。
2
有关慢查询SQL优化的思路
技术笔记 本文系统总结了MySQL慢查询SQL的定位方法与优化思路。首先介绍了如何通过命令行或配置文件开启慢查询日志,并使用mysqldumpslow工具进行日志分析。随后,从六个核心维度深入剖析了慢查询的常见原因及优化策略:包括索引的合理设置与失效规避、SQL语句的精简与重构(如避免冗余字段、优化JOIN与排序)、针对不同数据量级的查询方案(如延迟关联与游标查询)、表结构设计的规范(如主键设置、分库分表)、业务逻辑的批量与并行处理,以及关键系统参数的调优建议,为开发者提供了一套全面且实用的数据库性能提升指南。
3
破局双维度查询难题:淘宝订单ID的用户基因设计奥秘
业务拆解 本文深入剖析了淘宝订单ID中"用户基因"设计的奥秘,揭示了其如何巧妙破解海量数据下基于userId与orderId的双维度查询难题。文章详细解析了订单ID的结构(时间序列+订单类型+用户基因段),对比了传统分片方案的痛点,并阐述了通过单向哈希加盐提取用户基因、结合布隆过滤器防冲突的具体实现流程。该方案不仅避免了全表扫描与冗余表维护的高昂成本,还优化了Snowflake算法,为分布式系统中的高效路由与ID生成提供了极具价值的参考。
4
布隆过滤器因内存上限无法处理超量数据的优化方案
业务拆解 本文针对布隆过滤器因数据量增加导致内存上限不足的问题,深入分析并提出了三种优化方案。首先是分片布隆过滤器,通过哈希取模将数据分散,简单高效但受限于单机内存且易引发GC问题;其次是分布式布隆过滤器,借助Redis摆脱单机限制,并结合一致性哈希实现分片以优化缓存空间;最后是可扩展布隆过滤器,通过动态增加层数和容量来应对数据增长,但查询效率会随层数增加而降低。综合对比,推荐单机项目使用分片方案,分布式架构采用Redis分布式分片方案,以有效解决超量数据处理难题。
5
优化:将针对单一日志表的冷热数据分离类改造成通用类
业务拆解 本文记录了将针对单一日志表的冷热数据分离逻辑重构为通用类的优化过程。为解决旧版方案中代码冗余及复用性差的问题,新方案通过参数化固定逻辑、引入泛型机制适配不同数据实体,并为不同日志表提供专属的Elasticsearch插入重载函数,实现了高度定制化与通用性的统一。文章详细展示了重构后的具体流程图及Java代码实现,包括利用CompletableFuture进行多线程并行处理、基于游标的批量数据查询与迁移、以及完善的重试与异常处理机制,有效提升了海量日志数据冷热分离任务的执行效率与代码可维护性。

目录

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00