动手学微服务(二):面试官问我有没有使用过ShardingJDBC

《动手学微服务》系列文章将专注微服务中的常见思想、常用技术和常见架构。本系列的特点是不仅在理论上对微服务的知识进行梳理,还会有一系列的动手实践,不仅在平时学习会有帮助,也有助于面试。本人也是微服务的小学徒,为了巩固所学而创建此专栏,欢迎大家持续关注。

前言

在《动手学微服务(一):实战MySQL读写分离和分库分表》中,我们对分库分表和读写分离的概念进行的介绍,在此基础上我们还搭建了MySQL的主从架构并对"用户中台"的业务创建了100张分表。

然而,读写分离和分库分表的实现还需要一个关键的角色,那就是中间件层,它将负责写请求分配给主库,读请求分配给读库。而ShardingSphere全家桶就提供了这个方面的解决方案。

大名鼎鼎的ShardingSphere全家桶

ShardingSphere是一套开源的分布式数据库中间件 解决方案组成的生态圈,它由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成。

他们均提供标准化的数据分片分布式事务数据库治理功能,可适用于如Java和云原生等场景。

ShardingSphere在2020年4月16日成为Apache顶级项目。下面我们将分别介绍他们家的三款产品,只有了解了他们的适用场景,我们才能更好的进行技术选型。

ShardingProxy

ShardingProxy是一款透明化的数据库代理端,提供封装了数据库二进制协议的服务端版本,用于完成对异构语言的支持。 目前兼容MySQL/PostgreSQL协议的访问客户端。

下面是ShardingProxy的架构图,可以看到它使用的是中心化的架构设计,这也容易导致性能瓶颈

ShardingJDBC

ShardingJDBC是一款轻量级Java框架,在Java的JDBC层 提供的额外服务。它以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架

ShardingJDBC有如下特点:

  • 在业务层引入依赖包性能损耗很低,主要是对SQL进行改写,路由到不同数据库中。
  • 兼容任何基于JDBC的ORM框架,如:JPA, Hibernate, Mybatis, JDBC Template。
  • 支持任何第三方的数据库连接池,如:DBCP, C3P0, BoneCP, Druid, HikariCP等。
  • 支持任意实现JDBC规范的数据库。目前支持MySQL,Oracle,SQLServer,PostgreSQL以及任何遵循SQL92标准的数据库。

上面是ShardingJDBC的架构图,可以看到它是在应用层面实现的SQL改写完成路由作用。

ShardingSidecar

ShardingSidecar定位为 Kubernetes 的云原生数据库代理,以 Sidecar 的形式代理所有对数据库的访问。 通过无中心、零侵入的方案提供与数据库交互的啮合层,即 Database Mesh,又可称数据库网格。

由于本人对云原生了解的不多,在这里就不多讨论的,感兴趣的小伙伴可以前往官网查看。

ShardingJDBC的底层原理介绍

基本概念

学习ShardingJDBC之前,我们需要了解它的一些核心概念,我们以订单表t_order为例子。

  • 逻辑表:水平拆分的数据库表的相同逻辑和数据结构的表总称。这里就是t_order
  • 真实表:分片所在的真实存在的物理表。这里就是t_order_0t_order_9
  • 数据节点:分片的最小单位。由数据源+数据表组成。这里就是ds_0.t_order_0
  • 绑定表:分片规则一致的主表和子表。例如t_ordert_order_item且均按照order_id分片。

分片算法

分片策略包含分片键和分片算法。ShardingJDBC有5种分片算法:

  • 标准分片策略 :StandardShardingStrategy。
    • 对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。
    • 只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。前者处理=和in,后者处理BETWEEN AND, >, <, >=, <=分片。不配置后者默认做全库路由处理。
  • 复合分片策略 :ComplexShardingStrategy。
    • 支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
  • 行表达式分片策略 :InlineShardingStrategy
    • 使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。
    • 对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0t_user_7
  • Hint分片策略 :HintShardingStrategy。
    • 通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略。
  • 不分片策略:NoneShardingStrategy

数据分片的内核分析

ShardingSphere的3个产品的数据分片主要流程是完全一致的。核心由SQL解析 => 执行器优化 => SQL路由 => SQL改写 => SQL执行 => 结果归并的流程组成。

  • SQL解析:词法解析+语法解析(抽象语法树)
  • 执行器优化:合并和优化分片条件。
  • SQL路由:匹配分片策略。
  • SQL改写:正确性改写/优化改写。
  • SQL执行:通过多线程执行器异步执行
  • 结果归并:多个结果集合并到一起。

我们这里重点看看ShardingJDBC都有哪些路由策略和归并策略。

路由策略

这张图可以概括ShardingJDBC的路由引擎:

  • 直接路由 :直接选定想走的数据源,通过HintManager来实现。使用场景比如说从源表查询但是插入到分表。
  • 标准路由 :按照查询语句中的字段,根据路由规则,直接对表名进行修改。是ShardingJDBC最推荐的路由方式。
    • 适用场景:不包含关联查询或仅包含绑定表之间关联查询的SQL。
    • 例如:SELECT * FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE order_id IN (1, 2); 会改写成下面两个SQL
      • SELECT * FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
      • SELECT * FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE order_id IN (1, 2);
  • 笛卡尔积路由 :非绑定表之间的关联查询需要拆解为笛卡尔积组合执行。
    • 例如上面的SQL,如果是非绑定表的关系,则无法确定分片规则,需要改成对应的4条SQL去执行。
    • 笛卡尔积路由查询性能较低,应该尽量避免。
  • 全库路由 :对于不带分片键的DQL、DML和DDL等,会匹配数据库中的所有表。
    • 例如:SELECT * FROM t_order WHERE good_prority IN (1, 10);,会匹配所有表。
  • 全库实例路由:用于DCL,比较少用,详见官网。
  • 单播路由:用于获取某一真实表信息的场景,它仅需要从任意库中的任意真实表中获取数据即可。
  • 阻断路由:用于屏蔽SQL对数据库的操作。

归并策略

所谓归并就是将多个查询的结果集进行汇总的功能实现,下图是ShardingJDBC的归并策略。

ShardingSphere支持的结果归并从功能上分为5种,支持组合

  • 遍历归并
  • 排序归并
  • 分组归并
  • 聚合归并
  • 分页归并

遍历归并

最为简单的归并方式。 只需将多个数据结果集合并为一个单向链表即可。

例如执行这条SQL:select user_id from t_user后将多个分表的结果直接进行遍历归并。

排序归并

SQL中存在ORDER BY的情况下,因此每个数据结果集自身是有序的。这相当于对多个有序的数组进行排序,归并排序是最适合此场景的排序算法。

ShardingSphere在对排序的查询进行归并时,将每个结果集的当前数据值进行比较(通过实现Java的Comparable接口完成),并将其放入优先级队列。 通过图中我们可以看到,当进行第一次next调用时,排在队列首位的t_score_0将会被弹出队列,并且将当前游标指向的数据值(也就是100)返回至查询客户端,并且将游标下移一位之后,重新放入优先级队列。

分组归并

分组归并示意图:

流式分组归并与排序归并的区别仅仅在于两点:

  1. 它会一次性的将多个数据结果集中的分组项相同的数据全数取出。
  2. 它需要根据聚合函数的类型进行聚合计算。

原理示意图:

聚合归并

大致示意图如下,原理都类似:

分页归并

一般是LIMIT写法是:LIMIT 10000000,然而,由于LIMIT并不能通过索引查询数据,因此如果可以保证ID的连续性,通过ID进行分页是比较好的解决方案。比如:SELECT * FROM t_order WHERE id > 100000 AND id <= 100010 ORDER BY id;

第二种方案是通过记录上次查询结果的最后一条记录的ID进行下一页的查询SELECT * FROM t_order WHERE id > 10000000 LIMIT 10;

总结

使用ShardingJDBC之后,尽量使用简单查询类型的SQL,少用分组查询和聚合函数。对于分页查询则要谨慎使用,避免产生全表扫描的情况。

实战:使用ShardingJDBC完成分库分表配置

我们还是以通过SpringCloud搭建用户中台的业务来举例子,学习如何使用ShardingJDBC。在这一步,我们会用到上一篇文章搭建的MySQL主从架构以及创建的用户分表。

首先我们引入三个依赖:MySQL、ShardingJDBC、mybatisplus

xml 复制代码
<!-- mysql -->  
<dependency>  
    <groupId>mysql</groupId>  
    <artifactId>mysql-connector-java</artifactId>  
    <version>8.0.28</version>  
</dependency>  
<!-- sharding-jdbc -->  
<dependency>  
    <groupId>org.apache.shardingsphere</groupId>  
    <artifactId>shardingsphere-jdbc-core</artifactId>  
    <version>5.3.2</version>  
</dependency>  
<dependency>  
    <groupId>com.baomidou</groupId>  
    <artifactId>mybatis-plus-boot-starter</artifactId>  
    <version>3.5.3</version>  
</dependency>

配置数据源的驱动类为ShardingSphereDriver,url需要也要改成Sharding的配置文件。

yml 复制代码
spring:  
  datasource:  
    # sharding-jdbc配置  
    driver-class-name: org.apache.shardingsphere.driver.ShardingSphereDriver  
    url: jdbc:shardingsphere:classpath:test-db-sharding.yml

在resource目录下新建一个test-db-sharding.yml,存储ShardingJDBC的配置文件如下。这是我稍微解释一下这些配置的含义。

  • 配置主数据源和从数据源。
  • 配置读写分离规则,读写分离的数据源我们设置为user_ds,写策略配置写到主库,读策略是从库。
  • 配置不分库分表的默认数据源
  • 配置分表策略,我们这只有t_user这张表。
    • 配置实际数据节点:user_ds.t_user_${}里面的表达式(0..99).collect(){it.toString().padLeft(2,'0')}理解为一个集合,这个集合是字符串00到99。这表示最终生成的表是- user_ds.t_user_00user_ds.t_user_99这些集合。
      • padLeft:保证字符串长度为两位,如果长度不足,则在左侧填充0。
      • .collect(){...}是一个Groovy闭包,用于对范围内的每个整数进行处理。
    • standard表示使用标准分片策略。根据user_id分片,分片算法是t_user-inline
    • 分片算法t_user-inline:使用内联分片算法INLINE根据user_id对100取模,将结果转换为两位数的字符串,然后拼接生成实际表名。
  • 配置是否打印SQL以及最大连接数等。
yml 复制代码
dataSources:
  user_master:  ## 主数据源
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:8808/test_user?useUnicode=true&characterEncoding=utf8
    username: root
    password: 密码

  user_slave0:  ## 从数据源
    dataSourceClassName: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    jdbcUrl: jdbc:mysql://localhost:8809/test_user?useUnicode=true&characterEncoding=utf8
    username: root
    password: 密码

rules:
  # 读写分离规则
  - !READWRITE_SPLITTING
    dataSources:
      user_ds: # 读写分离数据源
        staticStrategy:
          writeDataSourceName: user_master # 写策略
          readDataSourceNames: # 读策略
            - user_slave0
  - !SINGLE
    defaultDataSource: user_ds ## 不分表分分库的默认数据源
  - !SHARDING
    tables:
      t_user: # 表
        actualDataNodes: user_ds.t_user_${(0..99).collect(){it.toString().padLeft(2,'0')}}
        tableStrategy:
          standard:
            shardingColumn: user_id
            shardingAlgorithmName: t_user-inline
    shardingAlgorithms:
      t_user-inline:
        type: INLINE
        props:
          algorithm-expression: t_user_${(user_id % 100).toString().padLeft(2,'0')}
props:
  sql-show: true
  max-connections-size-per-query: 3

到这里ShardingJDBC就配置完了,接下来我们简单使用MybatisPlus编写一些业务代码如下,这里省略Mapper、Controller等代码。

java 复制代码
@Override
public UserDto getByUserId(Long userId) {
    if(userId== null){
        return null;
    }
    userDto = ConvertBeanUtils.convert(userMapper.selectById(userId), UserDto.class);
    return userDto;
}

简单使用curl进行测试,观察控制台输出,会打印原SQL和改写后的SQL,可以看到读对从库进行读操作。

同样的道理,还可以试一试update语句,可以看到写是对主库进行写操作。

insert也是同理,是对主库进行写操作。

总结

本文主要介绍了ShardingSphere全家桶,包括Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar。

我们着重介绍了ShardingJDBC,它作为一个轻量级Java框架,在JDBC层提供数据分片、分布式事务和数据库治理功能。本文讲解了它的分片算法、路由策略和结果归并策略,并结合SpringCloud,展示了如何通过ShardingJDBC实现MySQL的读写分离和分库分表配置,提供了具体的配置和代码示例。

希望这能为大家在学习和实践中提供有力支持。

相关推荐
8Qi818 分钟前
Redis哨兵模式(Sentinel)深度解析
java·数据库·redis·分布式·缓存·sentinel
数据库小组21 分钟前
从业务库到实时分析库,NineData 构建 MySQL 到 SelectDB 同步链路
数据库·mysql·数据库管理工具·数据同步·ninedata·数据库迁移·selectdb
CDN36027 分钟前
CDN HTTPS 证书配置失败?SSL 部署与域名绑定常见问题
数据库·https·ssl
无籽西瓜a28 分钟前
【西瓜带你学设计模式 | 第五期 - 建造者模式】建造者模式 —— 产品构建实现、优缺点与适用场景及模式区别
java·后端·设计模式·软件工程·建造者模式
Chengbei1134 分钟前
一次比较简单的360加固APP脱壳渗透
网络·数据库·web安全·网络安全·系统安全·网络攻击模型·安全架构
寒秋花开曾相惜35 分钟前
(学习笔记)3.9 异质的数据结构(3.9.1 结构)
c语言·网络·数据结构·数据库·笔记·学习
mcooiedo1 小时前
mybatisPlus打印sql配置
数据库·sql
wudl55661 小时前
MySQL 8.0.42 Docker 开发部署手册
数据库·mysql·docker
xhuiting1 小时前
MySQL专题总结(四)—— 高可用
java·数据库·mysql
无名-CODING1 小时前
小白初识 SpringCloud:微服务基础与 SpringCloud 核心作用
spring·spring cloud·微服务