成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

剖析 sharding-jdbc 如何實現分頁查詢

開發
本文從日常使用的角度出發來剖析一下sharding-jdbc底層是如何實現分頁查詢的,希望對你有幫助。

在之前的文章中筆者簡單的介紹了sharding-jdbc的使用,而本文從日常使用的角度出發來剖析一下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=1即user_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_1、user_2、user_0以此將這3組結果存放至優先隊列中。

基于這個隊列,sharding-jdbc會按照分頁查詢的邏輯跳過10個,所以它會不斷取出優先隊列中的第一個元素,然后將這組分表結果再次存回隊列,以我們的查詢為例就是:

  • 從user_1取出id為1的值,作為skip的第一個元素。
  • 將user_1查詢結果入隊,因為頭元素為4,和其他兩組比最大,所以存放至隊尾。
  • 再次從優先隊列中拿到user_2的隊首元素2,作為skip的第2個元素,然后再次存入隊尾。
  • 依次步驟完成跳過10個。
  • 然后再按照這個規律篩選出10個,最終得到11~20。

源碼印證分頁查詢工作機制

基于上述的圖解,我們通過源碼解析方式來印證,首先mybatis會基于我們的SQL調用execute方法獲取查詢結果,然后再通過handleResultSets生成列表并返回。 我們都知道sharding-jdbc通過自實現數據源的同時也給出對應的PreparedStatement即ShardingPreparedStatement,所以execute方法本質的執行者就是ShardingPreparedStatement,它會得到第2頁之前的所有數據,然后通過handleResultSets進行skip和limit得到最終結果:

@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();
        }
    }

而該執行方法最終會走到ShardingExecuteEngine的parallelExecute方法,通過異步查詢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張分表的數據之后,其內部邏輯最終會走到ShardingPreparedStatement的getResultSet方法,其內部會創建一個合并引擎DQLMergeEngine進行并調用getCurrentResultSet進行數據截?。?/p>

@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會調用合并引擎調用OrderByStreamMergedResult的next方法跳過前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個數據了,代碼再次回到PreparedStatementHandler的handleResultSets方法,該方法會調用到DefaultResultSetHandler的handleRowValuesForSimpleResultMap方法,該方法會循環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();
    }

而OrderByStreamMergedResult的next邏輯和之前差不多,就是通過輪詢優先隊列中的每一組分表對象的隊首元素,將其存到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;
    }

ShardingJDBC 在查詢的時候如果沒有分表鍵會帶來什么問題

自此我們了解了sharding-jdbc分頁查詢的內部工作機制,這里我們順便說一下這種算法的缺點,查閱官網說法是sharding-jdbc分頁查詢不會占用內存,說明查詢結果僅僅記錄的是游標:

首先,采用流式處理 + 歸并排序的方式來避免內存的過量占用。由于SQL改寫不可避免的占用了額外的帶寬,但并不會導致內存暴漲。 與直覺不同,大多數人認為ShardingSphere會將1,000,010 * 2記錄全部加載至內存,進而占用大量內存而導致內存溢出。 但由于每個結果集的記錄是有序的,因此ShardingSphere每次比較僅獲取各個分片的當前結果集記錄,駐留在內存中的記錄僅為當前路由到的分片的結果集的當前游標指向而已。 對于本身即有序的待排序對象,歸并排序的時間復雜度僅為O(n),性能損耗很小。

但是筆者在使用過程中,打印內存快照時發現,進行500w數據的深分頁查詢發現,它的做法和我們上文源碼所說的一致,就是將當前頁以及之前的結果全部加載到內存中,所以筆者認為使用sharding-jdbc時還是需要注意一下對內存的監控:

責任編輯:趙寧寧 來源: 寫代碼的SharkChili
相關推薦

2022-05-16 08:50:23

數據脫加密器

2025-04-03 09:39:14

2021-10-27 09:55:55

Sharding-Jd分庫分表Java

2024-03-14 09:30:04

數據庫中間件

2019-09-17 11:18:09

SQLMySQLJava

2023-11-03 09:17:12

數據庫配置庫表

2020-11-06 15:30:23

分庫分表Sharding-JD數據庫

2018-12-25 16:30:15

SQL Server高效分頁數據庫

2009-07-15 17:00:49

JDBC查詢

2023-07-24 09:00:00

數據庫MyCat

2023-11-17 15:34:03

Redis數據庫

2019-09-11 10:40:49

MySQL大分頁查詢數據庫

2009-09-21 13:42:47

Hibernate查詢

2010-11-18 13:40:48

mysql分頁查詢

2011-10-10 16:44:37

分頁數據庫

2009-08-04 14:23:36

ASP.NET查詢分頁

2010-04-16 16:12:51

jdbc分頁

2010-11-25 14:33:26

MySQL查詢分頁

2010-09-26 15:29:13

sql查詢分頁

2023-03-13 07:35:44

MyBatis分庫分表
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 日韩免费网站 | 久久精品一级 | 国产精品美女久久久久aⅴ国产馆 | 成人精品视频免费 | 韩日在线观看视频 | 日本不卡一区 | 欧美综合一区 | 精品日韩一区 | 精品二区视频 | www亚洲一区 | 日韩精品专区在线影院重磅 | 亚洲天堂久久 | 久热精品在线 | 欧美一级久久 | 777zyz色资源站在线观看 | 亚洲成人网在线播放 | 国产在线视频一区二区 | 亚洲国产精品久久久久秋霞不卡 | 欧美一级欧美三级在线观看 | 尹人av| 日日噜噜噜夜夜爽爽狠狠视频, | 国产a视频 | 男人的天堂亚洲 | 中文字幕一区二区三区精彩视频 | www.蜜桃av | 国产欧美精品一区二区 | 日韩视频在线播放 | 亚洲精品电影在线观看 | 色综合网站| 九九色综合 | h网站在线观看 | 成人黄色电影在线观看 | av网站在线播放 | 成人高清在线 | 成人在线免费观看av | 大伊人久久 | 亚洲视频免费 | 一区二区中文字幕 | 麻豆一区二区三区 | 精品蜜桃一区二区三区 | 国产精品视频不卡 |