Sharding-JDBC和Springboot整合分库分表


目录

  1. ShardingSphere

  2. 数据分片的3种解决方案

    2.1 客户端分片

    2.2 代理服务器分片

    2.3 分布式数据库

  3. Springboot整合ShardingJDBC

参考

ShardingSphere

概述

当数据库引入分库分表和读写分离,系统的数据存储架构就会变得复杂。与拆分前的单库单表相比,我们面临着以下问题。

● 如何对多数据库进行高效治理

● 如何进行跨节点关联查询

● 如何实现跨节点的分页和排序操作

● 如何生成全局唯一的主键

● 如何确保事务一致性

● 如何对数据进行迁移等

如果没有很好的工具来支持数据存储和访问,那么数据一致性将很难得到保障,这就需要引出实现分库分表的主流解决方案和代表性框架。

如果将分库分表抽象为一个核心概念,这个概念就是分片(Sharding)。即无论是分库还是分表,都是把数据划分成不同的数据片,并存储在不同的目标对象中。而具体的分片方式就会涉及实现分库分表的不同解决方案。

ShardingSphere是一种分布式数据库中间件。它除了提供标准化的数据分片解决方案,也实现了分布式事务和数据库治理功能。

功能列表

数据分片 分布式事务 数据库治理
分库&分表 标准化事务接口 配置动态化
读写分离 XA强一致事务 数据脱敏
分片策略定制化 柔性事务 可视化链路追踪
无中心化分布式主键

接口概览

ShardingSphere中的表结构

  1. 真实表
    数据库中真实存在的物理表。例如b_order0、b_order1

  2. 逻辑表
    相同结构的水平拆分数据库(表)的逻辑名称,是 SQL 中表的逻辑标识。 例:订单数据根据主键尾数拆分为 10 张表,分别是 t_order_0 到 t_order_9,他们的逻辑表名为 t_order。

  3. 数据节点
    数据分片的最小单元,由数据源名称和真实表组成。 例:ds_0.t_order_0。 逻辑表与真实表的映射关系,可分为均匀分布和自定义分布两种形式。

  4. 绑定表
    指分片规则一致的一组分片表。 使用绑定表进行多表关联查询时,必须使用分片键进行关联,否则会出现笛卡尔积关联或跨库关联,从而影响查询效率。 例如:t_order 表和 t_order_item 表,均按照 order_id 分片,并且使用 order_id 进行关联,则此两张表互为绑定表关系。 绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。

    举例说明,如果 SQL 为:
    SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
    在不配置绑定表关系时,假设分片键 order_id 将数值 10 路由至第 0 片,将数值 11 路由至第 1 片,那么路由后的 SQL 应该为 4 条,它们呈现为笛卡尔积:

    SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
    SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
    SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
    SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

    在配置绑定表关系,并且使用 order_id 进行关联后,路由的 SQL 应该为 2 条:

    SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
    SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

    需要注意的是,如果想要达到这种效果,则互为绑定表的各个表的分片键要完全相同。例如,在上面的这些SQL语句中,我们不难看出,这个需要完全相同的分片键就是record_id。

  5. 广播表

    广播表(BroadcastTable)全局表,也就是它会存在于多个库中冗余,避免跨库查询问题。比如省份、字典等一些基础数据,为了避免分库分表后关联表查询这些基础数据存在跨库问题,所以可以把这些数据同步给每一个数据库节点,这个就叫广播表。

分片策略

包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略。

  1. 标准分片策略(Standard-ShardingStrategy)
    是ShardingSphere最常用的一种分片策略,提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND, >, <, >=, <=分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。

  2. 复合分片策略(ComplexShardingStrategy)
    复合分片策略。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

  3. 行表达式分片策略(InlineShardingStrategy)
    使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。

    PS: 行表达式(Line Expression)

    行表达式是 ShardingSphere 一种用于实现简化和统一配置信息的工具,在日常开发过程中的应用非常广泛。它的使用方式非常直观,只需要在配置中使用${expression}表达室或$->{expression}表达式即可。例如,ds${0.1}user${0.1}就是一个行表达式,用来设置可用的数据源或数据表名称。基于行表达式语法,${begin.end}表示的是一个从begin到end的范围,而多个${expression}之间可以用”.”符号进行连接,表示多个表达式数值之间的一种笛卡儿积关系。如果采用图形化的表现形式,则ds${0.1}user${0.1}表达式最终会被解析成如图所示的结果。

  4. Hint分片策略(HintShardingStrategy)
    通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略。

  5. 不分片策略(NoneShardingStrategy)

分布式主键

ShardingSphere不仅提供了内置的分布式主键生成器,例如UUID、SNOWFLAKE,还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成器。

内置主键生成器:

  1. UUID
    采用UUID.randomUUID()的方式产生分布式主键。
  2. SNOWFLAKE(SnowflakeShardingKeyGenerator)

在分片规则配置模块可配置每个表的主键生成策略,默认使用雪花算法,生成64bit的长整型数据,是 ShardingSphere 默认的分布式主键生成策略。

SnowFlake算法是Twitter开源的分布式ID生成算法,其核心思想是使用一个64bit 的long型的数字作为全局唯一ID,且ID引入了时间戳,基本能够保持自增。SnowFlake 算法在分布式系统中的应用十分广泛,SnowFlake算法中的64bit 详细结构具有一定的规范,如图。

img

数据分片的3种解决方案

如果要列举业界关于分库分表的框架,大致分成三大类型,即客户端分片、代理服务器分片及分布式数据库。

客户端分片

所谓客户端分片,相当于在数据库的客户端就完成了分片规则的实现。显然,这种方式将分片管理的工作进行了前置,客户端管理维护所有的分片逻辑,并决定每次执行SQL语句所对应的目标数据库和数据表。

客户端分片这一解决方案也有不同的表现形式,其中最为简单的方式就是应用层分片,也就是说在应用程序中直接维护分片信息。客户端分片结构图如图。

在具体实现上,客户端分片在实现上通常会进行进一步的抽象,其方法是重写JDBC协议,也就是说在JDBC协议层面嵌入分片规则。这样,业务开发人员还是使用与JDBC规范完全兼容的一套API来操作数据库,但这套API自动完成了分片操作,从而实现对业务代码的零侵入。基于JDBC规范重写机制的客户端分片结构图如图。

对于客户端分片,典型的中间件包括阿里巴巴的 TDDL 及ShardingSphere。因为TDDL并没有开源,无法知道其使用了哪种客户端分片方案。而对ShardingSphere来说,它是重写JDBC规范以实现客户端分片的典型实现框架。

代理服务器分片

代理服务器分片的解决方案也比较明确,就是采用了代理机制,也就是说在应用层和数据库层之间添加一个代理层。有了代理层之后,我们就可以把分片规则集中维护在代理层中,对外提供与JDBC兼容的API并给到应用层。这样,应用层的业务开发人员就不用关心具体的分片规则,而只需要完成业务逻辑的实现。代理服务器分片结构图如图所示。

代理服务器分片的优点是解决了业务开发人员对分片规则的管理工作,缺点是添加了一层代理层,所以带来了一些问题,比如,因为新增了一层网络传输对性能所产生的影响。

对于代理服务器分片,常见的开源框架有Cobar及Mycat。而在 ShardingSphere 3.X版本中,也添加了Sharding-Proxy模块来实现代理服务器分片。

分布式数据库

在技术发展过程中,关系型数据库的主要问题在于缺乏分布式特性,也就说,缺乏在分布式环境下对大数量、高并发访问的有效数据处理机制。例如,我们知道事务是关系型数据库的本质特征之一,但在分布式环境下,如果想要基于MySQL等传统关系型数据库来实现事务,则会面临巨大的挑战。

幸好,以TiDB为代表的分布式数据库的兴起赋予了关系型数据库一定程度的分布式特性。在这些分布式数据库中,数据分片及分布式事务将是其内置的基础功能,对业务开发人员而言是完全透明的。业务开发人员只需要使用TiDB对外提供的JDBC 接口,就像使用MySQL等传统关系型数据库一样。

Springboot整合ShardingJDBC

依赖

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.14</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!--Spring Boot Web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--Spring Boot Sharding JDBC-->
        <dependency>
            <groupId>org.apache.shardingsphere</groupId>
            <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
            <version>5.1.1</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!--druid连接池-->
<!--        <dependency>-->
<!--            <groupId>com.alibaba</groupId>-->
<!--            <artifactId>druid</artifactId>-->
<!--            <version>1.2.8</version>-->
<!--        </dependency>-->
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>

    </dependencies>

数据库表

CREATE TABLE sharding_jdbc_user_0(
	`uid` BIGINT(10) NOT NULL PRIMARY KEY COMMENT '用户ID'
	,`name` VARCHAR(10) NOT NULL COMMENT '用户名'
	,`gender` TINYINT(2) DEFAULT NULL COMMENT '性别'
	,`age` INT COMMENT '年龄'
)ENGINE = InnoDB CHARACTER SET = utf8;

CREATE TABLE sharding_jdbc_user_1(
	`uid` BIGINT(10) NOT NULL PRIMARY KEY COMMENT '用户ID'
	,`name` VARCHAR(10) NOT NULL COMMENT '用户名'
	,`gender` TINYINT(2) COMMENT '性别'
	,`age` INT COMMENT '年龄'
)ENGINE = InnoDB CHARACTER SET = utf8;

注意这里的uid是使用雪花算法生成的,最好使用BIGINT类型,Java中对应使用Long类型

实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {

    private static final long serialVersionUID = -5606617662256295246L;

    public User(String name, int gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }

    private Long uid;
    private String name;
    private int gender;
    private int age;

    @Override
    public String toString(){
        return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE);
    }
}

Mapper层

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

接口层

@RestController
public class UserController {

    @Autowired
    private UserMapper userMapper;

    @RequestMapping("/addUser")
    public String addUser(){
        try {
            for (int i = 0; i < 10; i++) {
                User testName = new User("test_name" + i, 0, i+10);
                userMapper.insert(testName);
            }
            return "success";
        }catch (Exception e){
            System.out.println("adduser error:" + e);
            return "error:" + e.getMessage();
        }
    }

    @RequestMapping("user")
    public String user(String uid){
        try {
            List<User> userList = userMapper.selectList(null);
            return userList.toString();
        }catch (Exception e){
            System.out.println("query user error:" + e);
        }
        return null;
    }
}

配置文件

server:
  port: 80

spring:
  shardingsphere:
    mode:
      type: memory
    # 是否开启
    datasource:
      # 数据源(逻辑名字)
      names: m1
      # 配置数据源
      m1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/test?useSSL=false&autoReconnect=true&characterEncoding=UTF-8&serverTimezone=UTC
        username: root
        password: ---
    # 分片的配置
    rules:
      sharding:
        # 表的分片策略
        tables:
          # 逻辑表的名称
          user:
            # 数据节点配置,采用Groovy表达式
            actual-data-nodes: m1.sharding_jdbc_user_$->{0..1}
            # 配置策略
            table-strategy:
              # 用于单分片键的标准分片场景
              standard:
                sharding-column: uid
                # 分片算法名字
                sharding-algorithm-name: user_inline
            key-generate-strategy: # 主键生成策略
              column: uid  # 主键列
              key-generator-name: snowflake  # 策略算法名称(推荐使用雪花算法)
        key-generators:
          snowflake:
            type: SNOWFLAKE
        sharding-algorithms:
          user_inline:
            type: inline
            props:
              algorithm-expression: sharding_jdbc_user_$->{uid % 2}
    props:
      # 日志显示具体的SQL
      sql-show: true

mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.jason.model.User
  configuration:
    #在映射实体或者属性时,将数据库中表名和字段名中的下划线去掉,按照驼峰命名法映射 address_book ---> addressBook
    map-underscore-to-camel-case: true

测试结果

插入日志

sharding_jdbc_user_0

sharding_jdbc_user_1

文章作者: 小小千千
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小小千千 !
评论
  目录