如何實(shí)現(xiàn)分庫(kù)分表插件

前言

隨著系統(tǒng)數(shù)據(jù)量的日益增長(zhǎng),在說(shuō)起數(shù)據(jù)庫(kù)架構(gòu)和數(shù)據(jù)庫(kù)優(yōu)化的時(shí)候,我們難免會(huì)常常聽(tīng)到分庫(kù)分表這樣的名詞。

當(dāng)然,分庫(kù)分表有很多的方法論,比如垂直拆分、水平拆分;也有很多的中間件產(chǎn)品,比如MyCat、ShardingJDBC。

根據(jù)業(yè)務(wù)場(chǎng)景選擇合適的拆分方法,再選擇一個(gè)熟悉的開(kāi)源框架,就能幫助我們完成項(xiàng)目中所涉及到的數(shù)據(jù)拆分工作。

本文并不打算就這些方法論和開(kāi)源框架展開(kāi)深入的探討,筆者想討論另外一個(gè)場(chǎng)景:

如果系統(tǒng)中需要拆分的表并不多,只是1個(gè)或者少量的幾個(gè),我們是否值得引入一些相對(duì)復(fù)雜的中間件產(chǎn)品;特別是,如果我們對(duì)它們的原理不甚了解,是否有信心駕馭它們 ?

基于此,如果你的系統(tǒng)中有少量的表需要拆分,也沒(méi)有專門的資源去研究開(kāi)源組件,那么我們可以自己來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的分庫(kù)分表插件;當(dāng)然,如果你的系統(tǒng)比較復(fù)雜,業(yè)務(wù)量較大,還是采用開(kāi)源組件或者團(tuán)隊(duì)自研組件來(lái)解決這事較為穩(wěn)妥。

一、原理

分庫(kù)分表這事說(shuō)簡(jiǎn)單也簡(jiǎn)單,說(shuō)復(fù)雜那也挺復(fù)雜...

簡(jiǎn)單是因?yàn)樗暮诵牧鞒瘫容^明確。就是解析SQL語(yǔ)句,然后根據(jù)預(yù)先配置的規(guī)則,重寫(xiě)或路由到真實(shí)的數(shù)據(jù)庫(kù)表中去;

復(fù)雜在于,SQL語(yǔ)句復(fù)雜且靈活,比如分頁(yè)、去重、排序、分組、聚合、關(guān)聯(lián)查詢等操作,如何正確的解析它們。

所以就算是ShardingJDBC,在官網(wǎng)中也明確了支持項(xiàng)和不支持項(xiàng)。

二、注解式配置

相對(duì)于復(fù)雜的配置文件,我們采用較為輕便的注解式配置,它的定義如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Sharding {
    String tableName();     //邏輯表名
    String field();         //分片鍵
    String mode();          //算法模式
    int length() default 0; //分表數(shù)量
}

那么,在哪里使用它呢 ? 比如我們的用戶表需要分表,那就在User這個(gè)實(shí)體對(duì)象上標(biāo)注。

@Data
@Sharding(tableName = "user",field = "id",mode = "hash",length = 16)
public class User {
    private Long id;
    private String name;
    private String address;
    private String tel;
    private String email;
}

這就說(shuō)明了,我一共有 16 張用戶表,根據(jù)用戶ID,使用Hash算法來(lái)計(jì)算它的位置。

當(dāng)然,我們不止有Hash算法,還可以根據(jù)日期范圍來(lái)定義。

@Data
@Sharding(tableName = "car",field = "creatTime",mode = "range")
public class Car {
    private long id;
    private String number;
    private String brand;
    private String creatTime;
    private long userId;
}

三、分片算法

在這里,筆者實(shí)現(xiàn)了兩種分片方式,就是HashAlgorithm和RangeAlgorithm 。

1、范圍分片

如果你的系統(tǒng)中有使用冷熱數(shù)據(jù)分離,我們可以按照日期將不同月的數(shù)據(jù)分散到不同的表中。

比如車輛的創(chuàng)建時(shí)間是2019-12-10 15:30:00,這條數(shù)據(jù)將會(huì)被分配到car_201912這張表中去。

我們通過(guò)截取時(shí)間的年月部分,然后再加上邏輯表名即可。

public class RangeAlgorithm implements Algorithm {
    @Override
    public String doSharding(String tableName, Object value,int length) {
        if (value!=null){
            try{
                DateUtil.parseDateTime(value.toString());
                String replace = value.toString().substring(0, 7).replace("-", "");
                String newName = tableName+"_"+replace;
                return newName;
            }catch (DateException ex){
                logger.error("時(shí)間格式不符合要求!傳入?yún)?shù):{},正確格式:{}",value.toString(),"yyyy-MM-dd HH:mm:ss");
                return tableName;
            }
        }
        return tableName;
    }
}

2、Hash分片

在Hash分片算法中,我們可以先判斷表的數(shù)量,是不是2的冪次方。如果不是,就通過(guò)算數(shù)方式獲取下標(biāo),如果是呢,就通過(guò)位運(yùn)算的方式獲取下標(biāo)。當(dāng)然了,這是在HashMap源碼中學(xué)到的哦。

public class HashAlgorithm implements Algorithm {
    @Override
    public String doSharding(String tableName, Object value,int length) {
        if (this.isEmpty(value)){
            return tableName;
        }else{
            int h;
            int hash = (h = value.hashCode()) ^ (h >>> 16);
            int index;
            if (is2Power(length)){
                index = (length - 1) & hash;
            }else {
                index = Math.floorMod(hash, length);
            }
            return tableName+"_"+index;
        }
    }
}

四、攔截器

配置和分片算法都有了,接下來(lái)就是重頭戲了。在這里,我們使用Mybatis攔截器將它們派上用場(chǎng)。

常年CRUD的我們,都知道一條業(yè)務(wù)SQL肯定逃不出它們的范圍。其中,在業(yè)務(wù)上我們的刪除功能一般都是邏輯刪除,所以,基本上不會(huì)有DELETE操作。

相較而言,新增和修改SQL都比較簡(jiǎn)單且格式固定,查詢SQL往往比較靈活且復(fù)雜。所以,在這里筆者定義了兩個(gè)攔截器。

不過(guò),在介紹攔截器之前,我們有理由要了解另外兩個(gè)東西:SQL語(yǔ)法解析器和分片算法處理器。

1、JSqlParser

JSqlParser負(fù)責(zé)解析SQL語(yǔ)句,并轉(zhuǎn)化為Java類的層次結(jié)構(gòu)。我們可以先看個(gè)簡(jiǎn)單的例子來(lái)認(rèn)識(shí)它。

public static void main(String[] args) throws JSQLParserException {

    String insertSql = "insert into user (id,name,age) value(1001,'范閑',20)";
    Statement parse = CCJSqlParserUtil.parse(insertSql);
    Insert insert = (Insert) parse;

    String tableName = insert.getTable().getName();
    List<Column> columns = insert.getColumns();
    ItemsList itemsList = insert.getItemsList();
    System.out.println("表名:"+tableName+" 列名:"+columns+" 屬性:"+itemsList);
}
輸出: 表名:user 列名:[id, name, age] 屬性:(1001, '范閑', 20)

我們可以看到,JSqlParser可以解析出SQL的語(yǔ)法信息。相應(yīng)的,我們也可以更改對(duì)象內(nèi)容,從而達(dá)到修改SQL語(yǔ)句的目的。

2、算法處理器

我們的分片算法有多個(gè),具體應(yīng)該調(diào)用哪一個(gè)是在程序運(yùn)行期來(lái)決定的。所以,我們使用一個(gè)Map先將算法注冊(cè)起來(lái),然后根據(jù)分片模式來(lái)調(diào)用它。這也是策略模式的體現(xiàn)。

@Component
public class AlgorithmHandler {
    private Map<String, Algorithm> algorithm = new HashMap<>();
    @PostConstruct
    public void init(){
        algorithm.put("range",new RangeAlgorithm());
        algorithm.put("hash",new HashAlgorithm());
    }
    public String handler(String mode,String name,Object value,int length){
        return algorithm.get(mode).doSharding(name, value,length);
    }
}

3、攔截器

我們知道,MyBatis允許你在已映射語(yǔ)句執(zhí)行過(guò)程中的某一點(diǎn)進(jìn)行攔截調(diào)用。

如果你對(duì)它的原理還不熟悉,那么可以先看看筆者的文章:Mybatis攔截器的原理。

整體來(lái)看,它的流程如下:

  • 通過(guò)Mybatis攔截待執(zhí)行的SQL;
  • 通過(guò)JSqlParser解析SQL,獲取邏輯表名等;
  • 調(diào)用分片算法獲取真實(shí)表名;
  • 修改SQL,并修改BoundSql;
  • Mybatis執(zhí)行修改后的SQL,達(dá)成目的。

比如,對(duì)于insert語(yǔ)句和update語(yǔ)句,它的核心代碼如下:

insert

五、查詢及分頁(yè)

事實(shí)上,新增和修改都比較簡(jiǎn)單,較為復(fù)雜的是查詢語(yǔ)句。

但是,我們的插件并不在于要滿足所有的查詢語(yǔ)句,而是可以根據(jù)真實(shí)的業(yè)務(wù)場(chǎng)景來(lái)擴(kuò)展修改。

不過(guò)分頁(yè)功能基本上是逃不開(kāi)的。拿PageHelper為例,它的原理也是通過(guò)Mybatis攔截器來(lái)實(shí)現(xiàn)的。如果它和我們的分表插件在一起,可能會(huì)產(chǎn)生沖突。

所以在分表插件中,筆者也集成了分頁(yè)功能,基本上和PageHelper一樣,但并未直接使用它。另外,對(duì)于查詢來(lái)說(shuō),在查詢條件中是否帶有分片鍵,也是很關(guān)鍵的地方。

1、查詢

在范圍算法中,在業(yè)務(wù)上我們要求只查詢特定某一個(gè)月或者近幾個(gè)月的數(shù)據(jù)即可;在Hash算法中,我們則要求每次都帶有主鍵。

但第二個(gè)條件往往不能成立,業(yè)務(wù)方也滿足不了每次都必須帶有主鍵。

針對(duì)這種情況,我們只能遍歷所有的表,查詢符合條件的數(shù)據(jù),然后再匯總返回;


query

這種方式的缺點(diǎn)顯而易見(jiàn),性能較差。還有一種方式就是可以將常用的查詢條件與分片鍵建立映射關(guān)系,在查詢時(shí)先根據(jù)查詢條件找到分片鍵的字段值,然后再根據(jù)分片鍵查詢。

2、分頁(yè)

如上所言,插件中集成了分頁(yè)功能,實(shí)現(xiàn)流程與PageHelper一樣,但考慮到?jīng)_突,并未直接使用。

page

六、其他

事實(shí)上,筆者在想本文的標(biāo)題時(shí),著實(shí)比較苦惱。因?yàn)?code>分庫(kù)分表在業(yè)界是一個(gè)詞,但本文插件并不涉及分庫(kù),僅有的只是分表操作而已,不過(guò)本文的重點(diǎn)是思路,最終還是叫了分庫(kù)分表,還請(qǐng)盆友們見(jiàn)諒,不要叫我標(biāo)題黨~

由于篇幅所限,文中只有少量的代碼,如果感興趣的盆友可以去https://github.com/taoxun/sharding獲取完整Demo。

筆者的代碼中,包含了一些測(cè)試用例和建表SQL,創(chuàng)建完表后直接運(yùn)行項(xiàng)目即可。

最后編輯于
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. 簡(jiǎn)介 1.1 什么是 MyBatis ? MyBatis 是支持定制化 SQL、存儲(chǔ)過(guò)程以及高級(jí)映射的優(yōu)秀的...
    笨鳥(niǎo)慢飛閱讀 6,234評(píng)論 0 4
  • 水平分庫(kù)分表的關(guān)鍵步驟以及可能遇到的問(wèn)題 在之前的文章中,我介紹了分庫(kù)分表的幾種表現(xiàn)形式和玩法,也重點(diǎn)介紹了垂直分...
    meng_philip123閱讀 1,877評(píng)論 2 14
  • 在之前的文章中,我介紹了分庫(kù)分表的幾種表現(xiàn)形式和玩法,也重點(diǎn)介紹了垂直分庫(kù)所帶來(lái)的問(wèn)題和解決方法。本篇中,我們將聊...
    程序員BUG閱讀 738評(píng)論 0 0
  • 一、分庫(kù)分表原則 關(guān)系型數(shù)據(jù)庫(kù)本身比較容易成為系統(tǒng)性能瓶頸,單機(jī)存儲(chǔ)容量、連接數(shù)、處理能力等都很有限,數(shù)據(jù)庫(kù)本身的...
    whisperoy閱讀 988評(píng)論 0 0
  • 小時(shí)候是住在外婆家里的,直到小學(xué)四年級(jí)才回到爸媽身邊。記得剛回家的那會(huì)兒,老爸總是會(huì)很寵溺的看著我,經(jīng)常主動(dòng)給我買...
    辰琳兒閱讀 295評(píng)論 0 0

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