Sharding-jdbc

前言

课程目标

  1. 掌握 Sharding-JDBC 的使用方式
  2. 掌握分布式事务、全局 ID 等问题的解决方案
  3. 理解 Sharding-JDBC 的工作流程和实现原理
  4. 理解基于 Mycat 和 Sharding-JDBC 的差别 

全文请结合项目观看。 

一、分片核心概念

        在我们用 Sharding-JDBC 之前,有一些核心概念是必须掌握的。

 1.1 核心概念

逻辑表、真实表、分片键、数据节点、动态表、广播表、绑定表。

1.1.1 主要概念

 逻辑表会在 SQL 解析和路由时被替换成真实的表名。

1.1.2 动态表

 1.1.3 绑定表

跟 Mycat 的 ER 表对应

 1.1.4 广播表

跟 Mycat 的全局表对应

二、Sharding-JDBC 实战

快速入门:https://shardingsphere.apache.org/document/legacy/4.x/document/cn/quick-start/sharding-jdbc-quick-start/

2.1 引入依赖

核心依赖是:sharding-jdbc-core

注意组织名称(groupId)是 org.apache.shardingsphere,捐献给 Apache 之后(课程中统一用这个)。
io.shardingjdbc :更名之前(废弃)
io.shardingsphere:更名之后(废弃)
包名和某些类有差异,如果升级需要注意,import 的包名都需要修改。
Apache 的包为 Spring Boot 提供了 starter 的依赖:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-jdbc-spring-boot-starter</artifactId>
    <version>4.1.1</version>
</dependency>

2.2 API 使用

        先看 API 是怎么用的,也就是自己像 JDBC 的代码一样创建数据源,创建连接。后面再看在 Spring 里面怎么使用。

2.2.1 数据分片

https://shardingsphere.apache.org/document/legacy/4.x/document/cn/manual/shardingjdbc/usage/sharding/

 spring-boot-sharding-jdbc 工程 com.qingshan.jdbc.ShardJDBCTest:

public class ShardJDBCTest {
    public static void main(String[] args) throws SQLException {
        // 配置真实数据源
        Map<String, DataSource> dataSourceMap = new HashMap<>();

        // 配置第一个数据源
        DruidDataSource dataSource1 = new DruidDataSource();
        dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource1.setUrl("jdbc:mysql://localhost:3306/ds0");
        dataSource1.setUsername("root");
        dataSource1.setPassword("root");
        dataSourceMap.put("ds0", dataSource1);

        // 配置第二个数据源
        DruidDataSource dataSource2 = new DruidDataSource();
        dataSource2.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource2.setUrl("jdbc:mysql://localhost:3306/ds1");
        dataSource2.setUsername("root");
        dataSource2.setPassword("root");
        dataSourceMap.put("ds1", dataSource2);

        // 配置Order表规则
        TableRuleConfiguration orderTableRuleConfig = new TableRuleConfiguration("t_order", "ds${0..1}.t_order");
        // 分库策略,使用inline实现
        InlineShardingStrategyConfiguration dataBaseInlineStrategy = new InlineShardingStrategyConfiguration("order_id", "ds${order_id % 2}");
        orderTableRuleConfig.setDatabaseShardingStrategyConfig(dataBaseInlineStrategy);

        // 分表策略,使用inline实现(没有分表,为什么不分表?)
        InlineShardingStrategyConfiguration tableInlineStrategy = new InlineShardingStrategyConfiguration("order_id", "t_order");
        orderTableRuleConfig.setTableShardingStrategyConfig(tableInlineStrategy);

        // 添加表配置
        ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration();
        shardingRuleConfig.getTableRuleConfigs().add(orderTableRuleConfig);

        // 获取数据源对象
        DataSource dataSource = ShardingDataSourceFactory.createDataSource(dataSourceMap, shardingRuleConfig, new Properties());

        String sql = "SELECT * from t_order WHERE order_id =?";
        try {
            Connection conn = dataSource.getConnection();
            PreparedStatement preparedStatement = conn.prepareStatement(sql);
            preparedStatement.setLong(1, 2);
            System.out.println();
            try (ResultSet rs = preparedStatement.executeQuery()) {
                while (rs.next()) {
                    // %2结果,路由到
                    System.out.println("---------order_id:" + rs.getLong(1));
                    System.out.println("---------user_id:" + rs.getLong(2));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

总结:

        ShardingDataSourceFactory 利用 ShardingRuleConfiguration 创建数据源。
        ShardingRuleConfiguration 可以包含多个 TableRuleConfiguration(多张表),每张表都可以通过 ShardingStrategyConfiguration 设置自己的分库和分表策略。有了数据源,就可以走 JDBC 的流程了。

2.2.2 读写分离

https://shardingsphere.apache.org/document/legacy/4.x/document/cn/manual/shardingjdbc/usage/read-write-splitting/

 com.qingshan.jdbc.MasterSlaveTest::

public class MasterSlaveTest {
    public static void main(String[] args) throws SQLException {
        // 配置真实数据源
        Map<String, DataSource> dataSourceMap = new HashMap<>();

        // 配置第一个数据源
        DruidDataSource dataSource1 = new DruidDataSource();
        dataSource1.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource1.setUrl("jdbc:mysql://192.168.44.121:3306/ds0");
        dataSource1.setUsername("root");
        dataSource1.setPassword("123456");
        dataSourceMap.put("master0", dataSource1);

        // 配置第二个数据源
        DruidDataSource dataSource2 = new DruidDataSource();
        dataSource2.setDriverClassName("com.mysql.jdbc.Driver");
        dataSource2.setUrl("jdbc:mysql://192.168.44.128:3306/ds0");
        dataSource2.setUsername("root");
        dataSource2.setPassword("123456");
        dataSourceMap.put("slave0", dataSource2);

        // 配置读写分离规则
        MasterSlaveRuleConfiguration masterSlaveRuleConfig = new MasterSlaveRuleConfiguration("qs_master_slave", "master0", Arrays.asList("slave0"));

        // 获取数据源对象
        DataSource dataSource = MasterSlaveDataSourceFactory.createDataSource(dataSourceMap, masterSlaveRuleConfig, new Properties());
        Connection conn = dataSource.getConnection();

        String selectSql = "SELECT * from t_order WHERE order_id =?";
        try {
            PreparedStatement preparedStatement = conn.prepareStatement(selectSql);
            // 直接在 slave 128 ds0 插入主节点没有的数据: insert into t_order(order_id, user_id) value(26732,26732)
            preparedStatement.setLong(1, 26732);
            System.out.println();
            try (ResultSet rs = preparedStatement.executeQuery()) {
                while (rs.next()) {
                    System.out.println("---------order_id:" + rs.getLong(1));
                    System.out.println("---------user_id:" + rs.getLong(2));
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

总结一下:

在 JDBC API 中使用,我们可以直接创建数据源。

如果在 Spring 中使用,我们自定义的数据源怎么定义使用呢?因为数据源是容器管理的,所以需要通过注解或者 xml 配置文件注入。

2.3 Spring 中使用

        先来总结一下,第一个,使用的数据源需要用 Sharding-JDBC 的数据源。而不是容器或者 ORM 框架定义的。这样才能保证动态选择数据源的实现。

        当然,流程是先由Sharding-JDBC 定义,再交给 Druid 放进池子里,再交给 MyBatis,最后再注入到 Spring。最外层是 Spring,因为代码是从 Spring 开始调用的。

        第二个,因为 Sharding-JDBC 是工作在客户端的,所以我们要在客户端配置分库分表的策略。跟 Mycat 不一样的是,Sharding-JDBC 没有内置各种分片策略和算法,需要我们通过表达式或者自定义的配置文件实现。

总体上,需要配置的就是这两个,数据源和分片策略。

配置的方式是多种多样的,在官网也有详细的介绍,大家可以根据项目的实际情况进行选择。

https://shardingsphere.apache.org/document/legacy/4.x/document/cn/manual/shardingjdbc/configuration/

位置:4.用户手册——4.1 Sharding-JDBC——4.1.2 配置手册

 2.3.1 Java 配置

        为了不跟项目定义的数据源冲突,单独创建了一个工程。

        gupao-shard-java 工程(自定义算法)

        第一种是把数据源和分片策略都写在 Java Config 中,加上注解。它的特点是非常灵
活,我们可以实现各种定义的分片策略。但是缺点是,如果把数据源、策略都配置在 Java
Config 中,就出现了硬编码,在修改的时候比较麻烦。

        单元测试类:com.gupaoedu.UserShardingTest

2.3.2 Spring Boot 配置

gupao-shard-prop 工程(具体分片策略)

第二种是直接使用 Spring Boot 的 application.properties 来配置,这个要基于starter 模块。

把数据源和分库分表策略都配置在 properties 文件中。这种方式配置比较简单,但是不能实现复杂的分片策略,不够灵活。

例子后面再跑。

2.3.3 yml 配置

spring-boot-sharding-jbbc 工程(读写分离)

        第三种是使用 Spring Boot 的 yml 配置(shardingjdbc.yml),也要依赖 starter模块。当然我们也可以结合不同的配置方式,比如把分片策略放在 JavaConfig 中,数据源配置在 yml 中或 properties 中。

        这里面配置了读写分离,确认一下查询是否发生在 slave 上。

stop slave;
SET GLOBAL SQL_SLAVE_SKIP_COUNTER=1;
start slave;
show slave status\G

2.4 Sharding-JDBC 分片案例验证

工程:gupao-shard-prop,这里我们用 application+java config 结合的方式。

我们这里验证的是切分到本地的两个库 ds0,ds1。

注意:之前 Mycat 的课程演示,不同的数据节点都在不同的机器上,这里我们以同一数据库服务中不同的 database 来替代。无论是多个 IP 的多个库还是一个 IP 的多个库,对于验证来说没有区别。

两个库里面都是相同的 4 张表:user_info,t_order,t_order_item,t_config。

这些表必须提前创建,中间件是不会帮我们生成表结构的。

然后我们用 MyBatis 的 Generator 生成相应的实体类、Mapper 接口和映射器。

对数据库的基本的 SSM 的操作完成了,接下来就是分库分表的配置,一个是数据源,一个是分片策略。

使用以下配置打印路由信息:

spring.shardingsphere.props.sql.show=true

我们先来看一下我们的数据源的配置:

spring.shardingsphere.datasource=

大家还记得我们直接配置 MySQL 数据源是怎么写的么:

spring.datasource.url=jdbc:mysql://192.168.44.168:8066/gupao
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

        当 我 们 使 用 了 Sharding-JDBC 的 数 据 源 以 后 , 对 于 数 据 的 操 作 会 交 给Sharding-JDBC 的代码来处理。

        先来给大家普及一下,分片策略从维度上分成两种,一种是分库,一种是分表。
        我们可以定义默认的分库分表策略(配置中注释了),例如:用 user_id 作为分片键。这里用到了一种分片策略的实现,叫做行内表达式。我们对 user_id 取模,然后选择数据库。如果模 2 等于 0,在第一个数据库中。模 2 等于 1,在第二个数据库中。

数据源名称是行内表达式组装出来的:

spring.shardingsphere.sharding.tables.user_info.actual-data-nodes=ds$->{0..1}.user_info

        对于不同的表,也可以单独配置分库策略(databaseStrategy)和分表策略(tableStrategy)。案例中只有分库没有分表,所以没定义 tableStrategy。

2.4.1 取模分片

首先我们来验证一下 user_info 表的取模分片(modulo ['mɔdjuləu])。

我们根据 user_id,把用户数据划分到两个数据节点上。

在本地创建两个数据库 ds0 和 ds1,都 user_info 创建表:

CREATE TABLE `user_info` (
`user_id` bigint(19) NOT NULL,
`user_name` varchar(45) DEFAULT NULL,
`account` varchar(45) NOT NULL,
`password` varchar(45) DEFAULT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这里只定义了分库策略,没有定义单库内的分表策略,两个库都是相同的表名:
路由的结果:ds0.user_info,ds1.user_info。

如果定义了分库策略,两个库里面都有两张表,那么路由的结果可能是 4 种
ds0.user_info0,ds0.user_info1;
ds1. user_info0, ds1. user_info1

spring.shardingsphere.sharding.tables.user_info.actual-data-nodes=ds$->{0..1}.user_info
spring.shardingsphere.sharding.tables.user_info.databaseStrategy.inline.shardingColumn=user_id
spring.shardingsphere.sharding.tables.user_info.databaseStrategy.inline.algorithm-expression=ds${user_id %2}

首先两个数据库的 user_info 表里面没有任何数据。

        在单元测量测试类 UserShardingTest 里面,执行 insert(),调用 Mapper 接口循环插入 100 条数据。

        我们看一下插入的结果。user_id 为偶数的数据,都落到了第一个库。user_id 为奇数的数据,都落到了第二个库。

        执行 select()测一下查询,看看数据分布在两个节点的时候,我们用程序查询,能不能取回正确的数据。

2.4.2 绑定表

        第二种是绑定表,也就是父表和子表有关联关系。主表和子表使用相同的分片策略。

CREATE TABLE `t_order` (
`order_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `t_order_item` (
`item_id` int(11) NOT NULL,
`order_id` int(11) NOT NULL,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`item_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

绑定表将使用主表的分片策略:

# 分库算法 t_order 多库分表
spring.shardingsphere.sharding.tables.t_order.databaseStrategy.inline.shardingColumn=order_id
spring.shardingsphere.sharding.tables.t_order.databaseStrategy.inline.algorithm-expression=ds${order_id % 2}
spring.shardingsphere.sharding.tables.t_order.actual-data-nodes=ds$->{0..1}.t_order
​
# 分库算法 t_order_item 多库分表
spring.shardingsphere.sharding.tables.t_order_item.databaseStrategy.inline.shardingColumn=order_id
spring.shardingsphere.sharding.tables.t_order_item.databaseStrategy.inline.algorithm-expression=ds${order_id
% 2}
spring.shardingsphere.sharding.tables.t_order_item.actual-data-nodes=ds$->{0..1}.t_order_item
​
# 绑定表规则列表, 防止关联查询出现笛卡尔积
spring.shardingsphere.sharding.binding-tables[0]=t_order,t_order_item

除了定义分库和分表算法之外,我们还需要多定义一个 binding-tables。

绑定表不使用分片键查询时,会出现笛卡尔积。
——什么叫笛卡尔积?假如有 2 个数据库,两张表要相互关联,两张表又各有分表,那么 SQL 的执行路径就是 2*2*2=8 种。

不适用分片键:

 使用分片键:

(mycat 不支持这种二维的路由,要么是分库,要么是分表) 

我们去看一下测试的代码 OrderShardingTest 和 OrderItemShardingTest:

  1. 先插入主表的数据,再插入子表的数据。
  2. 先看插入。再看查询。

2.4.3 广播表

最后一种是广播表,也就是需要在所有节点上同步操作的数据表。

spring.shardingsphere.sharding.broadcast-tables=t_config

我们用 broadcast-tables 来定义。

ConfigShardingTest.java

广播表特点:
插入和更新都会在所有节点上执行,查询呢?随机负载。

2.4.4 读写分离

参考 spring-boot-sharding-jdbc
在 com.qingshan.jdbc.MasterSlaveTest 里面已经验证过了。

master-slave-rules:
    master0:
        master-data-source-name: master0
        slave-data-source-names: slave0
    master1:
        master-data-source-name: master1
        slave-data-source-names: slave1

        OK,这个就是 Sharding-JDBC 里面几种主要的表类型的分片验证。

        如果我们需要更加复杂的分片策略,properties 文件中行内表达式的这种方式肯定满足不了实际上 properties 里面的分片策略可以指定,比如 user_info 表的分库和分表策略。

pring.shardingsphere.sharding.tables.user_info.tableStrategy.standard.shardingColumn=
spring.shardingsphere.sharding.tables.user_info.tableStrategy.standard.preciseAlgorithmClassName=
spring.shardingsphere.sharding.tables.user_info.tableStrategy.standard.rangeAlgorithmClassName=

      这个时候我们需要了解 Sharding-JDBC 中几种不同的分片策略。

三、分片策略详解

https://shardingsphere.apache.org/document/current/cn/features/sharding/concept/sharding/

工程:gupao-shard-java config

        Sharding-JDBC 中的分片策略有两个维度:分库(数据源分片)策略和分表策略。

        分库策略表示数据路由到的物理目标数据源,分表分片策略表示数据被路由到的目标表。分表策略是依赖于分库策略的,也就是说要先分库再分表,当然也可以不分库只分表。

        跟 Mycat 不一样,Sharding-JDBC 没有提供内置的分片算法,而是通过抽象成接口,让开发者自行实现,这样可以根据业务实际情况灵活地实现分片。

3.1 分片策略

        包含分片键和分片算法,分片算法是需要自定义的。可以用于分库,也可以用于分表。

        Sharding-JDBC 提供了 5 种分片策略(接口),策略全部继承自 ShardingStrategy,可以根据情况选择实现相应的接口:

3.1.1 行表达式分片策略

https://shardingsphere.apache.org/document/current/cn/features/sharding/concept/inline-expression/

        对应 InlineShardingStrategy 类。只支持单分片键,提供对=和 IN 操作的支持。行内表达式的配置比较简单:

例如:
${begin..end}表示范围区间
${[unit1, unit2, unit_x]}表示枚举值
t_user_$->{u_id % 8} 表示 t_user 表根据 u_id 模 8,而分成 8 张表,表名称为
t_user_0 到 t_user_7。

行表达式中如果出现连续多个${ expression }或$->{ expression }表达式,整个表达式最终的结果将会根据每个子表达式的结果进行笛卡尔组合:

例如,以下行表达式:
${['db1', 'db2']}_table${1..3}
最终会解析为:
db1_table1, db1_table2, db1_table3,
db2_table1, db2_table2, db2_table3

3.1.2 标准分片策略

        对应 StandardShardingStrategy 类。

        标 准 分 片 策 略 只 支 持 单 分 片 键 , 提 供 了 提 供 PreciseShardingAlgorithm 和RangeShardingAlgorithm 两个分片算法,分别对应于 SQL 语句中的=, IN 和 BETWEEN AND。

        如果要使用标准分片策略,必须要实现 PreciseShardingAlgorithm,用来处理=和IN 的分片。RangeShardingAlgorithm 是可选的。如果没有实现,SQL 语句会发到所有的数据节点上执行。

3.1.3 复合分片策略

        比如:根据日期和 ID 两个字段分片,每个月 3 张表,先根据日期,再根据 ID 取模。

        对应 ComplexShardingStrategy 类。可以支持等值查询和范围查询。

        复合分片策略支持多分片键,提供了 ComplexKeysShardingAlgorithm,分片算法需要自己实现。

3.1.4 Hint 分片策略

https://shardingsphere.apache.org/document/current/cn/usermanual/shardingsphere-jdbc/usage/sharding/hint/

        对应 HintShardingStrategy。通过 Hint 而非 SQL 解析的方式分片的策略。有点类似于 Mycat 的指定分片注解。

3.1.5 不分片策略

对应 NoneShardingStrategy。不分片的策略(只在一个节点存储)。

3.2 分片算法

        创建了分片策略之后,需要进一步实现分片算法,作为参数传递给分片策略。

        Sharding-JDBC 目前提供 4 种分片算法。

3.2.1 精确分片算法

        对应 PreciseShardingAlgorithm,用于处理使用单一键作为分片键的=与 IN 进行分片的场景。需要配合 StandardShardingStrategy 使用。

3.2.2 范围分片算法

         对应 RangeShardingAlgorithm,用于处理使用单一键作为分片键的 BETWEEN AND 进行分片的场景。需要配合 StandardShardingStrategy 使用。

        如果不配置范围分片算法,范围查询默认会路由到所有节点。

3.2.3 复合分片算法

         对应 ComplexKeysShardingAlgorithm,用于处理使用多键作为分片键进行分片的场景,包含多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。需要配合 ComplexShardingStrategy 使用。

3.2.4 Hint 分片算法

      对应 HintShardingAlgorithm,用于处理使用 Hint 行分片的场景。需要配合
HintShardingStrategy 使用。

3.2.5 自定义算法实现

gupao-shard-java

所有的算法都需要实现对应的接口,实现 doSharding()方法:
例如:

  • PreciseShardingAlgorithm

——传入分片键,返回一个精确的分片(数据源名称)

String doSharding(Collection<String> availableTargetNames, PreciseShardingValue<T> shardingValue);

PreciseShardingAlgorithm策略自定义接口代码实现:

1.自定义实现PreciseShardingAlgorithm接口策略:

/**
 * 数据库分库的策略,根据分片键,返回数据库名称
 */
public class DBShardAlgo implements PreciseShardingAlgorithm<Long> {
    @Override
    public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
        String db_name="ds";
        Long num = preciseShardingValue.getValue()%2;
        db_name = db_name + num;
        for (String each : collection) {
            if (each.equals(db_name)) {
                return each;
            }
        }
        throw new IllegalArgumentException();
    }

}

2.引用:

 

  •  RangeShardingAlgorithm

——传入分片键,返回多个数据源名称。

Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<T>
shardingValue);

 RangeShardingAlgorithm策略自定义接口代码实现:

1.自定义实现PreciseShardingAlgorithm接口策略:

public class TblRangeShardAlgo implements RangeShardingAlgorithm<Long> {
    @Override
    public Collection<String> doSharding(Collection<String> availableTargetNames, RangeShardingValue<Long> rangeShardingValue) {
        System.out.println("范围-*-*-*-*-*-*-*-*-*-*-*---------------"+availableTargetNames);
        System.out.println("范围-*-*-*-*-*-*-*-*-*-*-*---------------"+rangeShardingValue);
        Collection<String> collect = new LinkedHashSet<>();
        Range<Long> valueRange = rangeShardingValue.getValueRange();
        for (Long i = valueRange.lowerEndpoint(); i <= valueRange.upperEndpoint(); i++) {
            for (String each : availableTargetNames) {
                if (each.endsWith(i % availableTargetNames.size() + "")) {
                    collect.add(each);
                }
            }
        }
        //
        return collect;
    }

}

2.引用

略。 

  •  ComplexKeysShardingAlgorithm

——传入多个分片键,返回多个数据源名称

Collection<String> doSharding(Collection<String> availableTargetNames, Collection<ShardingValue>
shardingValues);

四、Sharding-JDBC 介绍

https://github.com/apache/shardingsphere
https://shardingsphere.apache.org/document/legacy/4.x/document/cn/overview/

 4.1 回顾

数据源选择的解决方案层次:

  • DAO:AbstractRoutingDataSource
  • ORM:MyBatis 插件
  • JDBC:Sharding-JDBC
  • Proxy:Mycat、Sharding-Proxy
  • Server:特定数据库或者版本

 4.2 发展历史

        它是从当当网的内部架构 ddframe 里面的一个分库分表的模块脱胎出来的,用来解
决当当的分库分表的问题,把跟业务相关的敏感的代码剥离后,就得到了 ShardingJDBC。它是一个工作在客户端的分库分表的解决方案。
DubboX,Elastic-job 也是当当开源出来的产品。。。

4.3 基本特性

        定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以jar 包形式提供服务,无需额外部署和依赖,可理解为增强版的 JDBC 驱动,完全兼容 JDBC 和各种 ORM 框架。

        也就是说,在 maven 的工程里面,我们使用它的方式是引入依赖,然后进行配置就可以了,不用像 Mycat 一样独立运行一个服务,客户端不需要修改任何一行代码,原来是 SSM 连接数据库,还是 SSM,因为它是支持 MyBatis 的。

        跟 mycat 一样,因为数据源有多个,所以要配置数据源,而且分片规则是定义在客户端的。

       第二个,我们来看一下 Sharding- JDBC 的架构。

4.4 架构

        我们在项目内引入 Sharding-JDBC 的依赖,我们的业务代码在操作数据库的时候,就会通过 Sharding-JDBC 的代码连接到数据库。

        也就是分库分表的一些核心动作,比如 SQL 解析,路由,执行,结果处理,都是由
它来完成的。它工作在客户端。

        当然,在 Sharding-Sphere 里面同样提供了代理 Proxy 的版本,跟 Mycat 的作用是一样的。Sharding-Sidecar 是一个 Kubernetes 的云原生数据库代理,正在开发中。

4.5 功能

回顾,分库分表后的几大问题,Sharding-JDBC 全部解决了:
跨库关联查询(ER 表)排序翻页计算分布式事务全局主键

4.5.1 全局 ID

https://shardingsphere.apache.org/document/current/cn/features/sharding/concept/key-generator/

无中心化分布式主键(包括 UUID 雪花 SNOWFLAKE)

使用 key-generator-column-name 配置,生成了一个 18 位的 ID。

Properties 配置:

spring.shardingsphere.sharding.tables.user_info.key-generator.column=user_id
spring.shardingsphere.sharding.tables.user_info.key-generator.type=SNOWFLAKE
  • keyGeneratorColumnName:指定需要生成 ID 的列
  • KeyGenerotorClass:指定生成器类,默认是 DefaultKeyGenerator.java,里面使用了雪花算法。

注意:ID 要用 BIGINT。Mapper.xml insert 语句里面不能出现主键。否则会报错:
Sharding value must implements Comparable 

五、分布式事务

(这个需要结合微服务架构:Spring Cloud Alibaba 分布式事务学习,后续再深入研究Spring Cloud Alibaba )

        我们用到分布式事务的场景有两种,一种是跨应用(比如微服务场景),一种是单应用多个数据库(分库分表的场景),对于代码层的使用来说的一样的。

5.1 事务概述

https://shardingsphere.apache.org/document/current/cn/features/transaction/

  • XA 模型的不足:需要锁定资源
  • SEATA:支持 AT、XA、TCC、SAGA
  • SEATA 是一种全局事务的框架。

5.2 两阶段事务-XA

XA 的依赖:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-transaction-xa-core</artifactId>
    <version>4.1.1</version>
</dependency>

工程:spring-boot-sharding-jdbc,单元测试类
com.gupaoedu.TransactionTest
在 Service 类上加上注解

@ShardingTransactionType(TransactionType.XA)
@Transactional(rollbackFor = Exception.class)

        默认是用 atomikos 实现的。
        其他事务类型:Local、BASE
        模拟在两个节点上操作,id=2673、id=2674 路由到两个节点,第二个节点插入两个相同对象,发生主键冲突异常,会发生回滚。

        XA 实现类:
XAShardingTransactionManager——XATransactionManager——AtomikosTransactionManager

5.3 柔性事务 Seata(尚不用了解)

https://shardingsphere.apache.org/document/legacy/4.x/document/cn/manual/sharding-jdbc/usage/transaction/
https://seata.io/zh-cn/docs/overview/what-is-seata.html
https://github.com/seata/seata
https://github.com/seata/seata-workshop 

         GTS 的社区版本叫 Fescar(Fast & Easy Commit And Rollback),Fescar 改名后叫 Seata AT(Simple Extensible Autonomous Transaction Architecture)。

  1. TM(事务管理器)向 TC(事务协调者)申请开启一个全局事务,全局事务创建成功并生成一个全局唯一的 XID
  2. XID 在微服务调用链路的上下文中传播
  3. RM(资源管理器)向 TC 注册分支事务,将其纳入 XID 对应全局事务的管辖
  4. TM 向 TC 发起针对 XID 的全局提交或回滚决议
  5. TC 调度 XID 下管辖的全部分支事务完成提交或回滚请求

1.需要额外部署 Seata-server 服务进行分支事务的协调。

2.使用@GlobalTransaction 注解。引入依赖:

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>sharding-transaction-base-seata-at</artifactId>
    <version>4.1.1</version>
</dependency>

3.seata 官方也提供了样例代码

https://github.com/seata/seata-samples

六、Sharding-JDBC 工作流程

https://shardingsphere.apache.org/document/current/cn/features/sharding/principle/

Sharding-JDBC 的原理总结起来很简单:

SQL 解析 => 执行器优化 => SQL 路由 => SQL 改写 => SQL 执行 => 结果归并。

 6.1 SQL 解析

        SQL 解析主要是词法和语法的解析。目前常见的 SQL 解析器主要有 fdb,jsqlparser和 Druid。Sharding-JDBC1.4.x 之前的版本使用 Druid 作为 SQL 解析器。从 1.5.x 版本开始,Sharding-JDBC 采用完全自研的 SQL 解析引擎。

6.2 SQL 路由

        SQL 路由是根据分片规则配置以及解析上下文中的分片条件,将 SQL 定位至真正的数据源。它又分为直接路由、简单路由和笛卡尔积路由。

        直接路由,使用 Hint 方式。

        Binding 表是指使用同样的分片键和分片规则的一组表,也就是说任何情况下,Binding 表的分片结果应与主表一致。例如:order 表和 order_item 表,都根据 order_id分片,结果应是 order_1 与 order_item_1 成对出现。这样的关联查询和单表查询复杂度和性能相当。如果分片条件不是等于,而是 BETWEEN 或 IN,则路由结果不一定落入单库(表),因此一条逻辑 SQL 最终可能拆分为多条 SQL 语句。

        笛卡尔积查询最为复杂,因为无法根据 Binding 关系定位分片规则的一致性,所以非 Binding 表的关联查询需要拆解为笛卡尔积组合执行。查询性能较低,而且数据库连接数较高,需谨慎使用。

6.3 SQL 改写

例如:将逻辑表名称改成真实表名称,优化分页查询等。

6.4 SQL 执行

因为可能链接到多个真实数据源, Sharding -JDBC 将采用多线程并发执行 SQL。

6.5 结果归并

例如数据的组装、分页、排序等等。

七、Sharding-JDBC 实现原理

        ShardingJDBC 在执行过程中,主要是三个环节:

  1. 一个是解析配置文件。
  2. 第二个是对 SQL 进行解析、路由和改写。
  3. 第三个把结果集汇总到一起返回给客户端。

        在这个里面我们最主关心的是第二步,路由。也就是说,怎么根据一个 SQL 语句,和配置好的分片规则,找到对应的数据库节点呢?

7.1 四大核心对象

        我们说 Sharding-JDBC 是一个增强版的 JDBC 驱动。那么,JDBC 的四大核心对象,或者说最重要的 4 个接口是什么?

  1. DataSource
  2. Connection
  3. Statement(PS)
  4. ResulstSet

Sharding-JDBC 实现了这四个核心接口,在类名前面加上了 Sharding:

  1. ShardingDataSource
  2. ShardingConnection
  3.  ShardingStatement ( PS )
  4. ShardingResulstSet

        如果要在 Java 代码操作数据库的过程里面,实现各种各样的逻辑,肯定是要从数据源就开始替换成自己的实现。当然,因为在配置文件里面配置了数据源,启动的时候就创建好了。

        问 题 就 在 于 , 我 们 是 什 么 时 候 用 ShardingDataSource 获 取 一 个ShardingConnection 的?

7.2 MyBatis 数据源获取

        Java API(com.qingshan.jdbc.ShardJDBCTest)我们就不说了。

        我们以整合了 MyBatis 的项目为例。MyBatis 封装了 JDBC 的核心对象,那么在MyBatis 操作 JDBC 四大对象的时候,就要替换成 Sharding-JDBC 的四大对象。

        没有看过 MyBatis 源码的同学一定要去看看。我们的查询方法最终会走到SimpleExecutor 的 doQuery()方法,这个是我们的前提知识。

        spring-boot-sharding-jdbc:com.qingshan.ShardTableTest

        doQuery() 方 法 里 面 调 用 了 prepareStatement() 创 建 连 接 , 通 过ShardingDataSource 返回了一个连接(衔接上了)。

        我们直接在 prepareStatement ()打断点:

private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
}

它经过以下两个方法,返回了一个 ShardingConnection:

DataSourceUtil.fetchConnection()
Connection con = dataSource.getConnection();

        基于这个 ShardingConnection,最终得到一个 ShardingPreparedStatement:

stmt = handler.prepare(connection, transaction.getTimeout());

        接下来就是执行:

return handler.query(stmt, resultHandler);

再调用了的 ShardingPreparedStatement 的 execute():

public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
}

最终调用的是 ShardingPreparedStatement 的 execute 方法:

public boolean execute() throws SQLException {
    try {
        clearPrevious();
        prepare();
        initPreparedStatementExecutor();
        return preparedStatementExecutor.execute();
    } finally {
        refreshTableMetaData(connection.getShardingContext(), routeResult.getSqlStatement());
        clearBatch();
    }
}

prepare 方法中,prepareEngine.prepare:

RouteContext routeContext = this.executeRoute(sql, clonedParameters);

执行路由:

private RouteContext executeRoute(String sql, List<Object> clonedParameters) {
    this.registerRouteDecorator();
    return this.route(this.router, sql, clonedParameters);
}

最后到相应的路由执行引擎,比如:ShardingStandardRoutingEngine。
SQL 的解析路由就是在这一步完成的。

八、Sharding-Proxy 介绍

下载地址:

https://shardingsphere.apache.org/document/current/cn/overview/#shardingsphere-proxy

        bin 目录就是存放启停脚本的,Linux 运行 start.sh 启动(windows 用 start.bat),默认端口 3307;

        conf 目录就是存放所有配置文件,包括 sharding-proxy 服务的配置文件、数据源以及sharding 规则配置文件和项目日志配置文件;

        lib 目录就是 sharding-proxy 核心代码,以及依赖的 JAR 包。

        需要的自定义分表算法,只需要将它编译成 class 文件,然后放到 conf 目录下,也可以打成 jar 包放在 lib 目录下。

还有一个管理界面:

https://github.com/apache/shardingsphere/releases

九、与 Mycat 对比

Sharding-JdbcMycat
工作层面JDBC 协议MySQL 协议/JDBC 协议
运行方式Jar 包, 客户端独立服务, 服务端
开发方式代码/配置改动连接地址( 数据源)
运维方式管理独立服务, 运维成本高
性能多线程并发按操作, 性能高独立服务+网络开销, 存在性能损失风险
功能范围协议层面包括分布式事务、 数据迁移等
适用操作OLTPOLTP+OLAP
支持数据库基于 JDBC 协议的数据库MySQL 和其他支持 JDBC 协议的数据库
支持语言Java 项目中使用支持 JDBC 协议的语言

        从易用性和功能完善的角度来说,Mycat 似乎比 Sharding-JDBC 要好,因为有现成的分片规则,也提供了 4 种 ID 生成方式,通过注解可以支持高级功能,比如跨库关联查询。

        建议:小型项目可以用 Sharding-JDBC。大型项目,可以用 Mycat。
 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值