Simple-Sharding : 一款極簡的分庫分表中間件

Simple-Sharding是一款基于JDBC API開發、簡單易用的分庫分表中間件,目標是通過較少的代碼來揭示分庫分表中間件最核心的本質。

背景

目前大多數互聯網公司在遇到數據層瓶頸的時候,幾乎都會做垂直或水平拆分。垂直拆分即按業務將庫表分離,但是當拆分后的單表數據量達到一個新的量級的時候,會接著對這個大表做水平拆分,即將單個大表拆分成多個分表,有時會將其中的一些分表落地到不同的分庫,以此來應對快速增長的業務。

從知名的分庫分表中間件TDDL和Cobar開始,各個公司也都相繼研發甚至開源了自己的分庫分表中間件,這些中間件主要分為兩類:一類是基于JDBC API實現的中間件,一類是類似于MySQL Proxy的代理中間件。整體的思路都是通過攔截應用層的SQL請求,根據相應規則做路由分發,然后落地到物理節點,最后執行獲取結果。

根據筆者研究市面上已有代碼的經驗,發現成熟的項目往往代碼量龐大,歷史變更較多,導致學習研究分庫分表中間件并不是一件十分容易的事情,很多時候抓不住本質,于是筆者就根據目前已經學習到的知識和經驗,自己動手寫了一個極簡主義的分庫分表中間件,我把她命名為Simple-Sharding !

開源地址為https://github.com/yuanwhy/simple-sharding ,歡迎Star,嘿嘿~

Simple-Sharding

下面具體介紹Simple-Sharding的一些細節以及筆者在其中的思考,歡迎批評指正。

實現思路

每一個學習過Java操作數據庫的同學,最開始都是從JDBC的知識入手,后來才慢慢在項目中引入像Mybatis這樣的ORM框架,建立起更加復雜的DAL(Data Access Layer)。

比如典型的代碼如下:

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(DB_URL,USER,PASS);
Statement stmt = conn.createStatement();
String sql;
sql = "SELECT id, first, last, age FROM Employees";
ResultSet rs = stmt.execute(sql);

JDBC全稱Java Database Connectivity, 是Java官方定義的一套訪問數據庫的接口規范,這些接口主要包括:

  • DataSource
  • Connection
  • Statement
  • PreparedStatement
  • ResultSet

每個數據庫廠商都會自己實現這一套接口并提供給應用程序使用,比如MySQL提供的Connector/J,程序包為mysql-connector-java-{version}.jar。再復雜的DAO(Data Access Object),本質上內部還是通過JDBC來操作數據庫,所有的ORM框架內部只有通過調用JDBC才能獲取數據庫連接。所以,Simple-Sharding的設計就是重寫這套JDBC API,提供給應用新的DataSource實現類。

在Simple-Sharding中, 實現了前四個關鍵的接口,即

  • LogicDataSource
  • LogicConnection
  • LogicStatement
  • LogicPreparedStatement

比如,LogicDataSource的主要作用就是創建邏輯意義的數據庫連接給上層使用,內部實現如下:

@Override
public Connection getConnection() throws SQLException {

    Connection connection = new LogicConnection(this);

    return connection;
}

應用將Simple-Sharding的DataSource注入到自己的IoC容器中,代替傳統的c3p0或dbcp數據源:

<bean id="dataSource" class="com.yuanwhy.simple.sharding.jdbc.LogicDataSource">
    <property name="logicDatabase" value="passport"/>
    <property name="shardingRule" ref="shardingRule"/>
    <property name="physicalDataSourceMap">
        <map>
            <entry key="passport_0" value-ref="physicalDataSource0"/>
            <entry key="passport_1" value-ref="physicalDataSource1"/>
        </map>
    </property>
</bean>

而LogicConnection的主要作用就是獲得Statement,而Statement是執行SQL語句的關鍵:

@Override
public Statement createStatement() throws SQLException {

    Statement statement = new  LogicStatement(this);

    return statement;
}

@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {

    LogicPreparedStatement prepareStatement = new LogicPreparedStatement(this, sql);

    return prepareStatement;
}

規則接口

除了提供JDBC API之外,還要提供給應用指明分庫分表規則的接口,因為中間件需要根據用戶定義的規則對原始的SQL進行路由和重寫,即根據分庫字段獲得分庫的真實庫名,根據分表字段獲得分表的真實表名。

public interface ShardingRule {

    String getFieldNameForDb();

    String getFieldNameForTable();

    String getDbSuffix(Object fieldValueForDb);

    String getTableSuffix(Object fieldValueForTable);

}

接口ShardingRule定義了四個方法,getFieldNameForDb是希望能得知哪一個是分庫字段,getFieldNameForTable是希望能得知哪一個是分表字段,然后通過getDbSuffix和getTableSuffix分別從分庫字段值和分表字段值中計算出物理庫名和物理表名的后綴。

Simple-Sharding提供了一個默認的分庫分表規則的實現HashShardingRule,該規則采用取模hash法來獲取后綴,當然應用也可以自己實現這個接口,來自定義分庫分表規則。

數據模型

在Simple-Sharding的Unit Test中建立了這樣一個數據模型:在passport庫中有user表,user表分買家(Role為0)和賣家(Role為1),user表有id、name、role等必要的字段。傳統的場景是passport庫中只有一個user表,現在根據需求做分庫分表,一種比較合理的方案是按買家和賣家來分庫,每個庫再做分表,于是邏輯架構如下圖所示:

User

分表規則默認使用HashShardingRule,為了考慮分布的均勻性,一般選擇id為分表字段進行取模運算作為表名后綴,比如這里分了兩個表,用戶id為11的分表為user_1表(11%2 = 1)。用戶id應該設置為全局唯一,這時候數據庫自增顯然不再適用,全局唯一id生成算法又是另外一個話題了,這里不作為討論的重點。

SQL解析與執行

在調用statement.execute(sql)執行SQL時,statement獲得的是原始SQL,中間件需要把原始SQL語句解析成AST(抽象語法樹),然后取得分庫分表字段值。Simple-Sharding使用了現成的SQL Parser工具 ,即阿里開源的Druid內部的SQL Parser組件:

List<SQLStatement> sqlStatements = SQLUtils.parseStatements(sql, JdbcConstants.MYSQL);

SQLStatement currentSqlStatement = sqlStatements.get(0);
MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();
currentSqlStatement.accept(visitor);

之后就可以通過currentSqlStatement和visitor直接獲得分庫分表字段值,當然前提是ShardingRule中配置了分庫分表字段的名稱。

解析之后,一方面要從配置的所有物理庫中獲取目標物理分庫physicalDataSource0或physicalDataSource1,另一方面要將原始SQL進行替換,將passport替換成passport_0或passport_1、user替換成user_0或user_1。

比如原始SQL為

select * from passport.user where role = 0 and id = 11;

重寫后的SQL為

select * from passport_0.user_1 where role = 0 and id = 11;

之后便是通過獲取到的物理數據源physicalDataSource0來像傳統方式一樣執行SQL語句,并將獲得的結果集返回給上層應用。在Simple-Sharding的LogicStatement類的doExecute方法中具體展示了在物理數據源上執行真實SQL的過程。

事務支持

Simple-Sharding目前僅支持單庫事務,分布式事務太過復雜,目前暫不考慮。單庫事務的實現思路其實很簡單,只要保證一串事務內的SQL解析后都落地到同一個分庫即可,即整個事務階段LogicConnection只會使用一個物理Connection,事務結束后又開啟新的事務的時候,LogicConnection又會開啟一個新的可能完全不同的物理Connetion。

關鍵代碼如下:

if(this.logicConnection.getPhysicalConnection() != null) {

    if (physicalDbName.equals(this.logicConnection.getPhysicalDbName())) {

        physicalConnection = this.logicConnection.getPhysicalConnection();

    } else {
        throw new RuntimeException("不支持跨庫事務 : " + originalSql);
    }

} else {

    physicalConnection = physicalDataSource.getConnection();

    this.logicConnection.setPhysicalConnection(physicalConnection);
    this.logicConnection.setPhysicalDbName(physicalDbName);

    physicalConnection.setAutoCommit(this.logicConnection.getAutoCommit());

}

那么如何測試事務呢?在Simple-Sharding的Unit Test中給出了測試事務的用例:獲取Connection之后,autoCommit設置為false便在邏輯上開啟了一個事務(autoCommit=true的時候每一條SQL都默認是一個事務),之后執行增刪改查并且沒提交的時候,在其他會話中會表現出隔離性(MySQL默認隔離級別):

connection1.setAutoCommit(false);

User user = new User(123, "yuanwhy", 18, User.Role.BUYER.getId());
insertUser(connection1, user);
User foundUserFromConnection1 = selectUser(connection1, user);
Assert.assertTrue(user.equals(foundUserFromConnection1));

User foundUserFromConnection2 = selectUser(connection2, user);
Assert.assertTrue(foundUserFromConnection2 == null); // 事務隔離,connection2一定讀不到connection1的數據

這樣就基本在Simple-Sharding中實現了單庫事務,單庫事務對實際應用也是必須的,這一點在很多分庫分表中間件中都有實現。

總結

Simple-Sharding在JDBC API的基礎上實現了一套新的數據源,內部提供了基本的分庫分表支持,同時簡單地實現了單庫事務,基本上把分庫分表中間件最核心的流程走通了一遍。作為研究性質的項目,Simple-Sharding目前還很年輕,不推薦在生產環境中使用,希望對于想學習分庫分表中間件的同學有所幫助。

最后,歡迎Star和交流 : https://github.com/yuanwhy/simple-sharding .

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,030評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,310評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,951評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,796評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,566評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,055評論 1 322
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,142評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,303評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,799評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,683評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,899評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,409評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,135評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,520評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,757評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,528評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,844評論 2 372

推薦閱讀更多精彩內容