1. 程式人生 > >分庫分表神器 Sharding-JDBC,幾千萬的資料你不搞一下?

分庫分表神器 Sharding-JDBC,幾千萬的資料你不搞一下?

今天我們介紹一下 `Sharding-JDBC`框架和快速的搭建一個分庫分表案例,為講解後續功能點準備好環境。 ### **一、Sharding-JDBC 簡介** `Sharding-JDBC` 最早是噹噹網內部使用的一款分庫分表框架,到2017年的時候才開始對外開源,這幾年在大量社群貢獻者的不斷迭代下,功能也逐漸完善,現已更名為 `ShardingSphere`,2020年4⽉16⽇正式成為 `Apache` 軟體基⾦會的頂級項⽬。 隨著版本的不斷更迭 `ShardingSphere` 的核心功能也變得多元化起來。從最開始 Sharding-JDBC 1.0 版本只有資料分片,到 Sharding-JDBC 2.0 版本開始支援資料庫治理(註冊中心、配置中心等等),再到 Sharding-JDBC 3.0版本又加分散式事務 (支援 `Atomikos`、`Narayana`、`Bitronix`、`Seata`),如今已經迭代到了 Sharding-JDBC 4.0 版本。 ![](https://upload-images.jianshu.io/upload_images/15462057-1338c2fe728a608b?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 現在的 ShardingSphere 不單單是指某個框架而是一個生態圈,這個生態圈 `Sharding-JDBC`、`Sharding-Proxy` 和 `Sharding-Sidecar` 這三款開源的分散式資料庫中介軟體解決方案所構成。 `ShardingSphere` 的前身就是 `Sharding-JDBC`,所以它是整個框架中最為經典、成熟的元件,我們先從 `Sharding-JDBC` 框架入手學習分庫分表。 ### **二、核心概念** 在開始 `Sharding-JDBC`分庫分表具體實戰之前,我們有必要先了解分庫分表的一些核心概念。 **分片** 一般我們在提到分庫分表的時候,大多是以水平切分模式(水平分庫、分表)為基礎來說的,資料分片將原本一張資料量較大的表 `t_order` 拆分生成數個表結構完全一致的小資料量表 `t_order_0`、`t_order_1`、···、`t_order_n`,每張表只儲存原大表中的一部分資料,當執行一條`SQL`時會通過 `分庫策略`、`分片策略` 將資料分散到不同的資料庫、表內。 ![](https://upload-images.jianshu.io/upload_images/15462057-9296eacc73c5f733?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) **資料節點** 資料節點是分庫分表中一個不可再分的最小資料單元(表),它由資料來源名稱和資料表組成,例如上圖中 `order_db_1.t_order_0`、`order_db_2.t_order_1` 就表示一個數據節點。 **邏輯表** 邏輯表是指一組具有相同邏輯和資料結構表的總稱。比如我們將訂單表`t_order` 拆分成 `t_order_0` ···  `t_order_9` 等 10張表。此時我們會發現分庫分表以後資料庫中已不在有 `t_order` 這張表,取而代之的是 `t_order_n`,但我們在程式碼中寫 `SQL` 依然按 `t_order` 來寫。此時 `t_order` 就是這些拆分表的`邏輯表`。 **真實表** 真實表也就是上邊提到的 `t_order_n` 資料庫中真實存在的物理表。 **分片鍵** 用於分片的資料庫欄位。我們將 `t_order` 表分片以後,當執行一條SQL時,通過對欄位 `order_id` 取模的方式來決定,這條資料該在哪個資料庫中的哪個表中執行,此時 `order_id` 欄位就是 `t_order` 表的分片健。 ![](https://upload-images.jianshu.io/upload_images/15462057-5132895cecad97d7?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 這樣以來同一個訂單的相關資料就會存在同一個資料庫表中,大幅提升資料檢索的效能,不僅如此 `sharding-jdbc` 還支援根據多個欄位作為分片健進行分片。 **分片演算法** 上邊我們提到可以用分片健取模的規則分片,但這只是比較簡單的一種,在實際開發中我們還希望用 `>=`、`<=`、`>`、`<`、`BETWEEN` 和 `IN` 等條件作為分片規則,自定義分片邏輯,這時就需要用到分片策略與分片演算法。 從執行 SQL 的角度來看,分庫分表可以看作是一種路由機制,把 SQL 語句路由到我們期望的資料庫或資料表中並獲取資料,分片演算法可以理解成一種路由規則。 咱們先捋一下它們之間的關係,分片策略只是抽象出的概念,它是由分片演算法和分片健組合而成,分片演算法做具體的資料分片邏輯。 > 分庫、分表的分片策略配置是相對獨立的,可以各自使用不同的策略與演算法,每種策略中可以是多個分片演算法的組合,每個分片演算法可以對多個分片健做邏輯判斷。 ![](https://upload-images.jianshu.io/upload_images/15462057-4167a9e70240b4ac?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) > **注意**:sharding-jdbc 並沒有直接提供分片演算法的實現,需要開發者根據業務自行實現。 `sharding-jdbc` 提供了4種分片演算法: **1、精確分片演算法** 精確分片演算法(PreciseShardingAlgorithm)用於單個欄位作為分片鍵,SQL中有 `=` 與 `IN` 等條件的分片,需要在標準分片策略(`StandardShardingStrategy` )下使用。 **2、範圍分片演算法** 範圍分片演算法(RangeShardingAlgorithm)用於單個欄位作為分片鍵,SQL中有 `BETWEEN AND`、`>`、`<`、`>=`、`<=`  等條件的分片,需要在標準分片策略(`StandardShardingStrategy` )下使用。 **3、複合分片演算法** 複合分片演算法(ComplexKeysShardingAlgorithm)用於多個欄位作為分片鍵的分片操作,同時獲取到多個分片健的值,根據多個欄位處理業務邏輯。需要在複合分片策略(`ComplexShardingStrategy` )下使用。 **4、Hint分片演算法** Hint分片演算法(HintShardingAlgorithm)稍有不同,上邊的演算法中我們都是解析`SQL` 語句提取分片鍵,並設定分片策略進行分片。但有些時候我們並沒有使用任何的分片鍵和分片策略,可還想將 SQL 路由到目標資料庫和表,就需要通過手動干預指定SQL的目標資料庫和表資訊,這也叫強制路由。 **分片策略** 上邊講分片演算法的時候已經說過,分片策略是一種抽象的概念,實際分片操作的是由分片演算法和分片健來完成的。 **1、標準分片策略** 標準分片策略適用於單分片鍵,此策略支援 `PreciseShardingAlgorithm` 和 `RangeShardingAlgorithm` 兩個分片演算法。 其中 `PreciseShardingAlgorithm` 是必選的,用於處理 `=` 和 `IN` 的分片。`RangeShardingAlgorithm` 是可選的,用於處理`BETWEEN AND`, `>`, `<`,`>=`,`<=` 條件分片,如果不配置`RangeShardingAlgorithm`,SQL中的條件等將按照全庫路由處理。 **2、複合分片策略** 複合分片策略,同樣支援對 SQL語句中的 `=`,`>`, `<`, `>=`, `<=`,`IN`和 `BETWEEN AND` 的分片操作。不同的是它支援多分片鍵,具體分配片細節完全由應用開發者實現。 **3、行表示式分片策略** 行表示式分片策略,支援對 SQL語句中的 `=` 和 `IN` 的分片操作,但只支援單分片鍵。這種策略通常用於簡單的分片,不需要自定義分片演算法,可以直接在配置檔案中接著寫規則。 `t_order_$->{t_order_id % 4}` 代表 `t_order` 對其欄位 `t_order_id`取模,拆分成4張表,而表名分別是`t_order_0` 到 `t_order_3`。 **4、Hint分片策略** Hint分片策略,對應上邊的Hint分片演算法,通過指定分片健而非從 `SQL`中提取分片健的方式進行分片的策略。 **分散式主鍵** 資料分⽚後,不同資料節點⽣成全域性唯⼀主鍵是⾮常棘⼿的問題,同⼀個邏輯表(`t_order`)內的不同真實表(`t_order_n`)之間的⾃增鍵由於⽆法互相感知而產⽣重複主鍵。 儘管可通過設定⾃增主鍵 `初始值` 和 `步⻓` 的⽅式避免ID碰撞,但這樣會使維護成本加大,乏完整性和可擴充套件性。如果後去需要增加分片表的數量,要逐一修改分片表的步長,運維成本非常高,所以不建議這種方式。 實現分散式主鍵⽣成器的方式很多,具體可以百度,網上有很多 為了讓上手更加簡單,ApacheShardingSphere 內建了`UUID`、`SNOWFLAKE` 兩種分散式主鍵⽣成器,預設使⽤雪花演算法(`snowflake`)⽣成64bit的⻓整型資料。不僅如此它還抽離出分散式主鍵⽣成器的介面,⽅便我們實現⾃定義的⾃增主鍵⽣成演算法。 **廣播表** 廣播表:存在於所有的分片資料來源中的表,表結構和表中的資料在每個資料庫中均完全一致。一般是為字典表或者配置表 `t_config`,某個表一旦被配置為廣播表,只要修改某個資料庫的廣播表,所有資料來源中廣播表的資料都會跟著同步。 **繫結表** 繫結表:那些分片規則一致的主表和子表。比如:`t_order` 訂單表和 `t_order_item` 訂單服務專案表,都是按 `order_id` 欄位分片,因此兩張表互為繫結表關係。 那繫結表存在的意義是啥呢? 通常在我們的業務中都會使用 `t_order` 和 `t_order_item` 等表進行多表聯合查詢,但由於分庫分表以後這些表被拆分成N多個子表。如果不配置繫結表關係,會出現笛卡爾積關聯查詢,將產生如下四條`SQL`。 ``` SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id SELECT * FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id SELECT * FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id ``` ![](https://upload-images.jianshu.io/upload_images/15462057-6f7dbae49ede71b5?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 而配置繫結表關係後再進行關聯查詢時,只要對應表分片規則一致產生的資料就會落到同一個庫中,那麼只需 `t_order_0` 和 `t_order_item_0` 表關聯即可。 ``` SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id ``` ![](https://upload-images.jianshu.io/upload_images/15462057-0f23b896642631eb?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) > **注意**:在關聯查詢時 `t_order` 它作為整個聯合查詢的主表。所有相關的路由計算都只使用主表的策略,`t_order_item` 表的分片相關的計算也會使用 `t_order` 的條件,所以要保證繫結表之間的分片鍵要完全相同。 ### **三、和JDBC的貓膩** 從名字上不難看出,`Sharding-JDBC` 和 `JDBC`有很大關係,我們知道 JDBC 是一種 `Java` 語言訪問關係型資料庫的規範,其設計初衷就是要提供一套用於各種資料庫的統一標準,不同廠家共同遵守這套標準,並提供各自的實現方案供應用程式呼叫。 ![](https://upload-images.jianshu.io/upload_images/15462057-599a5a08f8d23b0b?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 但其實對於開發人員而言,我們只關心如何呼叫 JDBC API 來訪問資料庫,只要正確使用 `DataSource`、`Connection`、`Statement` 、`ResultSet` 等 API 介面,直接操作資料庫即可。所以如果想在 JDBC 層面實現資料分片就必須對現有的 API 進行功能拓展,而 Sharding-JDBC 正是基於這種思想,重寫了 JDBC 規範並完全相容了 JDBC 規範。 ![](https://upload-images.jianshu.io/upload_images/15462057-213e18a1dae3c565?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 對原有的 `DataSource`、`Connection` 等介面擴充套件成 `ShardingDataSource`、`ShardingConnection`,而對外暴露的分片操作介面與 JDBC 規範中所提供的介面完全一致,只要你熟悉 JDBC 就可以輕鬆應用 Sharding-JDBC 來實現分庫分表。 ![](https://upload-images.jianshu.io/upload_images/15462057-7d0faa62a6cb615f?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 因此它適用於任何基於 `JDBC` 的 `ORM` 框架,如:`JPA`, `Hibernate`,`Mybatis`,`Spring JDBC Template` 或直接使用的 JDBC。完美相容任何第三方的資料庫連線池,如:`DBCP`, `C3P0`, `BoneCP`,`Druid`, `HikariCP` 等,幾乎對主流關係型資料庫都支援。 **那 `Sharding-JDBC` 又是如何拓展這些介面的呢**?想知道答案我們就的從原始碼入手了,下邊我們以 JDBC API  中的 `DataSource` 為例看看它是如何被重寫擴充套件的。 資料來源 `DataSource` 介面的核心作用就是獲取資料庫連線物件 `Connection`,我們看其內部提供了兩個獲取資料庫連線的方法 ,並且繼承了 `CommonDataSource` 和 `Wrapper` 兩個介面。 ``` public interface DataSource extends CommonDataSource, Wrapper { /** *

Attempts to establish a connection with the data source that * this {@code DataSource} object represents. * @return a connection to the data source */ Connection getConnection() throws SQLException; /** *

Attempts to establish a connection with the data source that * this {@code DataSource} object represents. * @param username the database user on whose behalf the connection is * being made * @param password the user's password */ Connection getConnection(String username, String password) throws SQLException; } ``` 其中 `CommonDataSource` 是定義資料來源的根介面這很好理解,而 `Wrapper` 介面則是拓展 JDBC 分片功能的關鍵。 由於資料庫廠商的不同,他們可能會各自提供一些超越標準 JDBC API 的擴充套件功能,但這些功能非 JDBC 標準並不能直接使用,而 `Wrapper` 介面的作用就是把一個由第三方供應商提供的、非 JDBC 標準的介面包裝成標準介面,也就是`介面卡模式`。 既然講到了介面卡模式就多囉嗦幾句,也方便後邊的理解。 > 介面卡模式個種比較常用的設計模式,它的作用是將某個類的介面轉換成客戶端期望的另一個介面,使原本因介面不匹配(或者不相容)而無法在一起工作的兩個類能夠在一起工作。比如用耳機聽音樂,我有個圓頭的耳機,可手機插孔卻是扁口的,如果我想要使用耳機聽音樂就必須藉助一個轉接頭才可以,這個轉接頭就起到了適配作用。舉個栗子:假如我們 `Target` 介面中有 `hello()` 和 `word()` 兩個方法。 ``` public interface Target { void hello(); void world(); } ``` 可由於介面版本迭代`Target` 介面的 `word()` 方法可能會被廢棄掉或不被支援,`Adaptee`  類的 `greet()`方法將代替`hello()` 方法。 ``` public class Adaptee { public void greet(){ } public void world(){ } } ``` 但此時舊版本仍然有大量 `word()` 方法被使用中,解決此事最好的辦法就是建立一個介面卡`Adapter`,這樣就適配了 `Target` 類,解決了介面升級帶來的相容性問題。 ``` public class Adapter extends Adaptee implements Target { @Override public void world() { } @Override public void hello() { super.greet(); } @Override public void greet() { } } ``` 而 `Sharding-JDBC` 提供的正是非 JDBC 標準的介面,所以它也提供了類似的實現方案,也使用到了 `Wrapper` 介面做資料分片功能的適配。除了 DataSource 之外,Connection、Statement、ResultSet 等核心物件也都繼承了這個介面。 下面我們通過 `ShardingDataSource`  類原始碼簡單看下實現過程,下圖是繼承關係流程圖。 ![](https://upload-images.jianshu.io/upload_images/15462057-fd6eee5a485dbd9c?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) `ShardingDataSource`  類它在原 `DataSource` 基礎上做了功能拓展,初始化時註冊了分片SQL路由包裝器、SQL重寫上下文和結果集處理引擎,還對資料來源型別做了校驗,因為它要同時支援多個不同型別的資料來源。到這好像也沒看出如何適配,那接著向上看 `ShardingDataSource` 的繼承類  `AbstractDataSourceAdapter` 。 ``` @Getter public class ShardingDataSource extends AbstractDataSourceAdapter { private final ShardingRuntimeContext runtimeContext; /** * 註冊路由、SQl重寫上下文、結果集處理引擎 */ static { NewInstanceServiceLoader.register(RouteDecorator.class); NewInstanceServiceLoader.register(SQLRewriteContextDecorator.class); NewInstanceServiceLoader.register(ResultProcessEngine.class); } /** * 初始化時校驗資料來源型別 並根據資料來源 map、分片規則、資料庫型別得到一個分片上下文,用來獲取資料庫連線 */ public ShardingDataSource(final