2018-01-08 分布式數(shù)據(jù)庫(kù)分頁(yè)方案

分布式數(shù)據(jù)庫(kù)的分頁(yè)方案

本篇文章介紹了在 DRDS 上做時(shí)間序數(shù)據(jù)分頁(yè)展示的一種方法,初看簡(jiǎn)單,實(shí)則細(xì)節(jié)較多,需要一定的關(guān)系型數(shù)據(jù)庫(kù)基礎(chǔ),也是我們用戶在實(shí)際使用產(chǎn)品過(guò)程中碰到的問(wèn)題之一,這里分享給大家。##

來(lái)實(shí)現(xiàn)一個(gè)分頁(yè)吧

日常業(yè)務(wù)需求中,按時(shí)間序?qū)?shù)據(jù)進(jìn)行分頁(yè)展示是一個(gè)非常普遍的需求,單機(jī)關(guān)系型數(shù)據(jù)庫(kù)有比較標(biāo)準(zhǔn)化的解決方案,但是這個(gè)需求在分布式數(shù)據(jù)庫(kù)中實(shí)現(xiàn),粗看比較簡(jiǎn)單,實(shí)際上卻相當(dāng)復(fù)雜,如果要做好,需要有比較扎實(shí)的關(guān)系型數(shù)據(jù)庫(kù)認(rèn)知。

這個(gè)問(wèn)題分為兩個(gè)部分:</br>

  1. 功能性要求:
    ? a.數(shù)據(jù)需按照時(shí)間順序進(jìn)行返回
    ? b.所有數(shù)據(jù)均需要被遍歷到,不能有遺漏
    ? c.每頁(yè)返回100條數(shù)據(jù)
    ? d.同一個(gè)時(shí)間精度內(nèi)(例如同一個(gè)毫秒內(nèi)),會(huì)有多條數(shù)據(jù)同時(shí)被插入
  2. 性能要求:
    ? a.性能不能隨頁(yè)碼的增加而衰減,需要高效并且恒定
    ? b.性能不能隨著數(shù)據(jù)量的增加而衰減,例如10W條數(shù)據(jù)的時(shí)候很高效,100億條數(shù)據(jù)的時(shí)候也很高效
單機(jī)數(shù)據(jù)庫(kù)的做法

? 對(duì)于單機(jī)數(shù)據(jù)庫(kù)而言,以MySQL 為例,功能部分,直接按照自增主鍵從小到大排列即可,因?yàn)樽栽鲋麈I的大小能夠區(qū)分出數(shù)據(jù)生成的前后關(guān)系,所以功能上不存在問(wèn)題。

? 性能部分優(yōu)化,在 SQL帶其他過(guò)濾條件的情況下,可以將扁平的帶 LIMIT m,n 語(yǔ)句優(yōu)化成嵌套子查詢以便讓優(yōu)化器做索引覆蓋,避免在磁盤上遍歷數(shù)據(jù),SQL 如下所示:

SELECT *
    FROM table a JOIN(
    SELECT pk FROMtable
    WHERE some_column= ?
    ORDER BY pk LIMIT m, n) b ON a.pk= b.pk

在分布式數(shù)據(jù)庫(kù)中實(shí)現(xiàn)的難點(diǎn)

??對(duì)于單機(jī)數(shù)據(jù)庫(kù)來(lái)說(shuō),因?yàn)榇嬖跁r(shí)間序的自增主鍵,這個(gè)需求變得比較簡(jiǎn)單,但是對(duì)于分布式數(shù)據(jù)庫(kù)而言,這個(gè)問(wèn)題就變得比較復(fù)雜,主要因?yàn)橹麈I大小和數(shù)據(jù)生成時(shí)間并沒(méi)有本質(zhì)關(guān)聯(lián),即使是分布式強(qiáng)一致數(shù)據(jù)庫(kù),保證嚴(yán)格時(shí)間序代價(jià)也很高,這個(gè)導(dǎo)致只能通過(guò)類似 GMT_CREATE 這種時(shí)間字段進(jìn)行排序分頁(yè),但是 GMT_CREATE 可能重復(fù),或者存在大量重復(fù),這個(gè)導(dǎo)致分頁(yè)按時(shí)間排序處理變得更加復(fù)雜。

??另外性能層面,我們?nèi)缤?code>MySQL 不能使用扁平的帶LIMIT m,n語(yǔ)句進(jìn)行分頁(yè)處理,但是同時(shí)也不能優(yōu)化成帶子查詢的分頁(yè)語(yǔ)句,因?yàn)閿?shù)據(jù)分片的原因,需要將 LIMIT m,n優(yōu)化成 LIMIT 0,m+n,分頁(yè)挪到非常大的時(shí)候,需要返回到 DRDS 大量數(shù)據(jù),再 skip掉不必要的數(shù)據(jù),即使做了倒序優(yōu)化,一張拆分表性能最差的一頁(yè)數(shù)據(jù)查詢需要返回表中一半的數(shù)據(jù)才能滿足需求,這個(gè)在拆分表有10億或者100億數(shù)據(jù)的時(shí)候,很難滿足性能需求。

DRDS的方案

??對(duì)這個(gè)問(wèn)題仔細(xì)分析后,我們提出了一個(gè)方案。為了避免返回大量的中間結(jié)果數(shù)據(jù),我們希望不指定 LIMIT 的 OFFSET,而是用上一頁(yè)的最大值直接跳轉(zhuǎn)到下一頁(yè)的起始處,因此業(yè)務(wù)做一定的妥協(xié),功能上增加如下約束:
??1. 提供下一頁(yè)、上一頁(yè)、首頁(yè)、尾頁(yè)功能
??2. 可以在當(dāng)前頁(yè)相鄰的幾頁(yè)進(jìn)行跳轉(zhuǎn)(例如前后10頁(yè))
??3. 不允許做任意頁(yè)的跳轉(zhuǎn)
對(duì)于單機(jī)數(shù)據(jù)庫(kù),我們可以使用如下的 SQL 實(shí)現(xiàn)上述的需求:
SELECT * FROM table WHERE pk> 上一頁(yè)pk最大值 ORDRE BY pk LIMIT n

分布式數(shù)據(jù)庫(kù):
  1. 增加 GMT_CREATE 字段,其默認(rèn)值為CURRENT_TIMESTAMP

  2. 我們?nèi)匀话凑?GMT_CREATE 時(shí)間字段進(jìn)行第一個(gè)維度排序,但是 GMT_CREATE 有重復(fù),我們希望有一個(gè)固定順序,所以再以主鍵為第二個(gè)維度排序,所以排序部分為 ORDER BY gmt_create,pk 。

  3. 我們記住每一頁(yè) GMT_CREATEPK 的最大值作為下一頁(yè)數(shù)據(jù)的起始值,但是前面提到過(guò) GMT_CREATE 有數(shù)據(jù)重復(fù),如果通過(guò)簡(jiǎn)單AND條件拼接,會(huì)導(dǎo)致漏數(shù)據(jù),所以我們將條件寫為:

    gmt_create>= ? AND(gmt_create> ?or pk> ?)

從查詢邏輯上規(guī)避掉GMT_CREATE 重復(fù)帶來(lái)的可能漏數(shù)據(jù)的狀況,分頁(yè) SQL 變成了這個(gè):

SELECT *
        FROM page_test
        WHERE gmt_create>= ?
        AND(gmt_create> ?
        OR pk> ?)
        ORDER BY gmt_create,
        pk LIMIT n

當(dāng)然在MySQL 5.7上,我們還可以直接做二元組的比較(5.7之前的版本,多元組的比較 MySQL 無(wú)法利用到組合索引):

SELECT *
        FROM page_test
        WHERE(gmt_create, pk)> (?, ?)ORDER BY gmt_create, pk LIMIT n

結(jié)果驗(yàn)證和總結(jié)
如下圖page_test3表,數(shù)據(jù)量為36億多條,表結(jié)構(gòu)如圖所示,總共拆了96張表,4個(gè)RDS存儲(chǔ)數(shù)據(jù)。

其中數(shù)據(jù)起始值如下:
如果我們挑選數(shù)據(jù)集中間的值,從下圖看性能也很好,所以只要按照這套方案做分布式數(shù)據(jù)庫(kù)分頁(yè)或者全量掃描數(shù)據(jù),性能將不會(huì)劣化,可以嚴(yán)格按照時(shí)間序排列,并且不會(huì)掃漏已經(jīng)存在于數(shù)據(jù)庫(kù)中的數(shù)據(jù)。

代碼

package com.taobao.tddl.sample;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;

import com.alibaba.druid.pool.DruidDataSource;

public class PageSample {

        public static void main(String[] args) throws Exception {
    
    DruidDataSource ds = new DruidDataSource();
    ds.setUrl(
    "jdbc:mysql://drdsxxxx.drds.aliyuncs.com:3306/dbname?characterEncoding=utf8&rewriteBatchedStatements=true&clobberStreamingResults=true&allowMultiQueries=true");
    ds.setUsername("user");
    ds.setPassword("password");
    ds.init();
    int index = 0;
    boolean first = true;
    Object maxGmtCreate = null;
    long maxId = -1;
    while (true) {
    Connection conn = null;
    try {
    conn = ds.getConnection();
    PreparedStatement ps = null;
    if (first) {
    ps = conn.prepareStatement("SELECT * FROM page_test  order by gmt_create,id limit 99");
    
    first = false;
    } else {
    ps = conn.prepareStatement(
    "SELECT * FROM page_test where gmt_create >= ? and (gmt_create > ? or id > ?) order by gmt_create,id limit 99");
    ps.setObject(1, maxGmtCreate);
    ps.setObject(2, maxGmtCreate);
    ps.setLong(3, maxId);
    
    }
    ResultSet rs = ps.executeQuery();
    maxGmtCreate = null;
    maxId = -1;
    while (rs.next()) {
    System.out.println((++index) + " " + rs.getInt("id") + " " + rs.getString("gmt_Create"));
    maxGmtCreate = rs.getObject("gmt_create");
    maxId = rs.getLong("id");
    }
    if (maxId == -1) {
    break;
    }
    } finally {
    conn.close();
    }
    
    }
    
    }
}
轉(zhuǎn)自 阿里巴巴數(shù)據(jù)庫(kù)技術(shù) 公眾號(hào)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容