追求極致的數據庫分區分表方案

序言

一直在作企業應用,目前要作一些互聯網應用,固然只是應用是放在互聯網的,數據量距離真正的互聯網應用仍是有至關大的差距的。可是不可避免的,在數據庫出現瓶頸的狀況仍是有的,如今作互聯網上的應用,固然也要未雨綢繆,要考慮數據量大的時候的解決方案。 java

這個目前開源的商用的也都有很多解決方案,一來,作技術的都有這麼個臭毛病,即便是使用別人的方案,本身也要搞清楚內部的一些實現機制,這樣纔會有真正的體會,不然去評估一個方案的時候,就只能盲人摸象了。 sql

爲此,構建一個驗證型的分佈式數據庫框架,來解決數據庫的垂直與水平擴展方面的問題,因爲是驗證性開發,因此,思考不完善的地方確定存在,歡迎批評指正。 數據庫

提高數據庫處理能力方案

讀寫分離方案

海量數據的存儲及訪問,經過對數據庫進行讀寫分離,來提高數據的處理能力。讀寫分離它的方案特色是數據庫產生多個副本,數據庫的寫操做都集中到一個數據庫上,而一些讀的操做呢,能夠分解到其它數據庫上。這樣,只要付出數據複製的成本,就可使得數據庫的處理壓力分解到多個數據庫上,從而大大提高數據處理能力。 服務器

  • 優勢:因爲全部的數據庫副本,都有數據的全拷貝,所以全部的數據庫特性均可以實現,部分機器當機不影響系統的使用。
  • 缺點:數據的複製同步是一個問題,要麼採用數據庫自身的複製方案,要麼自行實現數據複製方案。須要考慮數據的遲滯性,一致性方面的問題。

數據分區方案

原來全部的數據都是在一個數據庫上的,網絡IO及文件IO都集中在一個數據庫上的,所以CPU、內存、文件IO、網絡IO均可能會成爲系統瓶頸。而分區的方案就是把某一個或某幾張相關的表的數據放在一個獨立的數據庫上,這樣就能夠把CPU、內存、文件IO、網絡IO分解到多個機器中,從而提高系統處理能力。 網絡

  • 優勢:不存在數據庫副本複製,性能更高。
  • 缺點:分區策略必須通過充分考慮,避免多個分區之間的數據存在關聯關係,每一個分區都是單點,若是某個分區宕機,就會影響到系統的使用。

數據分表方案

無論是上面的讀寫分離方案仍是數據分區方案,當數據量大到必定程度的時候,都會致使處理性能的不足,這個時候就沒有辦法了,只能進行分表處理。也就是把數據庫當中數據根據按照分庫原則分到多個數據表當中,這樣,就能夠把大表變成多個小表,不一樣的分表中數據不重複,從而提升處理效率。 app

  • 優勢:數據不存在多個副本,沒必要進行數據複製,性能更高。
  • 缺點:分表之間的數據不多進行集合運算;分表都是單點,若是某個分表宕機,若是使用的數據不在此分表,不影響使用。

分表也有兩種方案: 框架

1. 同庫分表:全部的分表都在一個數據庫中,因爲數據庫中表名不能重複,所以須要把數據表名起成不一樣的名字。 分佈式

  • 優勢:因爲都在一個數據庫中,公共表,沒必要進行復制,處理更簡單
  • 缺點:因爲還在一個數據庫中,CPU、內存、文件IO、網絡IO等瓶頸仍是沒法解決,只能下降單表中的數據記錄數。表名不一致,會導後續的處理複雜。

2. 不一樣庫分表:因爲分表在不一樣的數據庫中,這個時候就可使用一樣的表名。 函數

  • 優勢:CPU、內存、文件IO、網絡IO等瓶頸能夠獲得有效解決,表名相同,處理起來相對簡單
  • 缺點:公共表因爲在全部的分表都要使用,所以要進行復制、同步。

混合方案

經過上面的描述,咱們理解了讀寫分離,數據分區,數據分表三個解決方案,實際上都各有優勢,也各有缺 ,所以,實踐當中,會把三種方案混合使用。因爲數據不是一天長大的,實際上,在剛開始的時候,可能只採用其中一種方案,隨着應用的複雜,數據量的增加,會逐步採用多個方案混合的方案。以提高處理能力,避免單點。 性能

實現路線分析

正所謂條條大路通羅馬,解決這個問題的方案也有多種,但究其深源,均可以歸到兩種方案之上,一種是對用戶透明的方案,即用戶只用像普通的JDBC數據源同樣訪問便可,由框架解決全部的數據訪問問題。另一種是應用層解決,具體通常是在Dao層進行封裝。

JDBC層方案

  • 優勢:開發人員使用很是方便,開發工做量比較小;能夠實現數據庫無關。
  • 缺點:框架實現難度比較大,性能不必定能作到最優。

一樣是JDBC方案,也有兩種解決方案,一種是有代理模式,一種是無代理模式。

有代理模式,有一臺專門的代理服務器,來接收用戶請求,而後發送請求給數據庫集羣中的數據,並對數據進行聚集後再提交給請求方。

無代理模式,就是說沒有代理服務器,集羣框架直接部署在應用訪問端。

有代理模式,可以提供的功能更強大,甚至可買提供中間庫進行數據處理,無代理模式處理性能較強有代理模式少一次網絡訪問,相對來講性能更好,可是功能性不若有代理模式。

DAO層方案

  • 優勢:開發人員自由度很是大,性能調優更精準。
  • 缺點:開發人員在必定程度上受影響,與具體的Dao技術實現相關,較難作到數據庫無關。

因爲須要對SQL腳本進行判斷,而後進行路由,所以DAO層優化方案通常都是選用iBatis或Spring Jdbc Template等方案進行封裝,而對於Hibernate等高度封裝的OR映射方案,實現起來就很是困難了。

需求

需求決定了後續的解決方案及問題領域:

  • 採用JDBC層解決方案:對於最終用戶來講,要徹底透明
  • 採用無代理解決方案:數據庫集羣框架代碼直接放在應用層
  • 支持讀寫分離、分區、分表三種方式及其混合使用方式:三種方式能夠混用能夠提供極大的靈活性及對將來的擴展性
  • 須要提供靈活的分區及分表規則支持
  • 對於讀寫分離的方案,須要提供靈活的路由規則,好比:平均路由規則、加權路由規則,能夠提供寫庫的備用服務器,即主寫入服務器當機以後,便可寫入備用服務器當中。
  • 支持高性能分佈式主鍵生成器
  • 有良好的集羣事務功能
  • 能夠經過擴展點來對框架進行擴展,以便於處理分區、分表相關的操做。
  • 支持各類類型支持JDBC驅動的數據庫
  • 支持異構數據庫集羣
  • 支持count、sum、avg、min、max等統計函數
  • 支持排序
  • 支持光標移動
  • 支持結果集合並
  • 支持數據庫自增加主鍵
  • 支持數據庫分頁指令
  • 支持DDL語句處理
  • 支持Grovy腳本定義規則
  • 支持JDK1.5~1.8
  • 支持鏈接延遲獲取--只到使用時才申請數據庫鏈接,提高性能下降資源打敗

明確不支持的內容或限定條件:

  • 不支持分區之間的聯合查詢

特性說明

上面的紅色部分特性是最新添加的功能特性,這裏簡單解釋一下:

支持數據庫自增加主鍵:好比:Mysql,SqlServer等數據庫支持 auto increase主鍵,原來不支持,如今能夠完美支持了

支持數據庫分頁指令:好比,有些數據庫支持start limit或相似的分頁指令,原來不支持,如今能夠完美支持了

支持DDL語句:原來對數據庫結構方面的操縱,原來不支持,如今能夠完美的支持了,好比:能夠一次修改全部的分片的表結構

支持Grovy腳本定義分區分片規則:原來是隻能採用實現接口的方式進行分區和分片規則的實現,如今能夠用Grovyy腳原本定義了

支持JDK1.5~1.8:原來是是分紅兩個版本,一個jdbc3,一個jdbc4的,框架開發和維護很是麻煩,如今合併成一個工程,兩個能夠同步支持了

支持鏈接延遲獲取:原來是整個分區分片中須要的鏈接一次申請到,如今優化爲須要時才申請,這樣會大大下降對數據庫鏈接資源的須要,同時因爲事務變小,也能夠顯著提高處理效率。

結構設計

框架採用三層設計:最上層是Cluster,一個Cluster至關於咱們常規的一個數據庫;一個Cluster當中能夠包含一到多個Partition,也就是分區;而一個Partition中能夠包含一到多個Shard,也就是分片。

因此一個就造成了一個樹狀結構,經過Cluster->Partion->Shard就構成了整個數據庫集羣。可是對於開發人員來講,實際上並不知道這個內部結構,他只是鏈接上了一個JDBC數據源,而後作它應該作的事情就能夠了。

Cluster

以完整的形態對外提供服務,它封裝了Cluster當中全部Partition及其Shard的訪問。把它打開是一個數據庫集羣,對於使用者來講是一個完整的數據庫。

屬性名

類型

說明

id

String

集羣標識

userName

String

鏈接集羣時的用戶名

Password

String

鏈接集羣時的密碼

dataSources

List<DataSourceConfig>

集羣中須要訪問的數據源列表

partitions

List<Partition>;

集羣中包含的分區列表

Partition

分區,分區有兩種模式,一種是主從模式,用於作讀寫分離;另一種模式是分片模式,也就是說把一個表中的數據分解到多個表中。一個分區只能是其中的一種模式。可是一個Cluster能夠包含多個分區,不一樣的分區能夠是不一樣的模式。

屬性名

類型

說明

id

String

分區標識

mode

int

分區類型,能夠是主從,也能夠是分表

Password

String

鏈接集羣時的密碼

shards

List<Shard>

分區中包含的分片列表

partitionRules

List<PartitionRule>

分區規則,當進行處理的時候,路由到哪一個分區執行

Shard

Shard與一個物理的數據源相關聯。

屬性名

類型

說明

id

String

分區標識

dataSourceId

String

實際訪問的數據庫配置ID

readWeight

int

讀權重,僅用於主從讀寫分離模式

writeWeight

int

寫權重,僅用於主從讀寫分離模式

shardRules

List<ShardRule>

分片規則,當進行處理的時候,路由到哪一個分片執行,僅用於分模式

tableMappings

List<TableMapping>;

表名映射列表,僅用於同庫不一樣表名分表模式

分佈式主鍵接口

/**

* 分佈式Key獲取器

*

* @param <T>

*/

public interface ClusterKeyGenerator<T> {

T getKey(String tableName);

}

主鍵接口能夠用來生成各類主鍵類型,如:字符串、整型、長整型,入口參數必須是表名,框架已經實現了字符串、整型、長整型的分佈式高效主鍵生成器,固然,也能夠自行實現。

集羣管理器

public interface ClusterManager {

/**

* 返回是不是分片語句

*

* @param partition

* @param sql

* @return

*/

boolean isShardSql(Partition partition, String sql);

/**

* 添加語句處理器

*

* @param statementProcessor

*/

void addStatementProcessor(StatementProcessor statementProcessor);

/**

* 返回語句處理器列表

*

* @return

*/

List<StatementProcessor> getStatementProcessorList();

/**

* 給某個集羣的數據表產生主鍵

*

* @param cluster

* @param tableName

* @param <T>

* @return

*/

<T> T getPrimaryKey(Cluster cluster, String tableName);

/**

* 返回SQL對應的Statement

*

* @param sql

* @return

*/

Statement getSqlStatement(String sql);

/**

* 添加集羣

*

* @param cluster

*/

void addCluster(Cluster cluster);

/**

* 獲取集羣

*

* @param clusterId

* @return

*/

Cluster getCluster(String clusterId);

/**

* 返回某個分區與sql是否匹配

*

* @param partition

* @param sql

* @return

*/

boolean isMatch(Partition partition, String sql);

/**

* 返回某個分片是否匹配

*

* @param shard

* @param sql

* @return

*/

boolean isMatch(Partition partition, Shard shard, String sql);

/**

* 返回分片執行語句

*

* @param partition

* @param shard

* @param sql

* @return

*/

String getSql(Partition partition, Shard shard, String sql);

/**

* 獲取匹配的分區<br>

*

* @param clusterId

* @param sql

* @return

*/

Collection<Partition> getPartitions(String clusterId, String sql);

/**

* 獲取匹配的首個分區

*

* @param clusterId

* @param sql

* @return

*/

Partition getPartition(String clusterId, String sql);

/**

* 獲取匹配的首個分區

*

* @param cluster

* @param sql

* @return

*/

Partition getPartition(Cluster cluster, String sql);

/**

* 獲取匹配的分區

*

* @param cluster

* @param sql

* @return

*/

List<Partition> getPartitions(Cluster cluster, String sql);

/**

* 獲取匹配的分片

*

* @param partition

* @param sql

* @return

*/

List<Shard> getShards(Partition partition, String sql);

/**

* 返回分片均衡器

*

* @return

*/

ShardBalance getShardBalance();

/**

* 設置分片均衡器

*

* @param balance

*/

void setShardBalance(ShardBalance balance);

}

分區規則

/**

* 分區規則接口<br>

* 規則參數在實現類中定義

*

*/

public interface PartitionRule {

/**

* 返回是否命中,若是有多個命中,則只用第一個進行處理

*

* @param sql

* @return

*/

boolean isMatch(String sql);

}

框架自帶了經常使用分區規則,可是也能夠根據狀況自已擴展。

分片規則

/**

* 分片規則

*

*/

public interface ShardRule {

/**

* 返回是否屬於當前分片處理

*

* @param sql

* @return

*/

boolean isMatch(Partition partition, String sql);

}

框架自帶了經常使用的分片規則,可是也能夠根據狀況自已擴展

語句處理器

/**

* 用於對SQL進行特殊處理並進行結果合併等<br>

* <p/>

* 好比sql語句是select count(*) from abc<br>

* 則會到全部的shard執行,並對結果相加後返回

*

*/

public interface StatementProcessor {

/**

* 返回是否由此SQL處理器進行處理

*

* @param sql

* @return

*/

boolean isMatch(String sql);

/**

* 返回處理器轉換過以後的SQL

*

* @param sql

* @return

*/

String getSql(String sql);

/**

* 對結果進行合併

*

* @param results

* @return

* @throws SQLException

*/

ResultSet combineResult(List<ResultSet> results) throws SQLException;

}

在分片以後,許多時候單純查找回的數據已是不正確的了,這個時候就須要進行二次處理才能保證返回的結果是正確的。

好比:用戶輸入的SQL語句是:Select count(*) from aaa

這個時候就會用分片指令到各個分片去查找並返回結果,默認的處理結果是簡單合併結果集的方式。這個時候,若是有5個分片,會返回5條記錄給最終用戶,這固然不是他想要的結果。這個時候就是語句處理器大顯身手的時候了,他能夠偷樑換柱,也能夠改頭換面,經過它的處理,就能夠返回正確的結果了。

JDBC層封裝

固然,要想外部程序用JDBC的方式進行訪問,就得作JDBC層的實現。這個部分作了大量的處理,使得即高效又與用戶指望的方式相匹配。

能夠說上面全部的準備都是爲了這一層作準備的,畢竟最終要落到真正的數據庫訪問上。因爲接口就是標準的JDBC接口,所以就再也不詳述。

事務問題

在分區或分表模式中,因爲寫操做會被分解到不一樣的物理數據庫上去,這就會致使出現事務問題。所以框架內部集成了JTA,使得事務保持一致。

代碼實現

沒什麼好說的,噼裏啪啦,噼裏啪啦,一陣亂響,代碼就緒了,下面看看測試場景。

測試用例

JDBC方式訪問集羣

Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver");

Connection conn = DriverManager.getConnection("jdbc:dbcluster://cluster1", "username", "password");

Statement stmt = conn.createStatement();

stmt.execute(「select * from aaa」);

代碼解釋:

上面徹底是按照JDBC的方式訪問數據庫的,url必須以「jdbc:dbcluster://」開始,後面跟着的是集羣的ID名稱,上面示例中就是「cluster1」;用戶名、密碼必須與集羣中配置的相一致。接下來就與普通的jdbc數據源沒有任何區別了。

同庫分表

建立測試表

在同一個數據庫中建立一樣結構的表,好比:

CREATE TABLE `aaa0` (

`id` int(11) NOT NULL,

`aaa` varchar(45) DEFAULT NULL,

PRIMARY KEY (`id`)

);

CREATE TABLE `aaa1` (

`id` int(11) NOT NULL,

`aaa` varchar(45) DEFAULT NULL,

PRIMARY KEY (`id`)

);

CREATE TABLE `aaa2` (

`id` int(11) NOT NULL,

`aaa` varchar(45) DEFAULT NULL,

PRIMARY KEY (`id`)

);

測試代碼:

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

Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver");

Connection conn = DriverManager.getConnection("jdbc:dbcluster://cluster1", "username", "password");

Statement stmt = conn.createStatement();

String sql;

//插入100條數據

for (int i = 0; i < 100; i++) {

sql = "insert into aaa(id,aaa) values (" + clusterManager.getPrimaryKey(cluster, "aaa") + ",'ppp')";

boolean result = stmt.execute(sql);

}

}

運行結果:

Using shard:shard1 to execute sql:insert into aaa(id,aaa) values (1,'ppp')

Using shard:shard2 to execute sql:insert into aaa(id,aaa) values (2,'ppp')

Using shard:shard0 to execute sql:insert into aaa(id,aaa) values (3,'ppp')

Using shard:shard1 to execute sql:insert into aaa(id,aaa) values (4,'ppp')

Using shard:shard2 to execute sql:insert into aaa(id,aaa) values (5,'ppp')

Using shard:shard0 to execute sql:insert into aaa(id,aaa) values (6,'ppp')

…….

能夠看出,插入的數據確實分到了三個分片中。

再用Select語句查找插入的數據:

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

Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver");

Connection conn =

DriverManager.getConnection("jdbc:dbcluster://cluster1", "username", "password");

Statement stmt = conn.createStatement();

String sql = "select * from aaa order by id";

ResultSet resultSet = stmt.executeQuery(sql);

while (resultSet.next()) {

System.out.printf(" id: %d, aaa: %s \n", resultSet.getInt(1), resultSet.getString(2));

}

}

運行結果以下:

Using shard:shard0 to execute sql:select * from aaa order by id

Using shard:shard1 to execute sql:select * from aaa order by id

Using shard:shard2 to execute sql:select * from aaa order by id

id: 1, aaa: ppp

id: 2, aaa: ppp

id: 3, aaa: ppp

id: 4, aaa: ppp

id: 5, aaa: ppp

id: 6, aaa: ppp

……

從上面的結果能夠看到,明顯已經合併告終果而且是按順序顯示的

接下來,把測試的數據刪除掉:

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

Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver");

Connection conn = DriverManager.getConnection("jdbc:dbcluster://cluster1", " username ", "password");

Statement stmt = conn.createStatement();

String sql = "delete from aaa";

stmt.execute(sql);

}

運行結果以下:

Using shard:shard0 to execute sql:delete from aaa

Using shard:shard1 to execute sql:delete from aaa

Using shard:shard2 to execute sql:delete from aaa

再去數據庫中查看,數據確實已經被刪除。

不一樣庫分表

在不一樣的數據庫中建立一樣結構的表,好比:

CREATE TABLE test0. aaa (

`id` int(11) NOT NULL,

`aaa` varchar(45) DEFAULT NULL,

PRIMARY KEY (`id`)

);

CREATE TABLE test1. aaa(

`id` int(11) NOT NULL,

`aaa` varchar(45) DEFAULT NULL,

PRIMARY KEY (`id`)

);

CREATE TABLE test2. aaa(

`id` int(11) NOT NULL,

`aaa` varchar(45) DEFAULT NULL,

PRIMARY KEY (`id`)

);

測試用例同同庫分表,結果測試一樣OK。

讀寫分離

插入與刪除等比較簡單,就再也不展現了,下面看看讀指令的執行過程。

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

Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver");

Connection conn =

DriverManager.getConnection("jdbc:dbcluster://cluster1", "username", "password");

Statement stmt = conn.createStatement();

for (int i = 1; i <= 100; i++) {

boolean result = stmt.execute(「select * from aaa」);

}

}

運行結果:

Using shard:shard3 to execute sql:select * from aaa

Using shard:shard2 to execute sql:select * from aaa

Using shard:shard1 to execute sql:select * from aaa

Using shard:shard1 to execute sql:select * from aaa

Using shard:shard1 to execute sql:select * from aaa

Using shard:shard1 to execute sql:select * from aaa

Using shard:shard2 to execute sql:select * from aaa

Using shard:shard1 to execute sql:select * from aaa

Using shard:shard1 to execute sql:select * from aaa

Using shard:shard1 to execute sql:select * from aaa

Using shard:shard1 to execute sql:select * from aaa

Using shard:shard1 to execute sql:select * from aaa

Using shard:shard3 to execute sql:select * from aaa

能夠看到,讀的SQL已經由三個分片進行了均衡執行。

記錄集遍歷

對於ResultSet的遍歷,也有良好的支持,對於各類移動光標的方法都有支持,並且支持排序的移動,同時對於性能也有良好支持,性能接近於單表操做。

下面展現一下絕對定位:

Class.forName("org.tinygroup.dbcluster.jdbc.TinyDriver");

Connection conn = DriverManager.getConnection(

"jdbc:dbcluster://cluster1", "luog", "123456");

Statement stmt = conn.createStatement();

String sql = "select * from aaa order by id";

ResultSet resultSet = stmt.executeQuery(sql);

resultSet.absolute(10);

System.out.printf(" id: %d, aaa: %s \n", resultSet.getInt(1),

resultSet.getString(2));

while (resultSet.next()) {

System.out.printf(" id: %d, aaa: %s \n", resultSet.getInt(1),

resultSet.getString(2));

}

運行結果:

Using shard:shard0 to execute sql:select * from aaa order by id

Using shard:shard1 to execute sql:select * from aaa order by id

Using shard:shard2 to execute sql:select * from aaa order by id

id: 10, aaa: ppp

id: 11, aaa: ppp

id: 12, aaa: ppp

id: 13, aaa: ppp

id: 14, aaa: ppp

id: 15, aaa: ppp

id: 16, aaa: ppp

id: 17, aaa: ppp

id: 18, aaa: ppp

id: 19, aaa: ppp

…….

能夠看到確實是從第10條開始顯示。

總結

分區分片通用解決方案,確實有至關的通用性,支持各類數據庫,提供了很是大的靈活性,支持多種集羣單獨或混合使用的場景,同時還能夠保持數據訪問的事務一致性,爲用戶的訪問提供與JDBC同樣的用戶接口,這也會大大下降開發人員的開發難度。基本上(違反需求中指定的限制條件的除外)能夠作到原有業務代碼透明訪問,下降了系統的遷移成本。同時它在性能方面也是很是傑出的,與原生的JDBC驅動程序相比,性能沒有顯著下降。固然它的配置也是很是簡單的,學習成本很是低。因爲作在JDBC層,所以能夠對Hibernate,iBatis等各類框架有良好支持。

相關文章
相關標籤/搜索