sharding-jdbc如何实现分页查询

news2024/10/6 16:28:06

写在文章开头

在之前的文章中笔者简单的介绍了sharding-jdbc的使用,而本文从日常使用的角度出发来剖析一下sharding-jdbc底层是如何实现分页查询的。

在这里插入图片描述

Hi,我是 sharkChili ,是个不断在硬核技术上作死的 java coder ,是 CSDN的博客专家 ,也是开源项目 Java Guide 的维护者之一,熟悉 Java 也会一点 Go ,偶尔也会在 C源码 边缘徘徊。写过很多有意思的技术博客,也还在研究并输出技术的路上,希望我的文章对你有帮助,非常欢迎你关注我的公众号: 写代码的SharkChili

因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

详解sharding-jdbc分页查询

前置步骤

之前的文章已经介绍过sharding-jdbc底层会通过重写数据源对应的prepareStament完成分表查询逻辑,而分页插件则是拦截SQL语句实现分页查询,所以使用sharding-jdbc进行分页查询只需引入用户所需的分页插件即可,以笔者为例,这里就直接使用pagehelper

 	    <!-- pagehelper 插件-->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
        </dependency>

分页查询代码示例

本文中笔者配置的分页算法是通过id取模的方式,假设我们的对应的user数据id为1,按照我们的算法,它将被存至1%3=1user_1表:

##使用哪一列用作计算分表策略,我们就使用id
spring.shardingsphere.sharding.tables.user.table-strategy.inline.sharding-column=id
##具体的分表路由策略,我们有3个user表,使用主键id取余3,余数0/1/2分表对应表user_0,user_2,user_2
spring.shardingsphere.sharding.tables.user.table-strategy.inline.algorithm-expression=user_$->{id % 3}

笔者在实验表中插入大约100w的数据,进行一次分页查询,其中分页算法为id%3

@Test
    void selectByPage() {
        //查询第2页的数据10条
        PageHelper.startPage(2, 10, false);

        //查询结果按照id升序排列
        UserExample userExample = new UserExample();
        userExample.setOrderByClause("id asc");
        //输出查询结果
        List<User> userList = userMapper.selectByExample(userExample);
        userList.forEach(System.out::println);

    }

最终结果如下,可以看到查询结果和单表情况下是一样的,即从11~20

User(id=11, name=user11, phone=)
User(id=12, name=user12, phone=)
User(id=13, name=user13, phone=)
User(id=14, name=user14, phone=)
User(id=15, name=user15, phone=)
User(id=16, name=user16, phone=)
User(id=17, name=user17, phone=)
User(id=18, name=user18, phone=)
User(id=19, name=user19, phone=)
User(id=20, name=user20, phone=)

详解sharding-jdbc对于分页查询的底层实现

按照正常的单表查询逻辑,假设我们要查询第2页的数据10条,我们对应的SQL就是:

select * from user limit (page-1)*10,size =>select * from user limit 10,10

sharding-jdbc分表分页查询则比较粗暴,它会将对应分页及之前的数据全部查询来,然后进行排序,跳过对应页码的数据后,再取出对应量级的数据返回。

以我们的分页查询为例,它会将每个分表的按照id进行升序排列之后取出各自的前20条数据,每张分表前20条数据之后,sharding-jdbc会根据我们的排序算法比对各张分表的第一条数据,很明显user_1对应的结果最小,所以按照此规则轮询分表的user_1user_2user_0以此将这3组结果存放至优先队列中。

基于这个队列,sharding-jdbc会按照分页查询的逻辑跳过10个,所以它会不断取出优先队列中的第一个元素,然后将这组分表结果再次存回队列,以我们的查询为例就是:

  1. user_1取出id为1的值,作为skip的第一个元素。
  2. user_1查询结果入队,因为头元素为4,和其他两组比最大,所以存放至队尾。
  3. 再次从优先队列中拿到user_2的队首元素2,作为skip的第2个元素,然后再次存入队尾。
  4. 依次步骤完成跳过10个。
  5. 然后再按照这个规律筛选出10个,最终得到11~20。

在这里插入图片描述

源码印证

基于上述的图解,我们通过源码解析方式来印证,首先mybatis会基于我们的SQL调用execute方法获取查询结果,然后再通过handleResultSets生成列表并返回。
我们都知道sharding-jdbc通过自实现数据源的同时也给出对应的PreparedStatementShardingPreparedStatement,所以execute方法本质的执行者就是ShardingPreparedStatement,它会得到第2页之前的所有数据,然后通过handleResultSets进行skiplimit得到最终结果:

@Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //调用sharding-jdbc的ShardingPreparedStatement的execute获取各个分表前2页的所有数据
    ps.execute();
    //通过skip结合limit得到所有结果
    return resultSetHandler.handleResultSets(ps);
  }

步入execute方法可以看到其内部本质是调用preparedStatementExecutor进行查询处理的:

 @Override
    public boolean execute() throws SQLException {
        try {
            clearPrevious();
            //获取查询SQL
            shard();
            initPreparedStatementExecutor();
            //执行SQL结果并返回
            return preparedStatementExecutor.execute();
        } finally {
            clearBatch();
        }
    }

而该执行方法最终会走到ShardingExecuteEngineparallelExecute方法,通过异步查询3张分表的结果,再通过外部传入的回调执行器处理这3个异步任务的查询结果:


 private <I, O> List<O> parallelExecute(final Collection<ShardingExecuteGroup<I>> inputGroups, final ShardingGroupExecuteCallback<I, O> firstCallback,
                                           final ShardingGroupExecuteCallback<I, O> callback) throws SQLException {
        Iterator<ShardingExecuteGroup<I>> inputGroupsIterator = inputGroups.iterator();
        ShardingExecuteGroup<I> firstInputs = inputGroupsIterator.next();
        //提交3个异步任务
        Collection<ListenableFuture<Collection<O>>> restResultFutures = asyncGroupExecute(Lists.newArrayList(inputGroupsIterator), callback);
        //通过回调执行器callback阻塞获取3个异步结果
        return getGroupResults(syncGroupExecute(firstInputs, null == firstCallback ? callback : firstCallback), restResultFutures);
    }

得到3张分表的数据之后,其内部逻辑最终会走到ShardingPreparedStatementgetResultSet方法,其内部会创建一个合并引擎DQLMergeEngine进行并调用getCurrentResultSet进行数据截取:

@Override
    public ResultSet getResultSet() throws SQLException {
        //......
        if (routeResult.getSqlStatement() instanceof SelectStatement || routeResult.getSqlStatement() instanceof DALStatement) {
        //反射创建分表合并引擎
            MergeEngine mergeEngine = MergeEngineFactory.newInstance(connection.getShardingContext().getDatabaseType(),
                    connection.getShardingContext().getShardingRule(), routeResult, connection.getShardingContext().getMetaData().getTable(), queryResults);
             //截取最终结果
            currentResultSet = getCurrentResultSet(resultSets, mergeEngine);
        }
        return currentResultSet;
    }

而该引擎就是DQLMergeEngine,进行合并操作时,会调用LimitDecoratorMergedResult跳过前10个元素:

private MergedResult decorate(final MergedResult mergedResult) throws SQLException {
        Limit limit = routeResult.getLimit();
        //......
        //通过LimitDecoratorMergedResult跳过3张分表组合结果的前10个元素
        if (DatabaseType.MySQL == databaseType || DatabaseType.PostgreSQL == databaseType || DatabaseType.H2 == databaseType) {
            return new LimitDecoratorMergedResult(mergedResult, routeResult.getLimit());
        }
       //......
        return mergedResult;
    }

跳过的逻辑就比较简单了,LimitDecoratorMergedResult会调用合并引擎调用OrderByStreamMergedResultnext方法跳过前10个元素:

//LimitDecoratorMergedResult的skipOffset跳过10个元素
private boolean skipOffset() throws SQLException {
        for (int i = 0; i < limit.getOffsetValue(); i++) {
        //调用OrderByStreamMergedResult跳过组合结果的前10个元素
            if (!getMergedResult().next()) {
                return true;
            }
        }
        rowNumber = 0;
        return false;
    }

可以看到OrderByStreamMergedResult的逻辑就是我们上文所说的取出队列中的第一组查询结果的第一个元素,然后再将其存入队(因为取出第一个元素后,队首元素最大,这组结果会存至队尾),不断循环跳够10个:

@Override
    public boolean next() throws SQLException {
       //......
       //取出队列中第一组分表查询结果的第一个元素
        OrderByValue firstOrderByValue = orderByValuesQueue.poll();
        //如果这组分表结果还有元素则将这组分表结果入队,因为队首元素最大,所以会存放至队尾
        if (firstOrderByValue.next()) {
            orderByValuesQueue.offer(firstOrderByValue);
        }
       //......
        return true;
    }

经过上述步骤跳过10个元素后,就要截取第二页的10个数据了,代码再次回到PreparedStatementHandlerhandleResultSets方法,该方法会调用到DefaultResultSetHandlerhandleRowValuesForSimpleResultMap方法,该方法会循环10个,通过resultSet.next()移到下一条数据的游标,然后生成对象存储到resultHandler中,最终通过这个resultHandler就可以看到我们分页查询的List:

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    ResultSet resultSet = rsw.getResultSet();
    skipRows(resultSet, rowBounds);
    //通过resultSet.next()方法调用
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
  }

而next方法本质还是调用LimitDecoratorMergedResult的next方法,以rowNumber 来计数,调用mergedResult的next方法将游标移动到要返回的数据,

@Override
    public boolean next() throws SQLException {
       //......
       
        //同样基于优先队列取够10个
        return ++rowNumber <= limit.getRowCountValue() && getMergedResult().next();
    }

OrderByStreamMergedResultnext逻辑和之前差不多,就是通过轮询优先队列中的每一组分表对象的队首元素,将其存到currentQueryResult中,后续进行对象创建时就会从currentQueryResult中拿到这个结果生成User对象存入List中返回:

@Override
    public boolean next() throws SQLException {
    	//......
    	
        //从优先队列orderByValuesQueue拿到队首的一组分表查询结果
        OrderByValue firstOrderByValue = orderByValuesQueue.poll();
        //移动当前队列游标
        if (firstOrderByValue.next()) {
            orderByValuesQueue.offer(firstOrderByValue);
        }
        if (orderByValuesQueue.isEmpty()) {
            return false;
        }
        //将当前优先队列中的队首元素的queryResult作为本次的查询结果,作为后续创建User对象的数据
        setCurrentQueryResult(orderByValuesQueue.peek().getQueryResult());
        return true;
    }

存在的问题

自此我们了解了sharding-jdbc分页查询的内部工作机制,这里我们顺便说一下这种算法的缺点,查阅官网说法是sharding-jdbc分页查询不会占用内存,说明查询结果仅仅记录的是游标:

首先,采用流式处理 + 归并排序的方式来避免内存的过量占用。由于SQL改写不可避免的占用了额外的带宽,但并不会导致内存暴涨。 与直觉不同,大多数人认为ShardingSphere会将1,000,010 * 2记录全部加载至内存,进而占用大量内存而导致内存溢出。 但由于每个结果集的记录是有序的,因此ShardingSphere每次比较仅获取各个分片的当前结果集记录,驻留在内存中的记录仅为当前路由到的分片的结果集的当前游标指向而已。 对于本身即有序的待排序对象,归并排序的时间复杂度仅为O(n),性能损耗很小。

但是笔者在使用过程中,打印内存快照时发现,进行500w数据的深分页查询发现,它的做法和我们上文源码所说的一致,就是将当前页以及之前的结果全部加载到内存中,所以笔者认为使用sharding-jdbc时还是需要注意一下对内存的监控:

在这里插入图片描述

小结

以上便是笔者对于sharding-jdbc分页查询的全部解析,希望对你有帮助。

我是 sharkchiliCSDN Java 领域博客专家开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili
因为近期收到很多读者的私信,所以也专门创建了一个交流群,感兴趣的读者可以通过上方的公众号获取笔者的联系方式完成好友添加,点击备注 “加群” 即可和笔者和笔者的朋友们进行深入交流。

在这里插入图片描述

参考

ShardingSphere分页查询官方文档:https://shardingsphere.apache.org/document/4.1.1/cn/features/sharding/use-norms/pagination/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1659747.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

私域流量优化:如何利用 AIPL 模型洞察客户生命周期价值

在当今这个数字化时代&#xff0c;商业战场的硝烟从未如此浓烈。随着互联网红利的逐渐消退&#xff0c;公域流量的成本水涨船高&#xff0c;企业间对于有限用户资源的争夺已进入白热化阶段。每一次点击、每一个曝光背后&#xff0c;都是企业不得不承担的高昂代价。在此背景下&a…

1725 ssm资产管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计

一、源码特点 java ssm资产管理系统是一套完善的web设计系统&#xff08;系统采用SSM框架进行设计开发&#xff0c;springspringMVCmybatis&#xff09;&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/…

本安防爆手机在电力行业中的应用

在电力行业这一充满挑战与风险的领域中&#xff0c;安全始终是最为首要的考量。电力巡检、维修等作业往往涉及易燃、易爆环境&#xff0c;这就要求工作人员配备能够在极端条件下保障通讯和作业安全的专业设备。防爆手机应运而生&#xff0c;以其独特的设计和卓越的性能&#xf…

Kafka 执行命令超时异常: Timed out waiting for a node assignment

Kafka 执行命令超时异常&#xff1a; Timed out waiting for a node assignment 问题描述&#xff1a; 搭建了一个kafka集群环境&#xff0c;在使用命令行查看已有topic时&#xff0c;报错如下&#xff1a; [rootlocalhost bin]# kafka-topics.sh --list --bootstrap-server…

es关闭开启除了系统索引以外的所有索引

1、es 开启 “删除或关闭时索引名称支持通配符” 功能 2、kibanan平台执行 POST *,-.*/_close 关闭索引POST *,-.*/_open 打开索引3、其他命令 DELETE index_* // 按通配符删除以index_开头的索引 DELETE _all // 删除全部索引 DELETE *,-.* 删除全…

产品推荐 | 基于Xilinx Kintex-7 FPGA K7 XC7K325T PCIeX8 四路光纤卡

01 产品概述 板卡主芯片采用Xilinx公司的XC7K325T-2FFG900 FPGA&#xff0c;pin_to_pin兼容FPGAXC7K410T-2FFG900&#xff0c;支持8-Lane PCIe、64bit DDR3、四路SFP连接器、四路SATA接口、内嵌16个高速串行收发器RocketIO GTX&#xff0c;软件具有windows驱动。 02 技术指标…

【数据结构】栈的实现以及数组和链表的优缺点

个人主页&#xff1a;一代… 个人专栏&#xff1a;数据结构 1.栈 1.1栈的概念及结构 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端 称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进…

小程序(三)

十三、自定义组件 &#xff08;二&#xff09;数据方法声明位置 在js文件中 A、数据声明位置&#xff1a;data中 B、方法声明位置methods中&#xff0c;这点和普通页面不同&#xff01; Component({/*** 组件的属性列表*/properties: {},/*** 组件的初始数据*/data: {isCh…

在Ubuntu安装Carla时按照官方的教程将下载好的资源包解压放到Unreal\CarlaUE4\Content\Carla后执行./Update.sh

在Ubuntu安装Carla时按照官方的教程将下载好的资源包解压放到 Unreal\CarlaUE4\Content\Carla后执行./Update.sh 结果出现&#xff0c;将原来的Carla文件夹备份了有创建了一个新的空白Carla文件夹 原来自己下载解压后就不用再执行./Update.sh这个了&#xff0c;这个命令就是…

【Redis分布式缓存】分片集群

Redis 分片集群 搭建分片集群 集群结构 分片集群需要的节点数量较多&#xff0c;这里我们搭建一个最小的分片集群&#xff0c;包含3个master节点&#xff0c;每个master包含一个slave节点&#xff0c;结构如下&#xff1a; 这里我们会在同一台虚拟机中开启6个redis实例&…

逻辑回归和神经网络(原理+应用)

目录 一、背景介绍 二、题目要求 三、逻辑回归&#xff08;Logistic Regression&#xff09;与神经网络 四、输入输出变量 五、效果评估Gains介绍 六、模型构建 具体应用&#xff1a;预测客户是否有意预订有线电视交互服务 一、背景介绍 当今时代&#xff0c;有线电视交…

【JavaEE 初阶(四)】多线程进阶

❣博主主页: 33的博客❣ ▶️文章专栏分类:JavaEE◀️ &#x1f69a;我的代码仓库: 33的代码仓库&#x1f69a; &#x1faf5;&#x1faf5;&#x1faf5;关注我带你了解更多线程知识 目录 1.前言2.常见的锁策略2.1悲观锁vs乐观锁2.2轻量级锁vs重量级锁2.3自旋锁vs挂起锁2.4读写…

JS控制台代码:淘宝PC网页付款页面定时确认付款

淘宝定时抢东西用的 必须先输入完正确密码&#xff0c;考虑上了网络延迟&#xff0c;程序提前一秒钟点击确认&#xff0c;可自行修改&#xff1a; function checkTime() {var now new Date();var hours now.getHours();var minutes now.getMinutes();var seconds now.getS…

线程池核心原理浅析

前言 由于系统资源是有限的&#xff0c;为了降低资源消耗&#xff0c;提高系统的性能和稳定性&#xff0c;引入了线程池对线程进行统一的管理和监控&#xff0c;本文将详细讲解线程池的使用、原理。 为什么使用线程池 池化思想 线程池主要用到了池化思想&#xff0c;池化思想…

vivado 低级别 SVF JTAG 命令

低级别 SVF JTAG 命令 注释 &#xff1a; 在 Versal ™ 器件上不支持 SVF 。 低级别 JTAG 命令允许您扫描多个 FPGA JTAG 链。针对链操作所生成的 SVF 命令使用这些低级别命令来访问链中的 FPGA 。 报头数据寄存器 (HDR) 和报头指令寄存器 (HIR) 语法 HDR length […

健康知识集锦

页面 页面代码 <% layout(/layouts/default.html, {title: 健康知识管理, libs: [dataGrid]}){ %> <div class"main-content"><div class"box box-main"><div class"box-header"><div class"box-title"&g…

CDGA|电子行业数据治理六大痛点及突围之道

CDGA|电子行业数据治理六大痛点及突围之道 随着信息技术的迅猛发展&#xff0c;电子行业对数据的需求和依赖日益增强。然而&#xff0c;数据治理作为确保数据质量、安全性及有效利用的关键环节&#xff0c;在电子行业中却面临着一系列痛点。本文将深入探讨电子行业数据治理的六…

基于LMV358的负电源架构

嘿UU们&#xff0c;中午好啊&#xff01;吃了没&#xff1f;算算时间我的餐桌上应该快上杨梅和鱼胶冻了。 今天看某群&#xff0c;突然想到Jim williams的书里一个架构&#xff0c;但老爷子的东西是正负输出的&#xff0c;而且略微有点麻烦&#xff0c;我就想怎么样整个更适合…

实现网站HTTPS访问:全面指南

在当今网络安全至关重要的时代&#xff0c;HTTPS已经成为网站安全的基本标准。HTTPS&#xff08;超文本传输安全协议&#xff09;通过在HTTP协议基础上加入SSL/TLS加密层&#xff0c;确保了数据在用户浏览器和服务器之间的传输是加密的&#xff0c;有效防止数据被窃取或篡改&am…

专题六_模拟(2)

目录 6. Z 字形变换 解析 题解 38. 外观数列 解析 题解 6. Z 字形变换 6. Z 字形变换 - 力扣&#xff08;LeetCode&#xff09; 解析 题解 class Solution { public:string convert(string s, int numRows) {// 42.专题六_模拟_N 字形变换_C// 处理边界情况if (numRows …