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

前言

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

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

大名鼎鼎的ShardingSphere全家桶

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

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

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

image-t3yp.png

ShardingProxy

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

ShardingSphere-Proxy Architecture

下面是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标准的数据库。

ShardingSphere-JDBC Architecture

上面是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执行 => 结果归并的流程组成。

image-klqs.png

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

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

路由策略

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

image-yzwn.png

  • 直接路由:直接选定想走的数据源,通过 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种,支持组合

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

image-2xds.png

遍历归并

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

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

image-vbmr.png

排序归并

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

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

image-vozr.png

分组归并

分组归并示意图:

image-20dc.png

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

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

原理示意图:

image-rxex.png

聚合归并

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

image-t6vv.png

分页归并

一般是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

<!-- 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的配置文件。

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以及最大连接数等。
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等代码。

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

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

image-fy3v.png

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

image-qxoy.png

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

image-aihu.png

总结

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

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

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