ShardingSphere-JDBC入門實戰

前言

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

簡介

ShardingSphere-JDBC定位爲輕量級 Java 框架,在 Java 的 JDBC 層提供的額外服務。 它使用客戶端直連數據庫,以 jar 包形式提供服務,無需額外部署和依賴,可理解爲加強版的 JDBC 驅動,徹底兼容 JDBC 和各類 ORM 框架。總體架構圖以下(來自官網):mysql

shardingsphere-jdbc-brief.png

ShardingSphere-JDBC包含了衆多的功能模塊包括數據分片,分佈式主鍵,分佈式事務,讀寫分離,彈性伸縮等等;做爲一個數據庫中間件最核心的功能當屬數據分片了,ShardingSphere-JDBC提供了不少分庫分表的策略和算法,接下來看看具體是如何使用這些策略的;面試

數據分片

做爲一個開發者咱們但願中間件能夠幫咱們屏蔽底層的細節,讓咱們在面對分庫分表的場景下,能夠像使用單庫單表同樣簡單;固然ShardingSphere-JDBC不會讓你們失望,引入了分片數據源、邏輯表等概念;算法

分片數據源和邏輯表

  • 邏輯表:邏輯表是相對物理表來講的,一般作分表處理,某一張表會被分紅多張表,好比訂單表被拆分紅10張表,分別是t_order_0到t_order_9,而對應的邏輯表就是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類,用於處理使用多鍵做爲分片鍵進行分片的場景;
  • 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

以上兩個區間值二、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...

感謝關注

能夠關注微信公衆號「 回滾吧代碼」,第一時間閱讀,文章持續更新;專一Java源碼、架構、算法和麪試。
相關文章
相關標籤/搜索