ShardingSphere-JDBC入門實戰

語言: CN / TW / HK

前言

Apache ShardingSphere 是一套開源的分散式資料庫解決方案組成的生態圈,它由 JDBC、Proxy 和 Sidecar(規劃中)這 3 款既能夠獨立部署,又支援混合部署配合使用的產品組成;接下來的幾篇文章將重點分析ShardingSphere-JDBC,從資料分片,分散式主鍵,分散式事務,讀寫分離,彈性伸縮等幾個方面來介紹。

簡介

ShardingSphere-JDBC定位為輕量級 Java 框架,在 Java 的 JDBC 層提供的額外服務。 它使用客戶端直連資料庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解為增強版的 JDBC 驅動,完全相容 JDBC 和各種 ORM 框架。整體架構圖如下(來自官網):

shardingsphere-jdbc-brief.png

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類,提供PreciseShardingAlgorithmRangeShardingAlgorithm兩個分片演算法,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

實戰

有了以上相關基礎概念,接下來針對每種分片策略做一個簡單的實戰,在實戰前首先準備好庫和表;

準備

分別準備兩個庫:ds0ds1;然後每個庫分別包含兩個表:t_order0t_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 序列號位;來自官網的圖片:

image-20210415191555431.png

擴充套件

實現介面: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原始碼、架構、演算法和麵試。