在分布式服务中,要实现数据源得选择有如下相关方案

  • DAO:继承  AbstractRoutingDataSource 类,实现对应的切换数据源的方法,结合自定义注解 + 切面实现动态数据源切换。
  • ORM:MyBatis 插件进行数据源切换
  • JDBC:Sharding-JDBC 基于客户端的分库分表方案
  • Proxy:Mycat、Sharding-Proxy 基于代理的分库分表方案
  • Server:特定数据库或者版本
  • .........

基本概念及架构:

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

  DubboX,Elastic-job 也是当当开源出来的产品。

动态数据源切换DsProcessor shardingjdbc动态切换数据源_数据源

  2018 年 5 月,因为增加了 Proxy 的版本和 Sharding-Sidecar(尚未发布),Sharding-JDBC 更名为 Sharding Sphere,从一个客户端的组件变成了一个套件。

  2018 年 11 月,Sharding-Sphere 正式进入 Apache 基金会孵化器,这也是对Sharding-Sphere 的质量和影响力的认可。

  官网 :https://shardingsphere.apache.org/index_zh.html

  一般我们用的还是 io.shardingsphere 的包。因为更名后和捐献给 Apache 之后的 groupId 都不一样,在引入依赖的时候千万要注意。主体功能是相同的,但是在某些类的用法上有些差异,如果要升级的话 import 要全部修改,有些类和方法也要修改。

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

  • 适用于任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, Spring JDBC Template或直接使用JDBC。
  • 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  • 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。

动态数据源切换DsProcessor shardingjdbc动态切换数据源_动态数据源切换DsProcessor_02

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

  我们在项目内引入 Sharding-JDBC 的依赖,我们的业务代码在操作数据库的时候,就会通过 Sharding-JDBC 的代码连接到数据库。分库分表的一些核心动作,比如 SQL 解析,路由,执行,结果处理,都是由它来完成的。它工作在客户端。

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

动态数据源切换DsProcessor shardingjdbc动态切换数据源_动态数据源切换DsProcessor_03

核心功能 :

  分库分表后的几大问题:跨库关联查询、分布式事务、排序翻页计算、全局主键。

  数据分片

  1. 分库 & 分表
  2. 读写分离:https://shardingsphere.apache.org/document/current/cn/features/read-write-split/
  3. 分片策略定制化
  4. 无中心化分布式主键(包括 UUID、雪花、LEAF)

  分布式事务

  1. 标准化事务接口
  2. XA 强一致事务
  3. 柔性事务

核心概念:

  • 逻辑表:水平拆分的数据库(表)的相同逻辑和数据结构表的总称。例:订单数据根据主键尾数拆分为 10 张表,分别是 t_order_0 到 t_order_9,他们的逻辑表名为 t_order
  • 真实表:在分片的数据库中真实存在的物理表。即上个示例中的 t_order_0 到 t_order_9
  • 数据节点:数据分片的最小单元。由数据源名称和数据表组成,例:ds_0.t_order_0
  • 绑定表:指分片规则一致的主表和子表。例如:t_order 表和 t_order_item 表,均按照 order_id 分片,则此两张表互为绑定表关系。绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
  • 广播表:指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
  • 分片键:根据指定的分片键进行路由。分片键不一定是主键,也不一定有业务含义。

动态数据源切换DsProcessor shardingjdbc动态切换数据源_bc_04

使用规范 :

  虽然 Apache ShardingSphere 希望能够完全兼容所有的SQL以及单机数据库,但分布式为数据库带来了更加复杂的场景。包括一些特殊的 sql 或者分页都带来了巨大的挑战。对于这方面sharding-jdbc也做出了相关的说明

  与 Mycat 对比 :

 

Sharding-JDBC 

Mycat

工作 层面

JDBC 协议

MySQL 协议/JDBC 协议

运行方式

Jar 包,客户端

  独立服务,服务端

开发 方式

代码/配置改动

连接地址(数据源)

运维 方式 


管理独立服务,运维成本高

性能

多线程并发按操作,性能高

独立服务+网络开销,存在性能损失风险

功能 范围

协议层面 

包括分布式事务、数据迁移等

适用 操作

OLTP 

OLTP+OLAP

支持 数据库

基于 JDBC 协议的数据库

MySQL 和其他支持 JDBC 协议的数据库

支持 语言

Java 项目中使用 

支持 JDBC 协议的语言

维度

二维,支持分库又分表,比如user表继续拆分为user1、user2

一维,分了库后表不可以继续拆分,或者单库分表

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

建议:小型项目,分片规则简单的项目可以用 Sharding-JDBC。大型项目,可以用Mycat。

Sharding-JDBC 案例 :

  Sharding-JDBC要实现分库分表的方案主要分为以下几个步骤:

  1. 配置数据源。
  2. 配置表规则 TableRuleConfiguration。
  3. 配置分库+分表策略 DatabaseShardingStrategyConfig,TableShardingStrategyConfig。
  4. 获取数据源对象。
  5. 执行数据库操作。

1.首先我们创建一个标准的springboot工程。还需要引入相关依赖:

<dependency>
  <groupId>org.apache.shardingsphere</groupId>
  <artifactId>shardingsphere-jdbc-core</artifactId>
  <version>5.0.0-alpha</version>
</dependency>
<dependency>
  <groupId>com.zaxxer</groupId>
  <artifactId>HikariCP</artifactId>
  <version>3.4.1</version>
</dependency>
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>

2.编写类 :

public class ShardingJdbc5Test {

    public static void main(String[] args) throws SQLException {
        //代表真实的数据源
        Map<String, DataSource> dataSourceMap = new HashMap<>();
        //数据源1
        HikariDataSource source = new HikariDataSource();
        source.setDriverClassName("com.mysql.cj.jdbc.Driver");
        source.setJdbcUrl(String.format("jdbc:mysql://%s:%s/%s?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8", "192.168.1.101", 3306, "study"));
        source.setUsername("root");
        source.setPassword("123456");
        //数据源2
        HikariDataSource source2 = new HikariDataSource();
        source2.setDriverClassName("com.mysql.cj.jdbc.Driver");
        source2.setJdbcUrl(String.format("jdbc:mysql://%s:%s/%s?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8", "192.168.1.101", 3306, "study2"));
        source2.setUsername("root");
        source2.setPassword("123456");

        dataSourceMap.put("ds0", source);
        dataSourceMap.put("ds1", source2);

        //创建分片规则
        // * 针对数据库
        // * 针对表
        //* 一定要配置分片键
        //* 一定要配置分片算法
        //* 完全唯一id的问题

        ShardingRuleConfiguration configuration = new ShardingRuleConfiguration();

        ShardingTableRuleConfiguration tableRuleConfiguration =
                new ShardingTableRuleConfiguration("t_order", "ds${0..1}.t_order_${0..1}");
        tableRuleConfiguration.setKeyGenerateStrategy(new KeyGenerateStrategyConfiguration("order_id", "snowflake"));

        //把逻辑表和真实表的对应关系添加到分片规则配置中
        configuration.getTables().add(tableRuleConfiguration);
        //设置数据库分库规则
        configuration.setDefaultDatabaseShardingStrategy(
                new StandardShardingStrategyConfiguration
                        ("user_id", "db-inline"));
        Properties properties = new Properties();
        properties.setProperty("algorithm-expression", "ds${user_id%2}");
        //设置分库策略
        configuration.getShardingAlgorithms().
                put("db-inline", new ShardingSphereAlgorithmConfiguration("INLINE", properties));

        //设置表的分片规则(数据的水平拆分)
        configuration.setDefaultTableShardingStrategy(new StandardShardingStrategyConfiguration
                ("order_id", "order-inline"));
        //设置分表策略
        Properties props = new Properties();
        props.setProperty("algorithm-expression", "t_order_${order_id%2}");
        configuration.getShardingAlgorithms().put("order-inline",
                new ShardingSphereAlgorithmConfiguration("INLINE", props));
        //设置主键生成策略
        // * UUID
        // * 雪花算法
        Properties idProperties = new Properties();
        idProperties.setProperty("worker-id", "123");
        configuration.getKeyGenerators().put("snowflake", new ShardingSphereAlgorithmConfiguration(
                "SNOWFLAKE", idProperties));

        //被代理的 数据源
        DataSource dataSource = ShardingSphereDataSourceFactory
                .createDataSource(dataSourceMap, Collections.singleton(configuration), new Properties());
        //初始化数据库表
        String sql = "CREATE TABLE IF NOT EXISTS t_order (order_id BIGINT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, address_id BIGINT NOT NULL, status VARCHAR(50), PRIMARY KEY (order_id))";
        try (Connection connection = dataSource.getConnection();
             Statement statement = connection.createStatement()) {
            statement.executeUpdate(sql);
        }

        System.out.println("-------------- Process Success Begin ---------------");
        Random random = new Random();
        System.out.println("---------------------------- Insert Data ----------------------------");
        List<Long> result = new ArrayList<>(10);
        for (int i = 1; i <= 10; i++) {
            Order order = new Order();
            order.setUserId(random.nextInt(10000));
            order.setAddressId(i);
            order.setStatus("INSERT_TEST");
            String insertSql = "INSERT INTO t_order (user_id, address_id, status) VALUES (?, ?, ?)";

            try (Connection connection = dataSource.getConnection();
                 PreparedStatement preparedStatement = connection.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS)) {
                preparedStatement.setInt(1, order.getUserId());
                preparedStatement.setLong(2, order.getAddressId());
                preparedStatement.setString(3, order.getStatus());
                preparedStatement.executeUpdate();
                try (ResultSet resultSet = preparedStatement.getGeneratedKeys()) {
                    if (resultSet.next()) {
                        order.setOrderId(resultSet.getLong(1));
                    }
                }
            }
        }
        System.out.println("-------------- Process Success Finish --------------");
    }
}

3.在两个库上都建立对应的 order1、order2 表,表结构一致。字段自己调整就行

  运行上述main方法可以查看到相应的效果。

  总结:ShardingRuleConfiguration 可以包含多个 ShardingTableRuleConfiguration(多张表),也可以设置默认的分库和分表策略。每个 ShardingTableRuleConfiguration 可以针对表设置 ShardingSphereAlgorithmConfiguration,包括分库分表策略。

  ShardingSphereAlgorithmConfiguration有 5 种实现(标准、自动、复合、Hint、自定义)。ShardingDataSourceFactory 利用 ShardingRuleConfiguration 创建数据源。有了数据源,就可以走 JDBC 的流程了。

  • 自动分片算法:取模分片算法(MOD)、哈希取模分片算法(HASH_MOD)、基于分片容量的范围分片算法(VOLUME_RANGE)、基于分片边界的范围分片算法(BOUNDARY_RANGE)、自动时间段分片算法(AUTO_INTERVAL)
  • 标准分片算法:行表达式分片算法(INLINE)、时间范围分片算法(INTERVAL)
  • 复合分片算法:复合行表达式分片算法(COMPLEX_INLINE)多个分片键,其他都是单一的
  • Hint 分片算法:Hint 行表达式分片算法(HINT_INLINE)
  • 自定义类分片算法(CLASS_BASED)

  更多配置可以参考 https://shardingsphere.apache.org/document/current/cn/user-manual/shardingsphere-jdbc/configuration

整合SpringBoot :

  Sharding-JDBC 进行与 SpringBoot (2.3.0)的整合是方便的,主要是进行配置文件的配置。

1.创建标准的SpringBoot 工程,再加入以下依赖:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.72</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.12</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
            <version>5.0.0-alpha</version>
        </dependency>
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>3.4.2</version>
        </dependency>
    </dependencies>

2.进行 分库分表规则配置,新建 application-sharding.properties文件 :

server.port=8080
spring.shardingsphere.datasource.names=ds-0,ds-1
spring.shardingsphere.datasource.common.type=com.zaxxer.hikari.HikariDataSource
spring.shardingsphere.datasource.common.driver-class-name=com.mysql.cj.jdbc.Driver
spring.shardingsphere.datasource.ds-0.username=root
spring.shardingsphere.datasource.ds-0.password=123456
spring.shardingsphere.datasource.ds-0.jdbc-url=jdbc:mysql://192.168.1.101:3306/study?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.shardingsphere.datasource.ds-1.username=root
spring.shardingsphere.datasource.ds-1.password=123456
spring.shardingsphere.datasource.ds-1.jdbc-url=jdbc:mysql://192.168.1.101:3306/study2?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8
# 行表达式分库分表策略
# 针对分库的规则配置
spring.shardingsphere.rules.sharding.default-database-strategy.standard.sharding-column=user_id
spring.shardingsphere.rules.sharding.default-database-strategy.standard.sharding-algorithm-name=database-inline
spring.shardingsphere.rules.sharding.tables.t_order.actual-data-nodes=ds-$->{0..1}.t_order_$->{0..1}
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-column=order_id
spring.shardingsphere.rules.sharding.tables.t_order.table-strategy.standard.sharding-algorithm-name=t-order-inline
# 单个 绑定表
#spring.shardingsphere.rules.sharding.binding-tables=t_order,t_order_detail
# 如果有多个
#spring.shardingsphere.rules.sharding.binding-tables[0]=t_order,t_order_detail
#spring.shardingsphere.rules.sharding.binding-tables[1]=t_order,t_order_detail
# 采用key-generate-strategy 的 字段
spring.shardingsphere.rules.sharding.tables.t_order.key-generate-strategy.column=order_id
spring.shardingsphere.rules.sharding.tables.t_order.key-generate-strategy.key-generator-name=snowflake
# 分库策略--行表达式分片算法
spring.shardingsphere.rules.sharding.sharding-algorithms.database-inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.database-inline.props.algorithm-expression=ds-$->{user_id % 2}
#  分表策略--行表达式分片算法
spring.shardingsphere.rules.sharding.sharding-algorithms.t-order-inline.type=INLINE
spring.shardingsphere.rules.sharding.sharding-algorithms.t-order-inline.props.algorithm-expression=t_order_$->{order_id % 2}
# 雪花算法
spring.shardingsphere.rules.sharding.key-generators.snowflake.type=SNOWFLAKE
spring.shardingsphere.rules.sharding.key-generators.snowflake.props.worker-id=123

3.其他关于 mybatis 的相关配置这里就不贴出来了。然后在数据库中创建对应的表。编写 dao、service 进行测试。关于事务、全局ID、自定义分片策略下篇博客中会详细介绍。