Apache ShardingSphere 是一套開源的分佈式數據庫解決方案組成的生態圈,它由 JDBC、Proxy 和 Sidecar(規劃中)這 3 款既可以獨立部署,又支持混合部署配合使用的產品組成;接下來的幾篇文章將重點分析ShardingSphere-JDBC,從數據分片,分佈式主鍵,分佈式事務,讀寫分離,彈性伸縮等幾個方面來介紹。java
ShardingSphere-JDBC定位爲輕量級 Java 框架,在 Java 的 JDBC 層提供的額外服務。 它使用客戶端直連數據庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解爲加強版的 JDBC 驅動,徹底兼容 JDBC 和各類 ORM 框架。總體架構圖以下(來自官網):mysql
ShardingSphere-JDBC包含了衆多的功能模塊包括數據分片,分佈式主鍵,分佈式事務,讀寫分離,彈性伸縮等等;做爲一個數據庫中間件最核心的功能當屬數據分片了,ShardingSphere-JDBC提供了不少分庫分表的策略和算法,接下來看看具體是如何使用這些策略的;面試
做爲一個開發者咱們但願中間件能夠幫咱們屏蔽底層的細節,讓咱們在面對分庫分表的場景下,能夠像使用單庫單表同樣簡單;固然ShardingSphere-JDBC不會讓你們失望,引入了分片數據源、邏輯表等概念;算法
t_order
,對於開發者來講只須要使用邏輯表便可;ShardingDataSource
有了以上兩個最基本的概念固然還不夠,還須要分庫分表策略算法幫助咱們作路由處理;可是這兩個概念可讓開發者有一種使用單庫單表的感受,就像下面這樣一個簡單的實例:sql
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考慮更多的靈活性,把分片算法單獨抽象出來,方便開發者擴展;apache
提供了抽象分片算法類:ShardingAlgorithm
,根據類型又分爲:精確分片算法、區間分片算法、複合分片算法以及Hint分片算法;編程
PreciseShardingAlgorithm
類,主要用於處理 =
和 IN
的分片;RangeShardingAlgorithm
類,主要用於處理 BETWEEN AND
, >
, <
, >=
, <=
分片;ComplexKeysShardingAlgorithm
類,用於處理使用多鍵做爲分片鍵進行分片的場景;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"));
真實數據節點:這裏使用行表達式進行配置的,簡化了配置;上面的配置就至關於配置了:
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};看餘數和哪一個庫能匹配上就表示路由到哪一個庫;
配置分片規則ShardingRuleConfiguration
,包括多種配置規則:表規則配置、綁定表配置、廣播表配置、默認數據源名稱、默認數據庫分片策略、默認表分片策略、默認主鍵生成策略、主從規則配置、加密規則配置;
以上準備好,就能夠操做數據庫了,這裏執行插入操做:
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
,具體包括:
用在區間查詢的時候,好比下面的查詢SQL:
select * from t_order where order_id>2 and order_id<9
以上兩個區間值二、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; } }
在一些應用場景中,分片條件並不存在於 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.randomUUID()生成,主鍵沒有任何規則;對應的主鍵生成類:UUIDShardingKeyGenerator
;
實現類: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
,實現包括:
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; }
主從負載算法類:MasterSlaveLoadBalanceAlgorithm
,實現類包括:隨機和循環;
RoundRobinMasterSlaveLoadBalanceAlgorithm
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...
能夠關注微信公衆號「 回滾吧代碼」,第一時間閱讀,文章持續更新;專一Java源碼、架構、算法和麪試。