初識(shí)分庫(kù)分表框架DBSPLIT

點(diǎn)擊Dbsplit進(jìn)入我們的項(xiàng)目主頁(yè),獲得更多信息和內(nèi)容。

分庫(kù)分表示意圖

什么是dbsplit?

Dbsplit擴(kuò)展了Spring的JdbcTemplate, 在JdbcTemplate上增加了分庫(kù)分表,讀寫(xiě)分離和失效轉(zhuǎn)移等功能,并與Spring JDBC保持相同的風(fēng)格,簡(jiǎn)單實(shí)用,避免外部依賴(lài),不需要類(lèi)似cobar的代理服務(wù)器,堪稱(chēng)可伸縮的Spring JdbcTemplate。

一方面,它對(duì)于單庫(kù)單表擴(kuò)展了JdbcTemplate模板, 使其成為一個(gè)簡(jiǎn)單的ORM框架,可以直接對(duì)領(lǐng)域?qū)ο竽P瓦M(jìn)行持久和搜索操作,并且實(shí)現(xiàn)了讀寫(xiě)分離。

另一方面,對(duì)于分庫(kù)分表它與JdbcTemplate保持同樣的風(fēng)格,不但提供了一個(gè)簡(jiǎn)單的ORM框架,可以直接對(duì)領(lǐng)域?qū)ο竽P瓦M(jìn)行持久和搜索操作,還是先了數(shù)據(jù)分片和讀寫(xiě)分離等高級(jí)功能。

另外,擴(kuò)展的Dbsplit保持與原有JdbcTemplate完全兼容,對(duì)于特殊需求,完全可以回溯到原有JdbcTemplate提供的功能,即使用JDBC的方式來(lái)解決,這里面體現(xiàn)了通用和專(zhuān)用原則,通用原則解決80%的事情,而專(zhuān)用原則解決剩余的20%的事情。

此項(xiàng)目也提供了一個(gè)方便的腳本,可以一次性的建立多庫(kù)多表。

誰(shuí)應(yīng)該關(guān)注dbsplit?

特別適合想知道互聯(lián)網(wǎng)的分庫(kù)分表是怎么實(shí)現(xiàn)的,也適合那些想把分庫(kù)分表框架開(kāi)箱即用的項(xiàng)目,更適合想學(xué)習(xí)互聯(lián)網(wǎng)的小伙伴們。

如果你在尋找數(shù)據(jù)庫(kù)分庫(kù)分表的輕量級(jí)解決方案,請(qǐng)參考Dbsplit的實(shí)現(xiàn)和應(yīng)用場(chǎng)景,它是一個(gè)兼容Spring JDBC的并且支持分庫(kù)分表的輕量級(jí)的數(shù)據(jù)庫(kù)中間件,使用起來(lái)簡(jiǎn)單方便,性能接近于直接使用JDBC,并且能夠無(wú)縫的與Spring相結(jié)合,又具有很好的可維護(hù)性。

怎么使用dbsplit?

我們已經(jīng)完整的實(shí)現(xiàn)了一個(gè)具有分庫(kù)分表功能的框架dbsplit,現(xiàn)在,讓我們提供一個(gè)示例演示在我們的應(yīng)用中怎么來(lái)使用這個(gè)框架,大家也可以參考dbsplit項(xiàng)目中dbsplit-core/src/main/test中的源代碼。

首先,假設(shè)我們應(yīng)用中有個(gè)表需要增刪改查,它的DDL腳本如下:

drop table if exists TEST_TABLE_$I;

create table TEST_TABLE_$I
(
    ID bigint not null,
    NAME varchar(128) not null,
    GENDER               smallint default 0, 
    LST_UPD_USER         varchar(128) default "SYSTEM",
    LST_UPD_TIME         timestamp default now(),
    primary key(id),
    unique key UK_NAME(NAME)
);

我們把這個(gè)DDL腳本保存到table.sql文件中,然后,我們需要準(zhǔn)備好一個(gè)Mysql的數(shù)據(jù)庫(kù)實(shí)例,實(shí)例端口為localhost:3307, 因?yàn)榄h(huán)境的限制,我們用著一個(gè)數(shù)據(jù)庫(kù)實(shí)例來(lái)模擬兩個(gè)數(shù)據(jù)庫(kù)實(shí)例,兩個(gè)數(shù)據(jù)庫(kù)實(shí)例使用同一個(gè)端口,我們?yōu)門(mén)EST_TABLE設(shè)計(jì)了2個(gè)數(shù)據(jù)庫(kù)實(shí)例、每個(gè)實(shí)例2個(gè)數(shù)據(jù)庫(kù)、每個(gè)數(shù)據(jù)庫(kù)4個(gè)表,共16個(gè)分片表。

我們使用腳本創(chuàng)建創(chuàng)建用于分片的多個(gè)數(shù)據(jù)庫(kù)和表,腳本代碼如下所示:

build-db-split.sh -i "localhost:3307,localhost:3307" -m test_db -n table.sql -x 2 -y 4 -a test_user -b test_password -c root -d youarebest -l localhost -t

這里,需要提供系統(tǒng)root用戶(hù)的用戶(hù)名和密碼。

然后,我們登錄Mysql的命令行客戶(hù)端,我們看到一共創(chuàng)建了4個(gè)數(shù)據(jù)庫(kù),前2個(gè)數(shù)據(jù)庫(kù)屬于數(shù)據(jù)庫(kù)實(shí)例1,后2個(gè)數(shù)據(jù)庫(kù)屬于數(shù)據(jù)庫(kù)實(shí)例2,每個(gè)數(shù)據(jù)庫(kù)有4個(gè)表。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| test               |
| test_db_0          |
| test_db_1          |
| test_db_2          |
| test_db_3          |
+--------------------+
6 rows in set (0.01 sec)

mysql> use test_db_0;
Database changed
mysql> show tables;
+---------------------+
| Tables_in_test_db_0 |
+---------------------+
| TEST_TABLE_0        |
| TEST_TABLE_1        |
| TEST_TABLE_2        |
| TEST_TABLE_3        |
+---------------------+
4 rows in set (0.00 sec)

因此,一共我們創(chuàng)建了16個(gè)分片表。

然后,我們定義對(duì)應(yīng)這個(gè)數(shù)據(jù)庫(kù)表的領(lǐng)域?qū)ο竽P停谶@個(gè)領(lǐng)域?qū)ο竽P椭?,我們不需要任何注解,這是一個(gè)綠色的POJO。

public class TestTable {
    private long id;
    private String name;

    public enum Gender {
        MALE, FEMALE;

        public static Gender parse(int value) {
            for (Gender gender : Gender.values()) {
                if (value == gender.ordinal())
                    return gender;
            }
            return null;
        }
    };

    private Gender gender;
    private String lstUpdUser;
    private Date lstUpdTime;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Gender getGender() {
        return gender;
    }

    public void setGender(Gender gender) {
        this.gender = gender;
    }

    public String getLstUpdUser() {
        return lstUpdUser;
    }

    public void setLstUpdUser(String lstUpdUser) {
        this.lstUpdUser = lstUpdUser;
    }

    public Date getLstUpdTime() {
        return lstUpdTime;
    }

    public void setLstUpdTime(Date lstUpdTime) {
        this.lstUpdTime = lstUpdTime;
    }

    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }
}

因?yàn)槲覀兊膽?yīng)用程序需要保存這個(gè)實(shí)體,這就需要生成唯一的ID,發(fā)號(hào)器的設(shè)計(jì)和使用請(qǐng)參考第4章如何設(shè)計(jì)一款永不重復(fù)的高性能分布式發(fā)號(hào)器,這里我們需要配置一個(gè)發(fā)號(hào)器服務(wù)即可,代碼如下所示。

    <bean id="idService" class="com.robert.vesta.service.factory.IdServiceFactoryBean"
        init-method="init">
        <property name="providerType" value="PROPERTY" />
        
        <property name="machineId" value="${vesta.machine}" />
    </bean>

接下來(lái),我們?cè)赟pring環(huán)境中定義這個(gè)表的分片信息,這包括數(shù)據(jù)庫(kù)名稱(chēng)、表名稱(chēng)、數(shù)據(jù)庫(kù)分片數(shù)、表的分片數(shù),以及讀寫(xiě)分離等信息,本例中我們制定數(shù)據(jù)庫(kù)前綴為test_db,數(shù)據(jù)庫(kù)表名為T(mén)EST_TABLE,每個(gè)實(shí)例2個(gè)數(shù)據(jù)庫(kù),每個(gè)數(shù)據(jù)庫(kù)4張表,分片采用采用水平下標(biāo)策略,并且打開(kāi)讀寫(xiě)分離。

    <bean name="splitTable" class="com.robert.dbsplit.core.SplitTable"
        init-method="init">

        <property name="dbNamePrefix" value="test_db" />
        <property name="tableNamePrefix" value="TEST_TABLE" />

        <property name="dbNum" value="2" />
        <property name="tableNum" value="4" />

        <property name="splitStrategyType" value="HORIZONTAL" />
        <property name="splitNodes">
            <list>
                <ref bean="splitNode1" />
                <ref bean="splitNode2" />
            </list>
        </property>

        <property name="readWriteSeparate" value="true" />

    </bean>

我們看到,這個(gè)splitTable引用了兩個(gè)數(shù)據(jù)庫(kù)實(shí)例節(jié)點(diǎn):splitNode1和splitNode2,他們的聲明如下:

    <bean name="splitNode1" class="com.robert.dbsplit.core.SplitNode">
        <property name="masterTemplate" ref="masterTemplate0" />
        <property name="slaveTemplates">
            <list>
                <ref bean="slaveTemplate00"></ref>
            </list>
        </property>
    </bean>

    <bean name="splitNode2" class="com.robert.dbsplit.core.SplitNode">
        <property name="masterTemplate" ref="masterTemplate1" />
        <property name="slaveTemplates">
            <list>
                <ref bean="slaveTemplate10"></ref>
            </list>
        </property>
    </bean>

每個(gè)數(shù)據(jù)庫(kù)實(shí)例節(jié)點(diǎn)都引用了一個(gè)數(shù)據(jù)庫(kù)主模板以及若干個(gè)數(shù)據(jù)庫(kù)從模板,這是用來(lái)實(shí)現(xiàn)讀寫(xiě)分離的,因?yàn)槲覀兇蜷_(kāi)了讀寫(xiě)分離設(shè)置,所有的讀操作將由dbsplit路由到數(shù)據(jù)庫(kù)的從模板上,數(shù)據(jù)庫(kù)的主從模板的聲明引用到我們生命的數(shù)據(jù)庫(kù),因?yàn)槲覀兪窃诒镜刈鰷y(cè)試,這些數(shù)據(jù)源都指向了本地的Mysql數(shù)據(jù)庫(kù)localhost:3307。

    <bean id="masterTemplate0" class="org.springframework.jdbc.core.JdbcTemplate"
        abstract="false" lazy-init="false" autowire="default"
        dependency-check="default">
        <property name="dataSource">
            <ref bean="masterDatasource0" />
        </property>
    </bean>

    <bean id="slaveTemplate00" class="org.springframework.jdbc.core.JdbcTemplate"
        abstract="false" lazy-init="false" autowire="default"
        dependency-check="default">
        <property name="dataSource">
            <ref bean="slaveDatasource00" />
        </property>
    </bean>

到現(xiàn)在為止,我們定義好了表的分片信息,把我們把這個(gè)表加入到SplitTablesHolder的Bean中,代碼如下所示:

    <bean name="splitTablesHolder" class="com.robert.dbsplit.core.SplitTablesHolder"
        init-method="init">
        <property name="splitTables">
            <list>
                <ref bean="splitTable" />
            </list>
        </property>
    </bean>

接下來(lái),我們就需要聲明我們的SimpleSplitJdbcTemplate的Bean,它需要引用SplitTablesHolder的Bean,以及配置讀寫(xiě)分離的策略,配置代碼如下所示,

    <bean name="simpleSplitJdbcTemplate" class="com.robert.dbsplit.core.SimpleSplitJdbcTemplate">
        <property name="splitTablesHolder" ref="splitTablesHolder" />
        <property name="readWriteSeparate" value="${dbsplit.readWriteSeparate}" />
    </bean>

我們有了SimpleSplitJdbcTemplate的Bean,我們就可以把它導(dǎo)出給我們的服務(wù)層來(lái)使用了。這里我們通過(guò)一個(gè)測(cè)試用例來(lái)演示,在測(cè)試用例中初始化剛才我們配置的Spring環(huán)境,從Spring環(huán)境中獲取SimpleSplitJdbcTemplate的Bean simpleSplitJdbcTemplate,然后,示例里面的方法插入TEST_TABLE的記錄,然后,再把這條記錄查詢(xún)出來(lái),代碼如下所示。

    public void testSimpleSplitJdbcTemplate() {
        SimpleSplitJdbcTemplate simpleSplitJdbcTemplate = (SimpleSplitJdbcTemplate) applicationContext
                .getBean("simpleSplitJdbcTemplate");
        IdService idService = (IdService) applicationContext
                .getBean("idService");

        // Make sure the id generated is not align multiple of 1000
        Random random = new Random(new Date().getTime());
        for (int i = 0; i < random.nextInt(16); i++)
            idService.genId();

        long id = idService.genId();
        System.out.println("id:" + id);

        TestTable testTable = new TestTable();
        testTable.setId(id);
        testTable.setName("Alice-" + id);
        testTable.setGender(Gender.MALE);
        testTable.setLstUpdTime(new Date());
        testTable.setLstUpdUser("SYSTEM");

        simpleSplitJdbcTemplate.insert(id, testTable);

        TestTable q = new TestTable();

        TestTable testTable1 = simpleSplitJdbcTemplate.get(id, id,
                TestTable.class);

        AssertJUnit.assertEquals(testTable.getId(), testTable1.getId());
        AssertJUnit.assertEquals(testTable.getName(), testTable1.getName());
        AssertJUnit.assertEquals(testTable.getGender(), testTable1.getGender());
        AssertJUnit.assertEquals(testTable.getLstUpdUser(),
                testTable1.getLstUpdUser());
        // mysql store second as least time unit but java stores miliseconds, so
        // round up the millisends from java time
        AssertJUnit.assertEquals(
                (testTable.getLstUpdTime().getTime() + 500) / 1000 * 1000,
                testTable1.getLstUpdTime().getTime());

        System.out.println("testTable1:" + testTable1);
    }

如何使用用于創(chuàng)建分庫(kù)分表的腳本?

這里介紹一個(gè)用于創(chuàng)建分庫(kù)分表的腳本,這個(gè)腳本可以一次性的按照規(guī)則在多個(gè)mysql示例上創(chuàng)建多個(gè)數(shù)據(jù)庫(kù)和表,以及在每一個(gè)數(shù)據(jù)庫(kù)實(shí)例上創(chuàng)建一個(gè)統(tǒng)一的用戶(hù),并分配相應(yīng)的權(quán)限給此用戶(hù)。

1. 使用方法

Usage: $0 -i [INSTANCE_STR] -m [DB_PREFIX] -n [TABLE_SQL_FILE] -x [DB_SPLIT_NUM] -y [TABLE_SPLIT_NUM] -a [USER] -b [PASSWORD] -c [ROOT_USER] -d [ROOT_PASSWORD] -l [CONNECTION_HOST] -t 

Descriptions:
-i : instances string.
-m : db name.
-n : table file name.
-x : db number.
-y : table number.
-a : user name to be created.
-b : password for the user name to be created.
-c : root user.
-d : password for root user.
-l : for the connection host.
-t : debug sql output.

2. 使用示例

Example1: $0 -i "localhost:3306,localhost:3306" -m test_db -n table.sql -x 2 -y 2 -a test_user -b test_password -c root -d youarebest -l localhost -t
Example2: $0 -i "localhost:3306,localhost:3306" -m test_db -n table.sql -x 2 -y 2 -a test_user -b test_password -c root -d youarebest -l localhost

3. 源碼

#!/bin/bash

insts=localhost:3306,localhost:3306

db_prefix=test_db
table_sql_file=table.sql

db_num=2
table_num=2

user_name=test_user
password=test_password

root_user_name=root
root_password=cool

debug=FALSE

conn_host=localhost

build_db() {
  inst=$1
  inst_arr=(${inst//:/ })

  host=${inst_arr[0]}
  port=${inst_arr[1]}

  db=$2
  db_no=$3
  
  echo "info: building instance $inst db $db db no $db_no"

  for ((k=0;k<$table_num;k++)); do
    ((table_no=$table_num*$db_no+$k))

    echo "info: building instance $inst db $db db no $db_no table $table_no"    
    
    sql_command="sed 's/"'$index'"/$table_no/g' ./$table_sql_file | tr -t '\n' '\0'"
    sql_create_table=`eval "$sql_command"`
    
    if [[ $debug = 'TRUE' ]]; then
        echo "Create Table SQL: $sql_create_table"
    fi
    mysql -u$root_user_name -p$root_password -e "$sql_create_table" $db 2> /dev/null
     
  done  
}

build_inst() {
  inst=$1
  inst_arr=(${inst//:/ })

  host=${inst_arr[0]}
  port=${inst_arr[1]}

  inst_no=$2
  
  echo "info: building instance $inst no $inst_no"
  
  sql_delete_user="delete from mysql.user where user = '$user_name'; flush privileges"
  
  if [[ $debug = 'TRUE' ]]; then
    echo "Delete User SQL: $sql_delete_user"
  fi
  mysql -u$root_user_name -p$root_password -e "$sql_delete_user" 2> /dev/null
  
  mysql -u$root_user_name -p$root_password -e "create user '$user_name'@'$conn_host' identified by '$password'"
    
  for ((j=0;j<$db_num;j++)); do
    ((db_no=$db_num*$inst_no+$j)) 
    
    create_database_sql="drop database if exists ${db_prefix}_${db_no};create database ${db_prefix}_${db_no}"
    
    if [[ $debug = 'TRUE' ]]; then
      echo "Create Database SQL: $create_database_sql"
    fi    
    mysql -u$root_user_name -p$root_password -e "$create_database_sql" 2> /dev/null

    assign_rights_sql="grant all privileges on ${db_prefix}_${db_no}.* to '$user_name'@'$conn_host' identified by '$password';flush privileges"
    
    if [[ $debug = 'TRUE' ]]; then
      echo "Assign Rights SQL: $assign_rights_sql"
    fi    
    mysql -u$root_user_name -p$root_password -e "assign_rights_sql" 2> /dev/null    

    build_db $inst ${db_prefix}_${db_no} $db_no
  done   
}

main() {
    echo "properties: insts=$insts db_prefix=$db_prefix table_sql_file=$table_sql_file db_num=$db_num table_num=$table_num user_name=$user_name password=$password root_user_name=$root_user_name root_password=$root_password"

    insts_arr=(${insts//,/ })  
    insts_num=${#insts_arr[@]} 
    
    for ((i=0;i<$insts_num;i++)); do
      build_inst ${insts_arr[$i]} $i
    done
}

PrintUsage()
{
cat << EndOfUsageMessage

    Usage: $0 -i [INSTANCE_STR] -m [DB_PREFIX] -n [TABLE_SQL_FILE] -x [DB_SPLIT_NUM] -y [TABLE_SPLIT_NUM] -a [USER] -b [PASSWORD] -c [ROOT_USER] -d [ROOT_PASSWORD] -l [CONNECTION_HOST] -t 
    
    Descriptions:
    -i : instances string.
    -m : db name.
    -n : table file name.
    -x : db number.
    -y : table number.
    -a : user name to be created.
    -b : password for the user name to be created.
    -c : root user.
    -d : password for root user.
    -l : for the connection host.
    -t : debug sql output.
    
    Example1: $0 -i "localhost:3306,localhost:3306" -m test_db -n table.sql -x 2 -y 2 -a test_user -b test_password -c root -d youarebest -l localhost -t
    Example2: $0 -i "localhost:3306,localhost:3306" -m test_db -n table.sql -x 2 -y 2 -a test_user -b test_password -c root -d youarebest -l localhost
    
EndOfUsageMessage
}

InvalidCommandSyntaxExit()
{
        echo "Invalid command\n`PrintUsage`"
        exit;
}

if [ $# -eq 0 ]
then
    echo "`PrintUsage`"
    exit 1
fi


while getopts "i:m:n:x:y:a:b:c:d:l:t" arg
do
        case $arg in
             i)
                insts=$OPTARG 
                ;;
             m)
                db_prefix=$OPTARG
                ;;
             n)
                table_sql_file=$OPTARG
                ;;
             x)
                db_num=$OPTARG
                ;;
             y)
                table_num=$OPTARG
                ;;
             a)
                user_name=$OPTARG
                ;;
             b)
                password=$OPTARG
                ;;
             c)
                root_user_name=$OPTARG
                ;;
             d)
                root_password=$OPTARG
                ;;
             l)
                conn_host=$OPTARG
                ;;
             t)
                debug=TRUE
                ;;
             ?) 
                echo "`InvalidCommandSyntaxExit`"
                exit 1
                ;;
        esac
done

這個(gè)腳本僅僅是一個(gè)示例,計(jì)劃中,這個(gè)腳本需要支持三種分庫(kù)分表的策略,數(shù)據(jù)庫(kù)和表下標(biāo)累積的策略,數(shù)據(jù)庫(kù)和表下標(biāo)歸零的策略與兩種混合策略, 當(dāng)前腳本只支持第一種。

我們需要注意,這個(gè)建庫(kù)腳本不支持建立主從關(guān)系,但是可以建立主庫(kù)和從庫(kù)后再手工建立主從關(guān)系。

請(qǐng)掃描下面二維碼加入我們。


云時(shí)代架構(gòu)公眾號(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,695評(píng)論 19 139
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 47,288評(píng)論 6 342
  • 上帝之火閱讀 161評(píng)論 0 4
  • 高數(shù)及線代 個(gè)人技能 社會(huì)研究方法
    Whale__fall閱讀 140評(píng)論 0 0
  • 支原體是一種微生物原核細(xì)胞,細(xì)菌和病毒之間的體積,對(duì)人類(lèi)致病性支原體有3種,其中支原體是人類(lèi)泌尿生殖道常見(jiàn)的病原體...
    匡湊盯匡閱讀 337評(píng)論 0 0

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