《动手学微服务》系列文章将专注微服务中的常见思想、常用技术和常见架构。本系列的特点是不仅在理论上对微服务的知识进行梳理,还会有一系列的动手实践,不仅在平时学习会有帮助,也有助于面试。本人也是微服务的小学徒,为了巩固所学而创建此专栏,欢迎大家持续关注。
前言
在《动手学微服务(一):实战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_0
到t_order_9
。 - 数据节点:分片的最小单位。由数据源+数据表组成。这里就是
ds_0.t_order_0
- 绑定表:分片规则一致的主表和子表。例如
t_order
和t_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_0
到t_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);
会改写成下面两个SQLSELECT * 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)返回至查询客户端,并且将游标下移一位之后,重新放入优先级队列。
分组归并
分组归并示意图:
流式分组归并与排序归并的区别仅仅在于两点:
- 它会一次性的将多个数据结果集中的分组项相同的数据全数取出。
- 它需要根据聚合函数的类型进行聚合计算。
原理示意图:
聚合归并
大致示意图如下,原理都类似:
分页归并
一般是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_00
到user_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的读写分离和分库分表配置,提供了具体的配置和代码示例。
希望这能为大家在学习和实践中提供有力支持。