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的實現和應用場景,它是一個兼容Spring JDBC的並且支持分庫分表的輕量級的資料庫中間件,使用起來簡單方便,性能接近於直接使用JDBC,並且能夠無縫的與Spring相結合,又具有很好的可維護性。
我們已經完整的實現了一個具有分庫分表功能的框架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示例上創建多個資料庫和表,以及在每一個資料庫實例上創建一個統一的用戶,並分配相應的權限給此用戶。
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
#!/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
這個腳本僅僅是一個示例,計劃中,這個腳本需要支持三種分庫分表的策略,資料庫和表下標累積的策略,資料庫和表下標歸零的策略與兩種混合策略, 當前腳本只支持第一種。
我們需要注意,這個建庫腳本不支持建立主從關係,但是可以建立主庫和從庫後再手工建立主從關係。