初識資料庫分庫分表框架Dbsplit

2021-01-13 GitChat精品課

Dbsplit擴展了Spring的JdbcTemplate, 在JdbcTemplate上增加了分庫分表,讀寫分離和失效轉移等功能,並與Spring JDBC保持相同的風格,簡單實用,避免外部依賴,不需要類似cobar的代理伺服器,堪稱可伸縮的Spring JdbcTemplate。

什麼是dbsplit?

Dbsplit擴展了Spring的JdbcTemplate, 在JdbcTemplate上增加了分庫分表,讀寫分離和失效轉移等功能,並與Spring JDBC保持相同的風格,簡單實用,避免外部依賴,不需要類似cobar的代理伺服器,堪稱可伸縮的Spring JdbcTemplate。

一方面,它對於單庫單表擴展了JdbcTemplate模板, 使其成為一個簡單的ORM框架,可以直接對領域對象模型進行持久和搜索操作,並且實現了讀寫分離。

另一方面,對於分庫分表它與JdbcTemplate保持同樣的風格,不但提供了一個簡單的ORM框架,可以直接對領域對象模型進行持久和搜索操作,還是先了數據分片和讀寫分離等高級功能。

另外,擴展的Dbsplit保持與原有JdbcTemplate完全兼容,對於特殊需求,完全可以回溯到原有JdbcTemplate提供的功能,即使用JDBC的方式來解決,這裡面體現了通用和專用原則,通用原則解決80%的事情,而專用原則解決剩餘的20%的事情。

此項目也提供了一個方便的腳本,可以一次性的建立多庫多表。

誰應該關注dbsplit?

特別適合想知道網際網路的分庫分表是怎麼實現的,也適合那些想把分庫分表框架開箱即用的項目,更適合想學習網際網路的小夥伴們。

如果你在尋找資料庫分庫分表的輕量級解決方案,請參考Dbsplit的實現和應用場景,它是一個兼容Spring JDBC的並且支持分庫分表的輕量級的資料庫中間件,使用起來簡單方便,性能接近於直接使用JDBC,並且能夠無縫的與Spring相結合,又具有很好的可維護性。

怎麼使用dbsplit?

我們已經完整的實現了一個具有分庫分表功能的框架dbsplit,現在,讓我們提供一個示例演示在我們的應用中怎麼來使用這個框架,大家也可以參考dbsplit項目中dbsplit-core/src/main/test中的原始碼。

首先,假設我們應用中有個表需要增刪改查,它的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)
);


我們把這個DDL腳本保存到table.sql文件中,然後,我們需要準備好一個Mysql的資料庫實例,實例埠為localhost:3307, 因為環境的限制,我們用著一個資料庫實例來模擬兩個資料庫實例,兩個資料庫實例使用同一個埠,我們為TEST_TABLE設計了2個資料庫實例、每個實例2個資料庫、每個資料庫4個表,共16個分片表。

我們使用腳本創建創建用於分片的多個資料庫和表,腳本代碼如下所示:

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


這裡,需要提供系統root用戶的用戶名和密碼。

然後,我們登錄Mysql的命令行客戶端,我們看到一共創建了4個資料庫,前2個資料庫屬於資料庫實例1,後2個資料庫屬於資料庫實例2,每個資料庫有4個表。

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)


因此,一共我們創建了16個分片表。

然後,我們定義對應這個資料庫表的領域對象模型,在這個領域對象模型中,我們不需要任何註解,這是一個綠色的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);
 }
}

因為我們的應用程式需要保存這個實體,這就需要生成唯一的ID,發號器的設計和使用請參考第4章如何設計一款永不重複的高性能分布式發號器,這裡我們需要配置一個發號器服務即可,代碼如下所示。

<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>


接下來,我們在Spring環境中定義這個表的分片信息,這包括資料庫名稱、表名稱、資料庫分片數、表的分片數,以及讀寫分離等信息,本例中我們制定資料庫前綴為test_db,資料庫表名為TEST_TABLE,每個實例2個資料庫,每個資料庫4張表,分片採用採用水平下標策略,並且打開讀寫分離。

<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>

我們看到,這個splitTable引用了兩個資料庫實例節點: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>

每個資料庫實例節點都引用了一個資料庫主模板以及若干個資料庫從模板,這是用來實現讀寫分離的,因為我們打開了讀寫分離設置,所有的讀操作將由dbsplit路由到資料庫的從模板上,資料庫的主從模板的聲明引用到我們生命的資料庫,因為我們是在本地做測試,這些數據源都指向了本地的Mysql資料庫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>

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

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


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

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


我們有了SimpleSplitJdbcTemplate的Bean,我們就可以把它導出給我們的服務層來使用了。這裡我們通過一個測試用例來演示,在測試用例中初始化剛才我們配置的Spring環境,從Spring環境中獲取SimpleSplitJdbcTemplate的Bean simpleSplitJdbcTemplate,然後,示例裡面的方法插入TEST_TABLE的記錄,然後,再把這條記錄查詢出來,代碼如下所示。

public void testSimpleSplitJdbcTemplate() {    SimpleSplitJdbcTemplate simpleSplitJdbcTemplate = (SimpleSplitJdbcTemplate) applicationContext
       .getBean("simpleSplitJdbcTemplate");    IdService idService = (IdService) applicationContext
       .getBean("idService");    
   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());    
   
   AssertJUnit.assertEquals(
       (testTable.getLstUpdTime().getTime() + 500) / 1000 * 1000,
       testTable1.getLstUpdTime().getTime());    System.out.println("testTable1:" + testTable1);
 }

如何使用用於創建分庫分表的腳本?

這裡介紹一個用於創建分庫分表的腳本,這個腳本可以一次性的按照規則在多個mysql示例上創建多個資料庫和表,以及在每一個資料庫實例上創建一個統一的用戶,並分配相應的權限給此用戶。

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


這個腳本僅僅是一個示例,計劃中,這個腳本需要支持三種分庫分表的策略,資料庫和表下標累積的策略,資料庫和表下標歸零的策略與兩種混合策略, 當前腳本只支持第一種。

我們需要注意,這個建庫腳本不支持建立主從關係,但是可以建立主庫和從庫後再手工建立主從關係。

相關焦點

  • 分庫分表怎麼做?用的哪個組件?
    它使用客戶端直連資料庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解為增強版的 JDBC 驅動,完全兼容 JDBC 和各種 ORM 框架。適用於任何基於 JDBC 的 ORM 框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template 或直接使用 JDBC。
  • 數據量大了一定要分表,分庫分表Sharding-JDBC入門與項目實戰
    最近項目中不少表的數據量越來越大,並且導致了一些資料庫的性能問題。因此想藉助一些分庫分表的中間件,實現自動化分庫分表實現。調研下來,發現Sharding-JDBC目前成熟度最高並且應用最廣的Java分庫分表的客戶端組件。
  • 步步學LINQ to SQL:將類映射到資料庫表
    第二篇:步步學LINQ to SQL:使用LINQ檢索數據 第三篇:步步學LINQ to SQL:為實體類添加關係  下面闡述本文的目標以及該示例程序為初級開發人員介紹如何學習LINQ的基本要點:  ·使用LINQ to SQL將SQL Server資料庫表映射到與之關聯的對象上。
  • 95份中國期刊在SCI學科分庫排名情況出爐
    > 95份中國期刊在SCI學科分庫排名情況出爐
  • 極致經典,堪稱資料庫的...
    01-05 05:55:17 來源: 諮詢娛樂團隊 舉報   MySQL資料庫
  • 初識分布式圖資料庫 Nebula Graph 2.0 Query Engine
    二、框架Query 層主要框架如下所示:主要分為 4 個子模塊Parser:詞法語法解析模塊Validator:語句校驗模塊Planner:執行計劃和優化器模塊Executor:執行算子模塊三、代碼結構下面講下 nebula-graph 的代碼層次結構,如下所示|--src |--context //
  • 資料庫系統安全框架可劃分為三個層次
    資料庫系統的安全除依賴自身內部的安全機制外,還與外部網絡環境、應用環境、從業人員素質等因素息息相關,因此,從廣義上講,資料庫系統的安全框架可以劃分為三個層次:    (1)網絡系統層次;    (2)宿主作業系統層次;    (3)資料庫管理系統層次。
  • 引領數據創新,星環科技分布式資料庫KunDB亮相數據技術嘉年華
    趙志強主要從介紹星環KunDB產品、KunDB如何使用分布式技術解決分庫分表方案的核心痛點、KunDB上私有雲的技術總結、KunDB主要開發路線四個方面做有關主題演講,與參會人員分享星環科技近些年在分布式資料庫上所做的努力和貢獻,並希望未來將分享更多相關前沿技術。
  • MySQL資料庫教程-數據表欄位約束
    MySQL資料庫教程-數據表欄位約束為保證資料庫中存儲數據的規範化,一般需要在定義欄位時進行欄位規範與約束的定義。保證在進行數據錄入時,資料庫能夠通過這個規則、約束、規範檢查所錄入的數據,防止錯誤及不符合要求數據的錄入。本文主要介紹數據表欄位約束類型及其基本語法,為下一步創建數據表提供基礎與依據。
  • 2018年自考資料庫及其應用複習重點:第二章
    數據模型表示的是資料庫的框架,在該框架約束下填上具體數據才是資料庫。資料庫類型根據數據模型劃分。數據的數據結構形式就是數據模型。為保證數據的完整性,數據模型對數據的描述應包括)模型中包含哪些記錄類型,並對記錄類型進行命名;指明各個記錄類型由哪些數據項構成,並對數據項進行命名;每個數據項均需指明其數據類型和取值範圍。
  • 網友整理的資料庫及應用筆記(一)
    DD:數據字典,其中存放著資料庫三級結構的描述以及各數據項的類型、值域和關鍵字等,從結構上對數據的語言和數值範圍加以約束。    計算機系統中任何軟體必須在作業系統的支持下才能工作。    1975年SPARC公布了資料庫標準報告,提出了資料庫三級組織結構,稱SPARC分級結構,從內到外分三個層次描述,分別稱為內模式、概念模式、外模式。
  • 雲計算一周熱文回顧:五大主流資料庫模型
    五大主流資料庫模型無論是關係型資料庫還是非關係型資料庫,都是某種數據模型的實現。本文將為大家簡要介紹5種常見的數據模型,讓我們來追本溯源,窺探現在流行的資料庫解決方案背後的神秘世界。1.關係模型關係模型使用記錄(由元組組成)進行存儲,記錄存儲在表中,表由架構界定。表中的每個列都有名稱和類型,表中的所有記錄都要符合表的定義。SQL是專門的查詢語言,提供相應的語法查找符合條件的記錄,如表聯接(Join)。表聯接可以基於表之間的關係在多表之間查詢記錄。2.
  • 直擊資料庫面試題:資料庫查詢語句
    以如下形式顯示:課程ID,最高分,最低分 SELECT L.C# 課程ID,L.score 最高分,R.score 最低分 FROM SC L ,SC R WHERE L.C# = R.C# and L.score
  • Oracle資料庫21c進一步鞏固甲骨文在資料庫的業界領導地位
    甲骨文公司宣布在Oracle雲中推出最新版本的全球領先融合資料庫Oracle資料庫21c。Oracle資料庫21c不僅包含200多項全新創新,包括不可變區塊鍊表、資料庫內JavaScript、原生JSON二進位數據類型、資料庫內機器學習的AutoML、持久性內存存儲,同時增強了內存、圖形處理性能、資料庫分表、多租戶和安全性功能等。
  • 2020 年資料庫高頻面試題|原力計劃
    也有可能是每個 sql 消耗資源並不多,但是突然之間,有大量的 session 連進來導致 cpu 飆升,這種情況就需要跟應用一起來分析為何連接數會激增,再做出相應的調整,比如說限制連接數等4、大表怎麼優化?某個表有近千萬數據,CRUD比較慢,如何優化?分庫分表了是怎麼做的?分表分庫了有什麼問題?有用到中間件麼?他們的原理知道麼?
  • 雲棲大會 | 螞蟻自研資料庫OceanBase發布2.2版本核心特性和全新...
    十年磨一劍  完全自主研發的企業級分布式資料庫區別於開源資料庫的再發行產品,OceanBase由螞蟻集團完全自主研發,基於分布式架構和通用伺服器、實現了金融級可靠性及數據一致性,擁有100%的智慧財產權。