偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

剖析 sharding-jdbc 如何實(shí)現(xiàn)分頁(yè)查詢(xún)

開(kāi)發(fā)
本文從日常使用的角度出發(fā)來(lái)剖析一下sharding-jdbc底層是如何實(shí)現(xiàn)分頁(yè)查詢(xún)的,希望對(duì)你有幫助。

在之前的文章中筆者簡(jiǎn)單的介紹了sharding-jdbc的使用,而本文從日常使用的角度出發(fā)來(lái)剖析一下sharding-jdbc底層是如何實(shí)現(xiàn)分頁(yè)查詢(xún)的。

前置依賴(lài)引入

之前的文章已經(jīng)介紹過(guò)sharding-jdbc底層會(huì)通過(guò)重寫(xiě)數(shù)據(jù)源對(duì)應(yīng)的prepareStament完成分表查詢(xún)邏輯,而分頁(yè)插件則是攔截SQL語(yǔ)句實(shí)現(xiàn)分頁(yè)查詢(xún),所以使用sharding-jdbc進(jìn)行分頁(yè)查詢(xún)只需引入用戶(hù)所需的分頁(yè)插件即可,以筆者為例,這里就直接使用pagehelper:

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

分頁(yè)查詢(xún)代碼示例

本文中筆者配置的分頁(yè)算法是通過(guò)id取模的方式,假設(shè)我們的對(duì)應(yīng)的user數(shù)據(jù)id為1,按照我們的算法,它將被存至1%3=1即user_1表:

##使用哪一列用作計(jì)算分表策略,我們就使用id
spring.shardingsphere.sharding.tables.user.table-strategy.inline.sharding-column=id
##具體的分表路由策略,我們有3個(gè)user表,使用主鍵id取余3,余數(shù)0/1/2分表對(duì)應(yīng)表user_0,user_2,user_2
spring.shardingsphere.sharding.tables.user.table-strategy.inline.algorithm-expression=user_$->{id % 3}

筆者在實(shí)驗(yàn)表中插入大約100w的數(shù)據(jù),進(jìn)行一次分頁(yè)查詢(xún),其中分頁(yè)算法為id%3

@Test
    void selectByPage() {
        //查詢(xún)第2頁(yè)的數(shù)據(jù)10條
        PageHelper.startPage(2, 10, false);

        //查詢(xún)結(jié)果按照id升序排列
        UserExample userExample = new UserExample();
        userExample.setOrderByClause("id asc");
        //輸出查詢(xún)結(jié)果
        List<User> userList = userMapper.selectByExample(userExample);
        userList.forEach(System.out::println);

    }

最終結(jié)果如下,可以看到查詢(xún)結(jié)果和單表情況下是一樣的,即從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對(duì)于分頁(yè)查詢(xún)的底層實(shí)現(xiàn)

按照正常的單表查詢(xún)邏輯,假設(shè)我們要查詢(xún)第2頁(yè)的數(shù)據(jù)10條,我們對(duì)應(yīng)的SQL就是:

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

而sharding-jdbc分表分頁(yè)查詢(xún)則比較粗暴,它會(huì)將對(duì)應(yīng)分頁(yè)及之前的數(shù)據(jù)全部查詢(xún)來(lái),然后進(jìn)行排序,跳過(guò)對(duì)應(yīng)頁(yè)碼的數(shù)據(jù)后,再取出對(duì)應(yīng)量級(jí)的數(shù)據(jù)返回。

以我們的分頁(yè)查詢(xún)?yōu)槔鼤?huì)將每個(gè)分表的按照id進(jìn)行升序排列之后取出各自的前20條數(shù)據(jù),每張分表前20條數(shù)據(jù)之后,sharding-jdbc會(huì)根據(jù)我們的排序算法比對(duì)各張分表的第一條數(shù)據(jù),很明顯user_1對(duì)應(yīng)的結(jié)果最小,所以按照此規(guī)則輪詢(xún)分表的user_1、user_2、user_0以此將這3組結(jié)果存放至優(yōu)先隊(duì)列中。

基于這個(gè)隊(duì)列,sharding-jdbc會(huì)按照分頁(yè)查詢(xún)的邏輯跳過(guò)10個(gè),所以它會(huì)不斷取出優(yōu)先隊(duì)列中的第一個(gè)元素,然后將這組分表結(jié)果再次存回隊(duì)列,以我們的查詢(xún)?yōu)槔褪?

  • 從user_1取出id為1的值,作為skip的第一個(gè)元素。
  • 將user_1查詢(xún)結(jié)果入隊(duì),因?yàn)轭^元素為4,和其他兩組比最大,所以存放至隊(duì)尾。
  • 再次從優(yōu)先隊(duì)列中拿到user_2的隊(duì)首元素2,作為skip的第2個(gè)元素,然后再次存入隊(duì)尾。
  • 依次步驟完成跳過(guò)10個(gè)。
  • 然后再按照這個(gè)規(guī)律篩選出10個(gè),最終得到11~20。

源碼印證分頁(yè)查詢(xún)工作機(jī)制

基于上述的圖解,我們通過(guò)源碼解析方式來(lái)印證,首先mybatis會(huì)基于我們的SQL調(diào)用execute方法獲取查詢(xún)結(jié)果,然后再通過(guò)handleResultSets生成列表并返回。 我們都知道sharding-jdbc通過(guò)自實(shí)現(xiàn)數(shù)據(jù)源的同時(shí)也給出對(duì)應(yīng)的PreparedStatement即ShardingPreparedStatement,所以execute方法本質(zhì)的執(zhí)行者就是ShardingPreparedStatement,它會(huì)得到第2頁(yè)之前的所有數(shù)據(jù),然后通過(guò)handleResultSets進(jìn)行skip和limit得到最終結(jié)果:

@Override
  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    //調(diào)用sharding-jdbc的ShardingPreparedStatement的execute獲取各個(gè)分表前2頁(yè)的所有數(shù)據(jù)
    ps.execute();
    //通過(guò)skip結(jié)合limit得到所有結(jié)果
    return resultSetHandler.handleResultSets(ps);
  }

步入execute方法可以看到其內(nèi)部本質(zhì)是調(diào)用preparedStatementExecutor進(jìn)行查詢(xún)處理的:

@Override
    public boolean execute() throws SQLException {
        try {
            clearPrevious();
            //獲取查詢(xún)SQL
            shard();
            initPreparedStatementExecutor();
            //執(zhí)行SQL結(jié)果并返回
            return preparedStatementExecutor.execute();
        } finally {
            clearBatch();
        }
    }

而該執(zhí)行方法最終會(huì)走到ShardingExecuteEngine的parallelExecute方法,通過(guò)異步查詢(xún)3張分表的結(jié)果,再通過(guò)外部傳入的回調(diào)執(zhí)行器處理這3個(gè)異步任務(wù)的查詢(xún)結(jié)果:

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個(gè)異步任務(wù)
        Collection<ListenableFuture<Collection<O>>> restResultFutures = asyncGroupExecute(Lists.newArrayList(inputGroupsIterator), callback);
        //通過(guò)回調(diào)執(zhí)行器callback阻塞獲取3個(gè)異步結(jié)果
        return getGroupResults(syncGroupExecute(firstInputs, null == firstCallback ? callback : firstCallback), restResultFutures);
    }

得到3張分表的數(shù)據(jù)之后,其內(nèi)部邏輯最終會(huì)走到ShardingPreparedStatement的getResultSet方法,其內(nèi)部會(huì)創(chuàng)建一個(gè)合并引擎DQLMergeEngine進(jìn)行并調(diào)用getCurrentResultSet進(jìn)行數(shù)據(jù)截?。?/p>

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

而該引擎就是DQLMergeEngine,進(jìn)行合并操作時(shí),會(huì)調(diào)用LimitDecoratorMergedResult跳過(guò)前10個(gè)元素:

private MergedResult decorate(final MergedResult mergedResult) throws SQLException {
        Limit limit = routeResult.getLimit();
        //......
        //通過(guò)LimitDecoratorMergedResult跳過(guò)3張分表組合結(jié)果的前10個(gè)元素
        if (DatabaseType.MySQL == databaseType || DatabaseType.PostgreSQL == databaseType || DatabaseType.H2 == databaseType) {
            return new LimitDecoratorMergedResult(mergedResult, routeResult.getLimit());
        }
       //......
        return mergedResult;
    }

跳過(guò)的邏輯就比較簡(jiǎn)單了,LimitDecoratorMergedResult會(huì)調(diào)用合并引擎調(diào)用OrderByStreamMergedResult的next方法跳過(guò)前10個(gè)元素:

//LimitDecoratorMergedResult的skipOffset跳過(guò)10個(gè)元素
private boolean skipOffset() throws SQLException {
        for (int i = 0; i < limit.getOffsetValue(); i++) {
        //調(diào)用OrderByStreamMergedResult跳過(guò)組合結(jié)果的前10個(gè)元素
            if (!getMergedResult().next()) {
                return true;
            }
        }
        rowNumber = 0;
        return false;
    }

可以看到OrderByStreamMergedResult的邏輯就是我們上文所說(shuō)的取出隊(duì)列中的第一組查詢(xún)結(jié)果的第一個(gè)元素,然后再將其存入隊(duì)(因?yàn)槿〕龅谝粋€(gè)元素后,隊(duì)首元素最大,這組結(jié)果會(huì)存至隊(duì)尾),不斷循環(huán)跳夠10個(gè):

@Override
    public boolean next() throws SQLException {
       //......
       //取出隊(duì)列中第一組分表查詢(xún)結(jié)果的第一個(gè)元素
        OrderByValue firstOrderByValue = orderByValuesQueue.poll();
        //如果這組分表結(jié)果還有元素則將這組分表結(jié)果入隊(duì),因?yàn)殛?duì)首元素最大,所以會(huì)存放至隊(duì)尾
        if (firstOrderByValue.next()) {
            orderByValuesQueue.offer(firstOrderByValue);
        }
       //......
        return true;
    }

經(jīng)過(guò)上述步驟跳過(guò)10個(gè)元素后,就要截取第二頁(yè)的10個(gè)數(shù)據(jù)了,代碼再次回到PreparedStatementHandler的handleResultSets方法,該方法會(huì)調(diào)用到DefaultResultSetHandler的handleRowValuesForSimpleResultMap方法,該方法會(huì)循環(huán)10個(gè),通過(guò)resultSet.next()移到下一條數(shù)據(jù)的游標(biāo),然后生成對(duì)象存儲(chǔ)到resultHandler中,最終通過(guò)這個(gè)resultHandler就可以看到我們分頁(yè)查詢(xún)的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);
    //通過(guò)resultSet.next()方法調(diào)用
    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方法本質(zhì)還是調(diào)用LimitDecoratorMergedResult的next方法,以rowNumber 來(lái)計(jì)數(shù),調(diào)用mergedResult的next方法將游標(biāo)移動(dòng)到要返回的數(shù)據(jù),

@Override
    public boolean next() throws SQLException {
       //......
       
        //同樣基于優(yōu)先隊(duì)列取夠10個(gè)
        return ++rowNumber <= limit.getRowCountValue() && getMergedResult().next();
    }

而OrderByStreamMergedResult的next邏輯和之前差不多,就是通過(guò)輪詢(xún)優(yōu)先隊(duì)列中的每一組分表對(duì)象的隊(duì)首元素,將其存到currentQueryResult中,后續(xù)進(jìn)行對(duì)象創(chuàng)建時(shí)就會(huì)從currentQueryResult中拿到這個(gè)結(jié)果生成User對(duì)象存入List中返回:

@Override
    public boolean next() throws SQLException {
     //......
     
        //從優(yōu)先隊(duì)列orderByValuesQueue拿到隊(duì)首的一組分表查詢(xún)結(jié)果
        OrderByValue firstOrderByValue = orderByValuesQueue.poll();
        //移動(dòng)當(dāng)前隊(duì)列游標(biāo)
        if (firstOrderByValue.next()) {
            orderByValuesQueue.offer(firstOrderByValue);
        }
        if (orderByValuesQueue.isEmpty()) {
            return false;
        }
        //將當(dāng)前優(yōu)先隊(duì)列中的隊(duì)首元素的queryResult作為本次的查詢(xún)結(jié)果,作為后續(xù)創(chuàng)建User對(duì)象的數(shù)據(jù)
        setCurrentQueryResult(orderByValuesQueue.peek().getQueryResult());
        return true;
    }

ShardingJDBC 在查詢(xún)的時(shí)候如果沒(méi)有分表鍵會(huì)帶來(lái)什么問(wèn)題

自此我們了解了sharding-jdbc分頁(yè)查詢(xún)的內(nèi)部工作機(jī)制,這里我們順便說(shuō)一下這種算法的缺點(diǎn),查閱官網(wǎng)說(shuō)法是sharding-jdbc分頁(yè)查詢(xún)不會(huì)占用內(nèi)存,說(shuō)明查詢(xún)結(jié)果僅僅記錄的是游標(biāo):

首先,采用流式處理 + 歸并排序的方式來(lái)避免內(nèi)存的過(guò)量占用。由于SQL改寫(xiě)不可避免的占用了額外的帶寬,但并不會(huì)導(dǎo)致內(nèi)存暴漲。 與直覺(jué)不同,大多數(shù)人認(rèn)為ShardingSphere會(huì)將1,000,010 * 2記錄全部加載至內(nèi)存,進(jìn)而占用大量?jī)?nèi)存而導(dǎo)致內(nèi)存溢出。 但由于每個(gè)結(jié)果集的記錄是有序的,因此ShardingSphere每次比較僅獲取各個(gè)分片的當(dāng)前結(jié)果集記錄,駐留在內(nèi)存中的記錄僅為當(dāng)前路由到的分片的結(jié)果集的當(dāng)前游標(biāo)指向而已。 對(duì)于本身即有序的待排序?qū)ο?,歸并排序的時(shí)間復(fù)雜度僅為O(n),性能損耗很小。

但是筆者在使用過(guò)程中,打印內(nèi)存快照時(shí)發(fā)現(xiàn),進(jìn)行500w數(shù)據(jù)的深分頁(yè)查詢(xún)發(fā)現(xiàn),它的做法和我們上文源碼所說(shuō)的一致,就是將當(dāng)前頁(yè)以及之前的結(jié)果全部加載到內(nèi)存中,所以筆者認(rèn)為使用sharding-jdbc時(shí)還是需要注意一下對(duì)內(nèi)存的監(jiān)控:

責(zé)任編輯:趙寧寧 來(lái)源: 寫(xiě)代碼的SharkChili
相關(guān)推薦

2022-05-16 08:50:23

數(shù)據(jù)脫加密器

2025-04-03 09:39:14

2024-03-14 09:30:04

數(shù)據(jù)庫(kù)中間件

2021-10-27 09:55:55

Sharding-Jd分庫(kù)分表Java

2019-09-17 11:18:09

SQLMySQLJava

2023-11-03 09:17:12

數(shù)據(jù)庫(kù)配置庫(kù)表

2020-11-06 15:30:23

分庫(kù)分表Sharding-JD數(shù)據(jù)庫(kù)

2018-12-25 16:30:15

SQL Server高效分頁(yè)數(shù)據(jù)庫(kù)

2009-07-15 17:00:49

JDBC查詢(xún)

2023-07-24 09:00:00

數(shù)據(jù)庫(kù)MyCat

2023-11-17 15:34:03

Redis數(shù)據(jù)庫(kù)

2019-09-11 10:40:49

MySQL大分頁(yè)查詢(xún)數(shù)據(jù)庫(kù)

2010-11-18 13:40:48

mysql分頁(yè)查詢(xún)

2009-08-04 14:23:36

ASP.NET查詢(xún)分頁(yè)

2011-10-10 16:44:37

分頁(yè)數(shù)據(jù)庫(kù)

2009-09-21 13:42:47

Hibernate查詢(xún)

2010-04-16 16:12:51

jdbc分頁(yè)

2010-11-25 14:33:26

MySQL查詢(xún)分頁(yè)

2023-03-13 07:35:44

MyBatis分庫(kù)分表

2010-09-26 15:29:13

sql查詢(xún)分頁(yè)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)