sharding-jdbc實(shí)現(xiàn)分頁查詢的示例代碼
詳解sharding-jdbc分頁查詢
前置步驟
之前的文章已經(jīng)介紹過sharding-jdbc
底層會(huì)通過重寫數(shù)據(jù)源對(duì)應(yīng)的prepareStament
完成分表查詢邏輯,而分頁插件則是攔截SQL
語句實(shí)現(xiàn)分頁查詢,所以使用sharding-jdbc
進(jìn)行分頁查詢只需引入用戶所需的分頁插件即可,以筆者為例,這里就直接使用pagehelper
:
<!-- pagehelper 插件--> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> </dependency>
分頁查詢代碼示例
本文中筆者配置的分頁算法是通過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)行一次分頁查詢,其中分頁算法為id%3
@Test void selectByPage() { //查詢第2頁的數(shù)據(jù)10條 PageHelper.startPage(2, 10, false); //查詢結(jié)果按照id升序排列 UserExample userExample = new UserExample(); userExample.setOrderByClause("id asc"); //輸出查詢結(jié)果 List<User> userList = userMapper.selectByExample(userExample); userList.forEach(System.out::println); }
最終結(jié)果如下,可以看到查詢結(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ì)于分頁查詢的底層實(shí)現(xiàn)
按照正常的單表查詢邏輯,假設(shè)我們要查詢第2頁的數(shù)據(jù)10
條,我們對(duì)應(yīng)的SQL就是:
select * from user limit (page-1)*10,size =>select * from user limit 10,10
而sharding-jdbc
分表分頁查詢則比較粗暴,它會(huì)將對(duì)應(yīng)分頁及之前的數(shù)據(jù)全部查詢來,然后進(jìn)行排序,跳過對(duì)應(yīng)頁碼的數(shù)據(jù)后,再取出對(duì)應(yīng)量級(jí)的數(shù)據(jù)返回。
以我們的分頁查詢?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ī)則輪詢分表的user_1
、user_2
、user_0
以此將這3組結(jié)果存放至優(yōu)先隊(duì)列中。
基于這個(gè)隊(duì)列,sharding-jdbc會(huì)按照分頁查詢的邏輯跳過10個(gè),所以它會(huì)不斷取出優(yōu)先隊(duì)列中的第一個(gè)元素,然后將這組分表結(jié)果再次存回隊(duì)列,以我們的查詢?yōu)槔褪?
- 從
user_1
取出id為1的值,作為skip的第一個(gè)元素。 - 將
user_1
查詢結(jié)果入隊(duì),因?yàn)轭^元素為4,和其他兩組比最大,所以存放至隊(duì)尾。 - 再次從優(yōu)先隊(duì)列中拿到
user_2
的隊(duì)首元素2,作為skip
的第2個(gè)元素,然后再次存入隊(duì)尾。 - 依次步驟完成跳過10個(gè)。
- 然后再按照這個(gè)規(guī)律篩選出10個(gè),最終得到11~20。
源碼印證
基于上述的圖解,我們通過源碼解析方式來印證,首先mybatis
會(huì)基于我們的SQL
調(diào)用execute
方法獲取查詢結(jié)果,然后再通過handleResultSets
生成列表并返回。 我們都知道sharding-jdbc
通過自實(shí)現(xiàn)數(shù)據(jù)源的同時(shí)也給出對(duì)應(yīng)的PreparedStatement
即ShardingPreparedStatement
,所以execute
方法本質(zhì)的執(zhí)行者就是ShardingPreparedStatement
,它會(huì)得到第2頁之前的所有數(shù)據(jù),然后通過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頁的所有數(shù)據(jù) ps.execute(); //通過skip結(jié)合limit得到所有結(jié)果 return resultSetHandler.handleResultSets(ps); }
步入execute
方法可以看到其內(nèi)部本質(zhì)是調(diào)用preparedStatementExecutor
進(jìn)行查詢處理的:
@Override public boolean execute() throws SQLException { try { clearPrevious(); //獲取查詢SQL shard(); initPreparedStatementExecutor(); //執(zhí)行SQL結(jié)果并返回 return preparedStatementExecutor.execute(); } finally { clearBatch(); } }
而該執(zhí)行方法最終會(huì)走到ShardingExecuteEngine
的parallelExecute
方法,通過異步查詢3張分表的結(jié)果,再通過外部傳入的回調(diào)執(zhí)行器處理這3個(gè)異步任務(wù)的查詢結(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); //通過回調(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
跳過前10個(gè)元素:
private MergedResult decorate(final MergedResult mergedResult) throws SQLException { Limit limit = routeResult.getLimit(); //...... //通過LimitDecoratorMergedResult跳過3張分表組合結(jié)果的前10個(gè)元素 if (DatabaseType.MySQL == databaseType || DatabaseType.PostgreSQL == databaseType || DatabaseType.H2 == databaseType) { return new LimitDecoratorMergedResult(mergedResult, routeResult.getLimit()); } //...... return mergedResult; }
跳過的邏輯就比較簡(jiǎn)單了,LimitDecoratorMergedResult
會(huì)調(diào)用合并引擎調(diào)用OrderByStreamMergedResult
的next
方法跳過前10個(gè)元素:
//LimitDecoratorMergedResult的skipOffset跳過10個(gè)元素 private boolean skipOffset() throws SQLException { for (int i = 0; i < limit.getOffsetValue(); i++) { //調(diào)用OrderByStreamMergedResult跳過組合結(jié)果的前10個(gè)元素 if (!getMergedResult().next()) { return true; } } rowNumber = 0; return false; }
可以看到OrderByStreamMergedResult
的邏輯就是我們上文所說的取出隊(duì)列中的第一組查詢結(jié)果的第一個(gè)元素,然后再將其存入隊(duì)(因?yàn)槿〕龅谝粋€(gè)元素后,隊(duì)首元素最大,這組結(jié)果會(huì)存至隊(duì)尾),不斷循環(huán)跳夠10個(gè):
@Override public boolean next() throws SQLException { //...... //取出隊(duì)列中第一組分表查詢結(jié)果的第一個(gè)元素 OrderByValue firstOrderByValue = orderByValuesQueue.poll(); //如果這組分表結(jié)果還有元素則將這組分表結(jié)果入隊(duì),因?yàn)殛?duì)首元素最大,所以會(huì)存放至隊(duì)尾 if (firstOrderByValue.next()) { orderByValuesQueue.offer(firstOrderByValue); } //...... return true; }
經(jīng)過上述步驟跳過10個(gè)元素后,就要截取第二頁的10個(gè)數(shù)據(jù)了,代碼再次回到PreparedStatementHandler
的handleResultSets
方法,該方法會(huì)調(diào)用到DefaultResultSetHandler
的handleRowValuesForSimpleResultMap
方法,該方法會(huì)循環(huán)10個(gè),通過resultSet.next()
移到下一條數(shù)據(jù)的游標(biāo),然后生成對(duì)象存儲(chǔ)到resultHandler
中,最終通過這個(gè)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()方法調(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 來計(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
邏輯和之前差不多,就是通過輪詢優(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ì)首的一組分表查詢結(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作為本次的查詢結(jié)果,作為后續(xù)創(chuàng)建User對(duì)象的數(shù)據(jù) setCurrentQueryResult(orderByValuesQueue.peek().getQueryResult()); return true; }
存在的問題
自此我們了解了sharding-jdbc
分頁查詢的內(nèi)部工作機(jī)制,這里我們順便說一下這種算法的缺點(diǎn),查閱官網(wǎng)說法是sharding-jdbc分頁查詢不會(huì)占用內(nèi)存,說明查詢結(jié)果僅僅記錄的是游標(biāo):
首先,采用流式處理 + 歸并排序的方式來避免內(nèi)存的過量占用。由于SQL改寫不可避免的占用了額外的帶寬,但并不會(huì)導(dǎo)致內(nèi)存暴漲。 與直覺不同,大多數(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),性能損耗很小。
但是筆者在使用過程中,打印內(nèi)存快照時(shí)發(fā)現(xiàn),進(jìn)行500w
數(shù)據(jù)的深分頁查詢發(fā)現(xiàn),它的做法和我們上文源碼所說的一致,就是將當(dāng)前頁以及之前的結(jié)果全部加載到內(nèi)存中,所以筆者認(rèn)為使用sharding-jdbc
時(shí)還是需要注意一下對(duì)內(nèi)存的監(jiān)控:
小結(jié)
以上就是sharding-jdbc實(shí)現(xiàn)分頁查詢的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于sharding-jdbc分頁查詢的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot項(xiàng)目中使用Sharding-JDBC實(shí)現(xiàn)讀寫分離的詳細(xì)步驟
- sharding-jdbc 兼容 MybatisPlus動(dòng)態(tài)數(shù)據(jù)源的配置方法
- SpringBoot集成Sharding-JDBC實(shí)現(xiàn)分庫分表方式
- SpringBoot+MybatisPlus實(shí)現(xiàn)sharding-jdbc分庫分表的示例代碼
- sharding-jdbc讀寫分離原理詳細(xì)解析
- Sharding-jdbc報(bào)錯(cuò):Missing the data source name:‘m0‘解決方案
相關(guān)文章
通過實(shí)例了解cookie機(jī)制特性及使用方法
這篇文章主要介紹了通過實(shí)例了解cookie機(jī)制特性及使用方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-09-09實(shí)例講解Java中random.nextInt()與Math.random()的基礎(chǔ)用法
今天小編就為大家分享一篇關(guān)于實(shí)例講解Java中random.nextInt()與Math.random()的基礎(chǔ)用法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2019-02-02

關(guān)于SpringBoot中事務(wù)失效的幾種情況

詳解Spring整合mybatis--Spring中的事務(wù)管理(xml形式)