一直在作企業應用,目前要作一些互聯網應用,固然只是應用是放在互聯網的,數據量距離真正的互聯網應用仍是有至關大的差距的。可是不可避免的,在數據庫出現瓶頸的狀況仍是有的,如今作互聯網上的應用,固然也要未雨綢繆,要考慮數據量大的時候的解決方案。 java
這個目前開源的商用的也都有很多解決方案,一來,作技術的都有這麼個臭毛病,即便是使用別人的方案,本身也要搞清楚內部的一些實現機制,這樣纔會有真正的體會,不然去評估一個方案的時候,就只能盲人摸象了。 sql
爲此,構建一個驗證型的分佈式數據庫框架,來解決數據庫的垂直與水平擴展方面的問題,因爲是驗證性開發,因此,思考不完善的地方確定存在,歡迎批評指正。 數據庫
海量數據的存儲及訪問,經過對數據庫進行讀寫分離,來提高數據的處理能力。讀寫分離它的方案特色是數據庫產生多個副本,數據庫的寫操做都集中到一個數據庫上,而一些讀的操做呢,能夠分解到其它數據庫上。這樣,只要付出數據複製的成本,就可使得數據庫的處理壓力分解到多個數據庫上,從而大大提高數據處理能力。 服務器
原來全部的數據都是在一個數據庫上的,網絡IO及文件IO都集中在一個數據庫上的,所以CPU、內存、文件IO、網絡IO均可能會成爲系統瓶頸。而分區的方案就是把某一個或某幾張相關的表的數據放在一個獨立的數據庫上,這樣就能夠把CPU、內存、文件IO、網絡IO分解到多個機器中,從而提高系統處理能力。 網絡
無論是上面的讀寫分離方案仍是數據分區方案,當數據量大到必定程度的時候,都會致使處理性能的不足,這個時候就沒有辦法了,只能進行分表處理。也就是把數據庫當中數據根據按照分庫原則分到多個數據表當中,這樣,就能夠把大表變成多個小表,不一樣的分表中數據不重複,從而提升處理效率。 app
分表也有兩種方案: 框架
1. 同庫分表:全部的分表都在一個數據庫中,因爲數據庫中表名不能重複,所以須要把數據表名起成不一樣的名字。 分佈式
2. 不一樣庫分表:因爲分表在不一樣的數據庫中,這個時候就可使用一樣的表名。 函數
經過上面的描述,咱們理解了讀寫分離,數據分區,數據分表三個解決方案,實際上都各有優勢,也各有缺 ,所以,實踐當中,會把三種方案混合使用。因爲數據不是一天長大的,實際上,在剛開始的時候,可能只採用其中一種方案,隨着應用的複雜,數據量的增加,會逐步採用多個方案混合的方案。以提高處理能力,避免單點。 性能
正所謂條條大路通羅馬,解決這個問題的方案也有多種,但究其深源,均可以歸到兩種方案之上,一種是對用戶透明的方案,即用戶只用像普通的JDBC數據源同樣訪問便可,由框架解決全部的數據訪問問題。另一種是應用層解決,具體通常是在Dao層進行封裝。
一樣是JDBC方案,也有兩種解決方案,一種是有代理模式,一種是無代理模式。
有代理模式,有一臺專門的代理服務器,來接收用戶請求,而後發送請求給數據庫集羣中的數據,並對數據進行聚集後再提交給請求方。
無代理模式,就是說沒有代理服務器,集羣框架直接部署在應用訪問端。
有代理模式,可以提供的功能更強大,甚至可買提供中間庫進行數據處理,無代理模式處理性能較強有代理模式少一次網絡訪問,相對來講性能更好,可是功能性不若有代理模式。
因爲須要對SQL腳本進行判斷,而後進行路由,所以DAO層優化方案通常都是選用iBatis或Spring Jdbc Template等方案進行封裝,而對於Hibernate等高度封裝的OR映射方案,實現起來就很是困難了。
需求決定了後續的解決方案及問題領域:
明確不支持的內容或限定條件:
上面的紅色部分特性是最新添加的功能特性,這裏簡單解釋一下:
支持數據庫自增加主鍵:好比: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當中全部Partition及其Shard的訪問。把它打開是一個數據庫集羣,對於使用者來講是一個完整的數據庫。
屬性名 |
類型 |
說明 |
id |
String |
集羣標識 |
userName |
String |
鏈接集羣時的用戶名 |
Password |
String |
鏈接集羣時的密碼 |
dataSources |
List<DataSourceConfig> |
集羣中須要訪問的數據源列表 |
partitions |
List<Partition>; |
集羣中包含的分區列表 |
分區,分區有兩種模式,一種是主從模式,用於作讀寫分離;另一種模式是分片模式,也就是說把一個表中的數據分解到多個表中。一個分區只能是其中的一種模式。可是一個Cluster能夠包含多個分區,不一樣的分區能夠是不一樣的模式。
屬性名 |
類型 |
說明 |
id |
String |
分區標識 |
mode |
int |
分區類型,能夠是主從,也能夠是分表 |
Password |
String |
鏈接集羣時的密碼 |
shards |
List<Shard> |
分區中包含的分片列表 |
partitionRules |
List<PartitionRule> |
分區規則,當進行處理的時候,路由到哪一個分區執行 |
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接口,所以就再也不詳述。
在分區或分表模式中,因爲寫操做會被分解到不一樣的物理數據庫上去,這就會致使出現事務問題。所以框架內部集成了JTA,使得事務保持一致。
沒什麼好說的,噼裏啪啦,噼裏啪啦,一陣亂響,代碼就緒了,下面看看測試場景。
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等各類框架有良好支持。