前言:

今天来聊下 Sharding-JDBC 分库分表中间件,这个中间件属于应用层依赖中间件,与应用层是强耦合的,需要在应用中显示加入依赖的Jar包;

简单聊聊 Sharding-JDBC 后,本文最主要是聊下 SpringBoot 集成 Sharding-JDBC 的过程;

小Demo:

为此,专门写了一个SpringBoot集成Sharding-JDBC的Demo,并且这个Demo也支持很多其它的功能;

本文主线:

  • 聊聊Sharding-JDBC的基本知识和注意事项;
  • Sharding-JDBC 集成Demo介绍;
  • Sharding-JDBC 集成过程;

Sharding-JDBC 简介:

首先贴出官网,大家可以去自行查阅:Sharding-JDBC 介绍

1、基本概念:

Sharding-JDBC 定位为轻量级Java框架,在Java的JDBC层提供的额外服务,所以说它是一款属于 应用层依赖类中间件

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

2、兼容性:

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

3、架构图:

图片来源:sharding-JDBC官网

4、数据分片:

进行分库分表时,是绕不开 数据分片 的知识的。

数据分片指:按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果。

数据分片的拆分方式又分为:

  • 垂直分片
  • 水平分片 (最为常用的方式)
垂直分片:

按照 业务功能 拆分的方式称为垂直分片,又称为纵向拆分,它的核心理念是专库专用。

例如:本来一个库由订单表和用户表构成,由于并发量和数据量太大,可以将这原本的一个库进行拆分,拆分成两个库,一个订单库,里面只有一个订单表,一个用户库,里面只有一个用户表,这样使用两个库就能支持更大的并发量,提升数据库的并发瓶颈。

缺点:

垂直分片往往需要对架构和设计进行调整。

通常来讲,垂直分片是来不及应对互联网业务需求快速变化的;而且,它也并无法真正的解决单点瓶颈。垂直拆分可以缓解数据量和访问量带来的问题,但无法根治。

水平分片:

水平分片又称为横向拆分。

相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过 某个字段(或某几个字段) ,根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。

注意:水平分片从理论上突破了单机数据量处理的瓶颈,并且扩展相对自由,是分库分表的 标准解决方案

例如,本文的Demo中实现的分库分表就是使用的 水平分片 ;

根据用户表中 name 用户名字段进行分片;在新增用户数据时,首先根据配置的分片策略(分片策略包含分片算法)判断此用户名的数据到底新增到哪个数据库中,以及哪个表中。

5、分片算法:

数据分片拆分方式指的是按照某个维度将数据进行拆分; 而分片算法(方法)指的是分库分表后,怎么将SQL路由到具体哪个数据节点中。

常用的分片算法:
  • hash方式
  • 一致性hash方式
  • 按照数据范围
对于任何的分片算法都要考虑的问题:
  • 是否支持动态扩容,动态添加数据库节点机器?
  • 当某个节点数据库down掉后,数据损失是否可以降到最低,以及能否将该节点上的任务均衡的平滑分摊到其他节点上?

这三种具体的分片算法本文就不做介绍了,大家可以通过此文章 带着问题学习分布式系统之数据分片 进行详细的了解;

注意:本文Demo中使用的分片算法是 一致性hash算法 ,此算法可以满足上述两个要求;此算法可以具体参考此文章 白话解析:一致性哈希算法 consistent hashing 进行了解;

6、SQL执行流程:

简单描述下项目中的SQL在 Sharding-JDBC 中内部核心的执行流程:

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

下面主要介绍下 SQL路由、SQL改写 的概念:

  • SQL路由:根据解析上下文匹配用户配置的分片策略,并生成最终的路由路径;
  • SQL改写:将SQL改写为在真实数据库节点中可以正确执行的语句;

执行流程图:

图片来源:sharding-JDBC官网

Demo 介绍:

1、工程目录:

注意:本Demo中,像有些功能在配置文件中加了限制开关了,默认可能是不启用的,如果想使用的话,则需要在配置文件中进行设置,将开关打开即可;

例如:quartz 定时任务,默认不启用的;

2、工程环境:

2.1、pom.xml:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.6.RELEASE</version>
    <relativePath/>
</parent>


<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.7</java.version>

    <mybatis-spring-boot>1.2.0</mybatis-spring-boot>
    <mysql-connector>5.1.39</mysql-connector>
    <fastjson>1.2.41</fastjson>
</properties>


<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <version>2.2.1.RELEASE</version>
    </dependency>


    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <optional>true</optional>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>


    <!-- Spring Boot Mybatis 依赖 -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.1</version>
    </dependency>

    <!-- MySQL 连接驱动依赖 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>

    <!--druid 连接池-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-starter</artifactId>
        <version>1.1.16</version>
    </dependency>

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>${fastjson}</version>
    </dependency>

    <!--swagger-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>

    <!-- pagehelper分页工具 -->
    <dependency>
        <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>4.1.6</version>
    </dependency>

    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.10</version>
    </dependency>

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

    <!-- hutool 工具类 -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-setting</artifactId>
        <version>5.2.4</version>
    </dependency>


    <!-- quartz定时任务 -->
    <dependency>
        <groupId>org.quartz-scheduler</groupId>
        <artifactId>quartz</artifactId>
        <version>2.2.1</version>
    </dependency>

</dependencies>

建议 :pom.xml 中依赖的版本最好不要变动了,因为如果将里面的一些依赖版本变化了,可能会导致依赖版本兼容性问题出现,最终导致程序运行失败。

2.2、SQL环境:

本Demo中数据库使用的 Mysql,程序运行是前需要提前配置好数据库的;

  • 由于对t_user表进行了分库,所以需要创建两个库:数据库名:springboot0、springboot1
  • 并且也对t_user表进行了分表,分为了t_user0、t_user1、t_user2 三个表

①、在 springboot0 数据库中执行下面的sql语句创建表:

DROP TABLE IF EXISTS `t_role`;
CREATE TABLE `t_role` (
  `id` int(11NOT NULL AUTO_INCREMENT,
  `role_name` varchar(128NOT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user0
-- ----------------------------
DROP TABLE IF EXISTS `t_user0`;
CREATE TABLE `t_user0` (
  `id` int(65NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(10DEFAULT NULL COMMENT '姓名',
  `age` int(2DEFAULT NULL COMMENT '年龄',
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user1
-- ----------------------------
DROP TABLE IF EXISTS `t_user1`;
CREATE TABLE `t_user1` (
  `id` int(65NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(10DEFAULT NULL COMMENT '姓名',
  `age` int(2DEFAULT NULL COMMENT '年龄',
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user2
-- ----------------------------
DROP TABLE IF EXISTS `t_user2`;
CREATE TABLE `t_user2` (
  `id` int(65NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(10DEFAULT NULL COMMENT '姓名',
  `age` int(2DEFAULT NULL COMMENT '年龄',
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8;

②、然后在创建的 springboot1 数据库中执行sql语句创建表:

DROP TABLE IF EXISTS `t_user0`;
CREATE TABLE `t_user0` (
  `id` int(65NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(10DEFAULT NULL COMMENT '姓名',
  `age` int(2DEFAULT NULL COMMENT '年龄',
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user1
-- ----------------------------
DROP TABLE IF EXISTS `t_user1`;
CREATE TABLE `t_user1` (
  `id` int(65NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(10DEFAULT NULL COMMENT '姓名',
  `age` int(2DEFAULT NULL COMMENT '年龄',
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=34 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_user2
-- ----------------------------
DROP TABLE IF EXISTS `t_user2`;
CREATE TABLE `t_user2` (
  `id` int(65NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(10DEFAULT NULL COMMENT '姓名',
  `age` int(2DEFAULT NULL COMMENT '年龄',
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8;

上面Demo的基本信息介绍完了,接下来介绍重头戏了,Sharding-JDBC集成之路啦!

集成过程:

1、主要看下Sharding-JDBC的配置文件:

下面是Sharding-JDBC 配置文件的内容;

注意:

本Demo中的只有 t_user 表进行了分库分表,其它表没有进行分库分表,那些没进行分库分表的表是走设置的默认数据库;

分库2个,分表3个,分片键是t_user表中的 name 字段,分库分表都是依据 name 这个分片键的;

## 分库分表 配置: (下面配置的分库数量、虚拟节点数量等主要是为了实现一致性hash算法进行分片)

# 分库数量
sharding.datasource.count=2

# 分库虚拟节点数量
sharding.datasource.virtual.node.count=360

# 虚拟节点映射到物理节点范围:例如本文中是根据name名字进行分片的, 所以使用名字的hash值对虚拟节点数取余;
# 得到一个0-359的余数,然后按照余数所属的范围, 如果余数在0-179范围则数据分片访问 springboot0 数据源,
# 如果余数在180-359范围,则数据被分片访问 springboot1 数据源; 下面的分表原理一样。
sharding.datasource.virtual.node.count.rang=0-179,180-359

# 分表数量
sharding.table.count=3

# 分表虚拟节点数量
sharding.table.virtual.node.count=360

# 虚拟节点映射到物理节点范围
sharding.table.virtual.node.count.rang=0-119,120-249,250-359


# 实际数据源名字
spring.shardingsphere.datasource.names=springboot0,springboot1

# 数据源
spring.shardingsphere.datasource.springboot0.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.springboot0.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.springboot0.url=jdbc:mysql://localhost:3306/springboot0?characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.springboot0.username=root
spring.shardingsphere.datasource.springboot0.password=root

spring.shardingsphere.datasource.springboot1.type=com.alibaba.druid.pool.DruidDataSource
spring.shardingsphere.datasource.springboot1.driver-class-name=com.mysql.jdbc.Driver
spring.shardingsphere.datasource.springboot1.url=jdbc:mysql://localhost:3306/springboot1?characterEncoding=utf-8&useSSL=false
spring.shardingsphere.datasource.springboot1.username=root
spring.shardingsphere.datasource.springboot1.password=root


### 分片策略使用的是: 自定义的分片算法
## 实际的数据节点,符合 groovy 语法; 这里的{0..1}指的是0到1及其之间的数字,数字有0,1两个,代表分库是两个;
## 并且拼接在 springboot 后面,就构成了上面配置的实际数据源名称了
spring.shardingsphere.sharding.tables.t_user.actualDataNodes=springboot$->{0..1}.t_user$->{0..2}

## 分片键:name字段
spring.shardingsphere.sharding.tables.t_user.databaseStrategy.standard.shardingColumn=name
## 自定义 分库 算法
spring.shardingsphere.sharding.tables.t_user.databaseStrategy.standard.preciseAlgorithmClassName=com.lyl.algorithm.MyPreciseDBShardingAlgorithm

## 分片键:name字段
spring.shardingsphere.sharding.tables.t_user.tableStrategy.standard.shardingColumn=name
## 自定义 分表 算法
spring.shardingsphere.sharding.tables.t_user.tableStrategy.standard.preciseAlgorithmClassName=com.lyl.algorithm.MyPreciseTableShardingAlgorithm


# 不进行分库分表的数据源指定,使用设置的默认数据源springboot0 ;例如,本文中的 t_role表就不进行
# 分库分表,那关于 t_role 表的增删改差都走默认数据源 springboot0
spring.shardingsphere.sharding.default-data-source-name=springboot0
# 打印执行的数据库以及语句
spring.shardingsphere.props.sql.show=true

如果需要更改分库数量,或者分表数量的话,那么也需要对配置文件进行更改;例如:将分库数量 由之前的2个改为3个 ; 下面这些配置文件内容需要更改:

原配置内容:

sharding.datasource.count=2
sharding.datasource.virtual.node.count.rang=0-179,180-359
spring.shardingsphere.sharding.tables.t_user.actualDataNodes=springboot$->{0..1}.t_user$->{0..2}

更改后的配置内容:

sharding.datasource.count=3
sharding.datasource.virtual.node.count.rang=0-119,120-249,250-359
spring.shardingsphere.sharding.tables.t_user.actualDataNodes=springboot$->{0..2}.t_user$->{0..2}

2、分片算法:

介绍完上面数据分片的情况后,然后再介绍下项目中自定义的数据分片算法;

2.1、自定义的 分库 算法:
2.2、自定义的 分表 算法:

3、注意事项:

本项目中只有 t_user 表进行了分库分表, t_role 表没有进行 分库分表,t_role 表的增删改差走 默认数据源springboot0

注意: Sharding-JDBC 是 不支持跨库查询 的;当使用联合查询SQL时,如果在联合查询中存在 进行了分库的表 和 未进行分库的表 ,那么就可能会出现跨库查询,此时是查询不出数据的; 例如在本Demo中如果使用 t_user、t_role 两张表进行关联查询时,就会存在跨库问题。

提供的建议:
  • 如果一定要使用联合查询SQL,那么需要将联合查询中所有的表都进行分库,并且分库时选择的分片键字段需要一致,并将表设置为 绑定表 ,提升联合查询速度;

  • 一般在分库分表后不建议使用多表联合查询,由于会出现上面那种问题,建议使用 单表查询 。使用单表查询时,可以使用应用层代码将多个单表查询的结果集进行整合即可。

为什么建议使用单表查询呢:
  • 使用单表查询可以有效降低数据库端的读写压力;因为在使用多表关联查询时,数据库对SQL进行解析时比较复杂,并且需要在经历很多的计算后才能得到最终的结果集 等等;所以说多表关联查询时数据库压力非常大,如果数据库端压力过大的话,就需要进行扩容,扩容时就涉及到了数据一致性等问题,这是非常麻烦的。

  • 在使用单表查询时,主要将压力都集中到了服务应用代码上了,如果服务应用代码压力很大的话,可以很简单的 部署集群(多部署几台机器) + Nginx请求转发 就可以提高整体系统的吞吐量了。

自此SpringBoot集成 Sharding-JDBC 之路就此结束了,本文由于篇幅问题,只是给大家进行了较为宽泛的介绍,没有进行细致描述;

但是没关系呀,大家可以去 【木子雷】 公众号中或直接扫描下方二维码,输入 jdbc 获取本文中 Demo 的地址,大家可以自己仔细去阅读代码进行理解,如果有不理解的或Demo代码中存在问题的可以留言讨论呀!

点赞 + 评论 + 转发 哟

如果本文对您有帮助的话,请挥动下您爱发财的小手点下赞呀,您的支持就是我不断创作的动力,谢谢啦!

您可以微信搜索 【木子雷】 公众号,大量Java学习干货文章,您可以来瞧一瞧哟!