前言
Apache ShardingSphere 是一套開源的分佈式數據庫解決方案組成的生態圈,它由 JDBC、Proxy 和 Sidecar(規劃中)這 3 款既能夠獨立部署,又支持混合部署配合使用的產品組成;接下來的幾篇文章將重點分析ShardingSphere-JDBC,從數據分片,分佈式主鍵,分佈式事務,讀寫分離,彈性伸縮等幾個方面來介紹。
簡介
ShardingSphere-JDBC定位爲輕量級 Java 框架,在 Java 的 JDBC 層提供的額外服務。 它使用客戶端直連數據庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解爲增強版的 JDBC 驅動,完全兼容 JDBC 和各種 ORM 框架。整體架構圖如下(來自官網):
ShardingSphere-JDBC包含了衆多的功能模塊包括數據分片,分佈式主鍵,分佈式事務,讀寫分離,彈性伸縮等等;作爲一個數據庫中間件最核心的功能當屬數據分片了,ShardingSphere-JDBC提供了很多分庫分表的策略和算法,接下來看看具體是如何使用這些策略的;
數據分片
作爲一個開發者我們希望中間件可以幫我們屏蔽底層的細節,讓我們在面對分庫分表的場景下,可以像使用單庫單表一樣簡單;當然ShardingSphere-JDBC不會讓大家失望,引入了分片數據源、邏輯表等概念;
分片數據源和邏輯表
- 邏輯表:邏輯表是相對物理表來說的,通常做分表處理,某一張表會被分成多張表,比如訂單表被拆分成10張表,分別是t_order_0到t_order_9,而對應的邏輯表就是
t_order
,對於開發者來說只需要使用邏輯表即可; - 分片數據源:對於分庫來說,通常會有多個庫,或者說是多個數據源,所以這些數據源需要被統一管理起來,引入了分片數據源的概念,常見的
ShardingDataSource
有了以上兩個最基本的概念當然還不夠,還需要分庫分表策略算法幫助我們做路由處理;但是這兩個概念可以讓開發者有一種使用單庫單表的感覺,就像下面這樣一個簡單的實例:
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig,
new Properties());
Connection conn = dataSource.getConnection();
String sql = "select id,user_id,order_id from t_order where order_id = 103";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
ResultSet set = preparedStatement.executeQuery();
以上根據真實數據源列表,分庫分表策略生成了一個抽象數據源,可以簡單理解就是ShardingDataSource
;接下來的操作和我們使用jdbc操作正常的單庫單表沒有任何區別;
分片策略算法
ShardingSphere-JDBC在分片策略上分別引入了分片算法、分片策略兩個概念,當然在分片的過程中分片鍵也是一個核心的概念;在此可以簡單的理解分片策略 = 分片算法 + 分片鍵
;至於爲什麼要這麼設計,應該是ShardingSphere-JDBC考慮更多的靈活性,把分片算法單獨抽象出來,方便開發者擴展;
分片算法
提供了抽象分片算法類:ShardingAlgorithm
,根據類型又分爲:精確分片算法、區間分片算法、複合分片算法以及Hint分片算法;
- 精確分片算法:對應
PreciseShardingAlgorithm
類,主要用於處理=
和IN
的分片; - 區間分片算法:對應
RangeShardingAlgorithm
類,主要用於處理BETWEEN AND
,>
,<
,>=
,<=
分片; - 複合分片算法:對應
ComplexKeysShardingAlgorithm
類,用於處理使用多鍵作爲分片鍵進行分片的場景; - Hint分片算法:對應
HintShardingAlgorithm
類,用於處理使用Hint
行分片的場景;
以上所有的算法類都是接口類,具體實現交給開發者自己;
分片策略
分片策略基本和上面的分片算法對應,包括:標準分片策略、複合分片策略、Hint分片策略、內聯分片策略、不分片策略;
-
標準分片策略:對應
StandardShardingStrategy
類,提供PreciseShardingAlgorithm
和RangeShardingAlgorithm
兩個分片算法,PreciseShardingAlgorithm
是必須的,RangeShardingAlgorithm
可選的;public final class StandardShardingStrategy implements ShardingStrategy { private final String shardingColumn; private final PreciseShardingAlgorithm preciseShardingAlgorithm; private final RangeShardingAlgorithm rangeShardingAlgorithm; }
-
複合分片策略:對應
ComplexShardingStrategy
類,提供ComplexKeysShardingAlgorithm
分片算法;public final class ComplexShardingStrategy implements ShardingStrategy { @Getter private final Collection<String> shardingColumns; private final ComplexKeysShardingAlgorithm shardingAlgorithm; }
可以發現支持多個分片鍵;
-
Hint分片策略:對應
HintShardingStrategy
類,通過 Hint 指定分片值而非從 SQL 中提取分片值的方式進行分片的策略;提供HintShardingAlgorithm
分片算法;public final class HintShardingStrategy implements ShardingStrategy { @Getter private final Collection<String> shardingColumns; private final HintShardingAlgorithm shardingAlgorithm; }
-
內聯分片策略:對應
InlineShardingStrategy
類,沒有提供分片算法,路由規則通過表達式來實現; -
不分片策略:對應
NoneShardingStrategy
類,不分片策略;
分片策略配置類
在使用中我們並沒有直接使用上面的分片策略類,ShardingSphere-JDBC分別提供了對應策略的配置類包括:
StandardShardingStrategyConfiguration
ComplexShardingStrategyConfiguration
HintShardingStrategyConfiguration
InlineShardingStrategyConfiguration
NoneShardingStrategyConfiguration
實戰
有了以上相關基礎概念,接下來針對每種分片策略做一個簡單的實戰,在實戰前首先準備好庫和表;
準備
分別準備兩個庫:ds0
、ds1
;然後每個庫分別包含兩個表:t_order0
,t_order1
;
CREATE TABLE `t_order0` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL,
`order_id` bigint(20) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
準備真實數據源
我們這裏有兩個數據源,這裏都使用java代碼的方式來配置:
// 配置真實數據源
Map<String, DataSource> dataSourceMap = new HashMap<>();
// 配置第一個數據源
BasicDataSource dataSource1 = new BasicDataSource();
dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
dataSource1.setUrl("jdbc:mysql://localhost:3306/ds0");
dataSource1.setUsername("root");
dataSource1.setPassword("root");
dataSourceMap.put("ds0", dataSource1);
// 配置第二個數據源
BasicDataSource dataSource2 = new BasicDataSource();
dataSource2.setDriverClassName("com.mysql.jdbc.Driver");
dataSource2.setUrl("jdbc:mysql://localhost:3306/ds1");
dataSource2.setUsername("root");
dataSource2.setPassword("root");
dataSourceMap.put("ds1", dataSource2);
這裏配置的兩個數據源都是普通的數據源,最後會把dataSourceMap交給ShardingDataSourceFactory
管理;
表規則配置
表規則配置類TableRuleConfiguration
,包含了五個要素:邏輯表、真實數據節點、數據庫分片策略、數據表分片策略、分佈式主鍵生成策略;
TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration("t_order", "ds${0..1}.t_order${0..1}");
orderTableRuleConfig.setDatabaseShardingStrategyConfig(
new StandardShardingStrategyConfiguration("user_id", new MyPreciseSharding()));
orderTableRuleConfig.setTableShardingStrategyConfig(
new StandardShardingStrategyConfiguration("order_id", new MyPreciseSharding()));
orderTableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "id"));
-
邏輯表:這裏配置的邏輯表就是t_order,對應的物理表有t_order0,t_order1;
-
真實數據節點:這裏使用行表達式進行配置的,簡化了配置;上面的配置就相當於配置了:
db0 ├── t_order0 └── t_order1 db1 ├── t_order0 └── t_order1
-
數據庫分片策略:這裏的庫分片策略就是上面介紹的五種類型,這裏使用的
StandardShardingStrategyConfiguration
,需要指定分片鍵和分片算法,這裏使用的是精確分片算法;public class MyPreciseSharding implements PreciseShardingAlgorithm<Integer> { @Override public String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<Integer> shardingValue) { Integer index = shardingValue.getValue() % 2; for (String target : availableTargetNames) { if (target.endsWith(index + "")) { return target; } } return null; } }
這裏的shardingValue就是user_id對應的真實值,每次和2取餘;availableTargetNames可選擇就是{ds0,ds1};看餘數和哪個庫能匹配上就表示路由到哪個庫;
-
數據表分片策略:指定的**分片鍵(order_id)**和分庫策略不一致,其他都一樣;
-
分佈式主鍵生成策略:ShardingSphere-JDBC提供了多種分佈式主鍵生成策略,後面詳細介紹,這裏使用雪花算法;
配置分片規則
配置分片規則ShardingRuleConfiguration
,包括多種配置規則:表規則配置、綁定表配置、廣播表配置、默認數據源名稱、默認數據庫分片策略、默認表分片策略、默認主鍵生成策略、主從規則配置、加密規則配置;
-
表規則配置 tableRuleConfigs:也就是上面配置的庫分片策略和表分片策略,也是最常用的配置;
-
綁定表配置 bindingTableGroups:指分⽚規則⼀致的主表和⼦表;綁定表之間的多表關聯查詢不會出現笛卡爾積關聯,關聯查詢效率將⼤⼤提升;
-
廣播表配置 broadcastTables:所有的分⽚數據源中都存在的表,表結構和表中的數據在每個數據庫中均完全⼀致。適⽤於數據量不⼤且需要與海量數據的表進⾏關聯查詢的場景;
-
默認數據源名稱 defaultDataSourceName:未配置分片的表將通過默認數據源定位;
-
默認數據庫分片策略 defaultDatabaseShardingStrategyConfig:表規則配置可以設置數據庫分片策略,如果沒有配置可以在這裏面配置默認的;
-
默認表分片策略 defaultTableShardingStrategyConfig:表規則配置可以設置表分片策略,如果沒有配置可以在這裏面配置默認的;
-
默認主鍵生成策略 defaultKeyGeneratorConfig:表規則配置可以設置主鍵生成策略,如果沒有配置可以在這裏面配置默認的;內置UUID、SNOWFLAKE生成器;
-
主從規則配置 masterSlaveRuleConfigs:用來實現讀寫分離的,可配置一個主表多個從表,讀面對多個從庫可以配置負載均衡策略;
-
加密規則配置 encryptRuleConfig:提供了對某些敏感數據進行加密的功能,提供了⼀套完整、安全、透明化、低改造成本的數據加密整合解決⽅案;
數據插入
以上準備好,就可以操作數據庫了,這裏執行插入操作:
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig,
new Properties());
Connection conn = dataSource.getConnection();
String sql = "insert into t_order (user_id,order_id) values (?,?)";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
for (int i = 1; i <= 10; i++) {
preparedStatement.setInt(1, i);
preparedStatement.setInt(2, 100 + i);
preparedStatement.executeUpdate();
}
通過以上配置的真實數據源、分片規則以及屬性文件創建分片數據源ShardingDataSource
;接下來就可以像使用單庫單表一樣操作分庫分表了,sql中可以直接使用邏輯表,分片算法會根據具體的值就行路由處理;
經過路由最終:奇數入ds1.t_order1,偶數入ds0.t_order0;以上使用了最常見的精確分片算法,下面繼續看一下其他幾種分片算法;
分片算法
上面的介紹的精確分片算法中,通過PreciseShardingValue
來獲取當前分片鍵值,ShardingSphere-JDBC針對每種分片算法都提供了相應的ShardingValue
,具體包括:
- PreciseShardingValue
- RangeShardingValue
- ComplexKeysShardingValue
- HintShardingValue
區間分片算法
用在區間查詢的時候,比如下面的查詢SQL:
select * from t_order where order_id>2 and order_id<9
以上兩個區間值2、9會直接保存到RangeShardingValue
中,這裏沒有指定user_id用來做庫路由,所以會訪問兩個庫;
public class MyRangeSharding implements RangeShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
RangeShardingValue<Integer> shardingValue) {
Collection<String> result = new LinkedHashSet<>();
Range<Integer> range = shardingValue.getValueRange();
// 區間開始和結束值
int lower = range.lowerEndpoint();
int upper = range.upperEndpoint();
for (int i = lower; i <= upper; i++) {
Integer index = i % 2;
for (String target : availableTargetNames) {
if (target.endsWith(index + "")) {
result.add(target);
}
}
}
return result;
}
}
可以發現會檢查區間開始和結束中的每個值和2取餘,是否都能和真實的表匹配;
複合分片算法
可以同時使用多個分片鍵,比如可以同時使用user_id和order_id作爲分片鍵;
orderTableRuleConfig.setDatabaseShardingStrategyConfig(
new ComplexShardingStrategyConfiguration("order_id,user_id", new MyComplexKeySharding()));
orderTableRuleConfig.setTableShardingStrategyConfig(
new ComplexShardingStrategyConfiguration("order_id,user_id", new MyComplexKeySharding()));
如上在配置分庫分表策略時,指定了兩個分片鍵,用逗號隔開;分片算法如下:
public class MyComplexKeySharding implements ComplexKeysShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
ComplexKeysShardingValue<Integer> shardingValue) {
Map<String, Collection<Integer>> map = shardingValue.getColumnNameAndShardingValuesMap();
Collection<Integer> userMap = map.get("user_id");
Collection<Integer> orderMap = map.get("order_id");
List<String> result = new ArrayList<>();
// user_id,order_id分片鍵進行分表
for (Integer userId : userMap) {
for (Integer orderId : orderMap) {
int suffix = (userId+orderId) % 2;
for (String s : availableTargetNames) {
if (s.endsWith(suffix+"")) {
result.add(s);
}
}
}
}
return result;
}
}
Hint分片算法
在一些應用場景中,分片條件並不存在於 SQL,而存在於外部業務邏輯;可以通過編程的方式向 HintManager
中添加分片條件,該分片條件僅在當前線程內生效;
// 設置庫表分片策略
orderTableRuleConfig.setDatabaseShardingStrategyConfig(new HintShardingStrategyConfiguration(new MyHintSharding()));
orderTableRuleConfig.setTableShardingStrategyConfig(new HintShardingStrategyConfiguration(new MyHintSharding()));
// 手動設置分片條件
int hitKey1[] = { 2020, 2021, 2022, 2023, 2024 };
int hitKey2[] = { 3020, 3021, 3022, 3023, 3024 };
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig,
new Properties());
Connection conn = dataSource.getConnection();
for (int i = 1; i <= 5; i++) {
final int index = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
HintManager hintManager = HintManager.getInstance();
String sql = "insert into t_order (user_id,order_id) values (?,?)";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
// 分別添加庫和表分片條件
hintManager.addDatabaseShardingValue("t_order", hitKey1[index - 1]);
hintManager.addTableShardingValue("t_order", hitKey2[index - 1]);
preparedStatement.setInt(1, index);
preparedStatement.setInt(2, 100 + index);
preparedStatement.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}
}
}).start();
}
以上實例中,手動設置了分片條件,分片算法如下所示:
public class MyHintSharding implements HintShardingAlgorithm<Integer> {
@Override
public Collection<String> doSharding(Collection<String> availableTargetNames,
HintShardingValue<Integer> shardingValue) {
List<String> shardingResult = new ArrayList<>();
for (String targetName : availableTargetNames) {
String suffix = targetName.substring(targetName.length() - 1);
Collection<Integer> values = shardingValue.getValues();
for (int value : values) {
if (value % 2 == Integer.parseInt(suffix)) {
shardingResult.add(targetName);
}
}
}
return shardingResult;
}
}
不分片
配置NoneShardingStrategyConfiguration
即可:
orderTableRuleConfig.setDatabaseShardingStrategyConfig(new NoneShardingStrategyConfiguration());
orderTableRuleConfig.setTableShardingStrategyConfig(new NoneShardingStrategyConfiguration());
這樣數據會插入每個庫每張表,可以理解爲廣播表
分佈式主鍵
面對多個數據庫表需要有唯一的主鍵,引入了分佈式主鍵功能,內置的主鍵生成器包括:UUID、SNOWFLAKE;
UUID
直接使用UUID.randomUUID()生成,主鍵沒有任何規則;對應的主鍵生成類:UUIDShardingKeyGenerator
;
SNOWFLAKE
實現類:SnowflakeShardingKeyGenerator
;使⽤雪花算法⽣成的主鍵,⼆進製表⽰形式包含 4 部分,從⾼位到低位分表爲:1bit 符號位、41bit 時間戳位、10bit ⼯作進程位以及 12bit 序列號位;來自官網的圖片:
擴展
實現接口:ShardingKeyGenerator
,實現自己的主鍵生成器;
public interface ShardingKeyGenerator extends TypeBasedSPI {
Comparable<?> generateKey();
}
實戰
使用也很簡單,直接使用KeyGeneratorConfiguration
即可,配置對應的算法類型和字段名稱:
orderTableRuleConfig.setKeyGeneratorConfig(new KeyGeneratorConfiguration("SNOWFLAKE", "id"));
這裏使用雪花算法生成器,對應生成的字段是id;結果如下:
mysql> select * from t_order0;
+--------------------+---------+----------+
| id | user_id | order_id |
+--------------------+---------+----------+
| 589535589984894976 | 0 | 0 |
| 589535590504988672 | 2 | 2 |
| 589535590718898176 | 4 | 4 |
+--------------------+---------+----------+
分佈式事務
ShardingSphere-JDBC使用分佈式事務和使用本地事務沒什麼區別,提供了透明化的分佈式事務;支持的事務類型包括:本地事務、XA事務和柔性事務,默認是本地事務;
public enum TransactionType {
LOCAL, XA, BASE
}
依賴
根據具體使用XA事務還是柔性事務,需要引入不同的模塊;
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-transaction-xa-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>shardingsphere-transaction-base-seata-at</artifactId>
</dependency>
實現
ShardingSphere-JDBC提供了分佈式事務管理器ShardingTransactionManager
,實現包括:
- XAShardingTransactionManager:基於 XA 的分佈式事務管理器;
- SeataATShardingTransactionManager:基於 Seata 的分佈式事務管理器;
XA 的分佈式事務管理器具體實現包括:Atomikos、Narayana、Bitronix;默認是Atomikos;
實戰
默認的事務類型是TransactionType.LOCAL,ShardingSphere-JDBC天生面向多數據源,本地模式其實是循環提交每個數據源的事務,不能保證數據的一致性,所以需要使用分佈式事務,具體使用也很簡單:
//改變事務類型爲XA
TransactionTypeHolder.set(TransactionType.XA);
DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig,
new Properties());
Connection conn = dataSource.getConnection();
try {
//關閉自動提交
conn.setAutoCommit(false);
String sql = "insert into t_order (user_id,order_id) values (?,?)";
PreparedStatement preparedStatement = conn.prepareStatement(sql);
for (int i = 1; i <= 5; i++) {
preparedStatement.setInt(1, i - 1);
preparedStatement.setInt(2, i - 1);
preparedStatement.executeUpdate();
}
//事務提交
conn.commit();
} catch (Exception e) {
e.printStackTrace();
//事務回滾
conn.rollback();
}
可以發現使用起來還是很簡單的,ShardingSphere-JDBC會根據當前的事務類型,在提交的時候判斷是走本地事務提交,還是使用分佈式事務管理器ShardingTransactionManager
進行提交;
讀寫分離
對於同一時刻有大量併發讀操作和較少寫操作類型的應用系統來說,將數據庫拆分爲主庫和從庫,主庫負責處理事務性的增刪改操作,從庫負責處理查詢操作,能夠有效的避免由數據更新導致的行鎖,使得整個系統的查詢性能得到極大的改善。
主從配置
在上面章節介紹分片規則的時候,其中有說到主從規則配置,其目的就是用來實現讀寫分離的,核心配置類:MasterSlaveRuleConfiguration
:
public final class MasterSlaveRuleConfiguration implements RuleConfiguration {
private final String name;
private final String masterDataSourceName;
private final List<String> slaveDataSourceNames;
private final LoadBalanceStrategyConfiguration loadBalanceStrategyConfiguration;
}
- name:配置名稱,當前使用的4.1.0版本,這裏必須是主庫的名稱;
- masterDataSourceName:主庫數據源名稱;
- slaveDataSourceNames:從庫數據源列表,可以配置一主多從;
- LoadBalanceStrategyConfiguration:面對多個從庫,讀取的時候會通過負載算法進行選擇;
主從負載算法類:MasterSlaveLoadBalanceAlgorithm
,實現類包括:隨機和循環;
- ROUND_ROBIN:實現類
RoundRobinMasterSlaveLoadBalanceAlgorithm
- RANDOM:實現類
RandomMasterSlaveLoadBalanceAlgorithm
實戰
分別給ds0和ds1準備從庫:ds01和ds11,分別配置主從同步;讀寫分離配置如下:
List<String> slaveDataSourceNames0 = new ArrayList<String>();
slaveDataSourceNames0.add("ds01");
MasterSlaveRuleConfiguration masterSlaveRuleConfiguration0 = new MasterSlaveRuleConfiguration("ds0", "ds0",
slaveDataSourceNames0);
shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration0);
List<String> slaveDataSourceNames1 = new ArrayList<String>();
slaveDataSourceNames1.add("ds11");
MasterSlaveRuleConfiguration masterSlaveRuleConfiguration1 = new MasterSlaveRuleConfiguration("ds1", "ds1",
slaveDataSourceNames1);
shardingRuleConfig.getMasterSlaveRuleConfigs().add(masterSlaveRuleConfiguration1);
這樣在執行查詢操作的時候會自動路由到從庫,實現讀寫分離;
總結
本文重點介紹了ShardingSphere-JDBC的數據分片功能,這也是所有數據庫中間件的核心功能;當然分佈式主鍵、分佈式事務、讀寫分離等功能也是必不可少的;同時ShardingSphere還引入了彈性伸縮
的功能,這是一個非常亮眼的功能,因爲數據庫分片本身是有狀態的,所以我們在項目啓動之初都固定了多少庫多少表,然後通過分片算法路由到各個庫表,但是業務的發展往往超乎我們的預期,這時候如果想擴表擴庫會很麻煩,目前看ShardingSphere官網彈性伸縮
處於alpha開發階段,非常期待此功能。
參考
https://shardingsphere.apache.org/document/current/cn/overview/
感謝關注
可以關注微信公衆號「回滾吧代碼」,第一時間閱讀,文章持續更新;專注Java源碼、架構、算法和麪試。