1.1 ShardingSphere简介
最早是当当网内部使用的一款分库分表框架,名字叫Sharding-JDBC,定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。 它使用客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖,Sharding-JDBC直接封装JDBC API,可以理解为增强版的JDBC驱动,旧代码迁移成本几乎为零,适用于任何基于 JDBC 的 ORM 框架,支持任何第三方的数据库连接池,支持任意实现 JDBC 规范的数据库。
2017年的时候开始对外开源,在大量社区贡献者的不断迭代下,功能也逐渐完善,现已更名为ShardingSphere。
2020年正式成为Apache软件基会的顶级项。由Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar(计划中)这3款相互独立的产品组成。
1.2 Sharding-JDBC优点
Sharding-JDBC定位为轻量级 Java 框架,在 Java 的 JDBC 层提供的额外服务。
客户端直连数据库,以 jar 包形式提供服务,无需额外部署和依赖。
Sharding-JDBC直接封装JDBC API。
旧代码迁移成本几乎为零。
适用于任何基于 JDBC 的 ORM 框架。
支持任何第三方的数据库连接池。
支持任意实现 JDBC 规范的数据库。
1.3 核心概念
1.3.1 LogicTable(逻辑表)
数据分片的逻辑表,对于水平拆分的数据库(表),同一类表的总称。例:订单数据根据主键尾数拆分为10张表,分别是t_order_0到t_order_9,他们的逻辑表名为t_order。
1.3.2 ActualTable(真实表)
在分片的数据库中真实存在的物理表。即上个示例中的t_order_0到t_order_9。
1.3.3 DataNode(数据节点)
数据分片的最小单元。由数据源名称和数据表组成,例:ds_1.t_order_0。配置时默认各个分片数据库的表结构均相同,直接配置逻辑表和真实表对应关系即可。如果各数据库的表结果不同,可使用ds.actual_table配置。
1.3.4 BindingTable(绑定表)
指在任何场景下分片规则均一致的主表和子表。例:订单表和订单项表,均按照订单ID分片,则此两张表互为BindingTable关系。BindingTable关系的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。
1.3.5 BroadcastTable(广播表)
指所有的分片数据源中都存在的表,表结构和表中的数据在每个数据库中均完全一致。适用于数据量不大且需要与海量数据的表进行关联查询的场景,例如:字典表。
1.4 分片
1.4.1 分片键
用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:订单表订单ID分片尾数取模分片,则订单ID为分片字段。SQL中如果无分片字段,将执行全路由,性能较差,支持多分片字段。
1.4.2 分片算法
1.4.2.1 精确分片算法
精确分片算法(PreciseShardingAlgorithm)精确分片算法(=与IN语句),用于处理使用单一键作为分片键的=与IN进行分片的场景。需要配合StandardShardingStrategy使用
1.4.2.2 范围分片算法
范围分片算法(RangeShardingAlgorithm)用于处理使用单一键作为分片键的BETWEEN AND进行分片的场景。需要配合StandardShardingStrategy使用。
1.4.2.3 复合分片算法
复合分片算法(ComplexKeysShardingAlgorithm)用于多个字段作为分片键的分片操作,同时获取到多个分片健的值,根据多个字段处理业务逻辑。需要在复合分片策略(ComplexShardingStrategy )下使用。
1.4.2.4 Hint分片算法
Hint分片算法(HintShardingAlgorithm)稍有不同,上边的算法中我们都是解析 语句提取分片键,并设置分片策略进行分片。但有些时候我们并没有使用任何的分片键和分片策略,可还想将 SQL 路由到目标数据库和表,就需要通过手动干预指定SQL的目标数据库和表信息,也叫强制路由。
1.4.3 分片策略
包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略。
一个好的分片策略=好的分片键+好的的分片算法
1.4.3.1 标准分片策略
对应StandardShardingStrategy。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。
1.4.3.2复合分片策略
对应ComplexShardingStrategy。复合分片策略。提供对SQL语句中的=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。
1.4.3.3 行表达式分片策略
对应InlineShardingStrategy。使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。
1.4.3.4 Hint分片策略
对应HintShardingStrategy。通过Hint而非SQL解析的方式分片的策略。
1.4.3.5 不分片策略
对应NoneShardingStrategy。不分片的策略。
1.5 分布式主键
等同于本文3.4部分
1.6 分布式事务
数据库事务需要满足ACID(原子性、一致性、隔离性、持久性)四个特性。
原子性(Atomicity)指事务作为整体来执行,要么全部执行,要么全不执行。
一致性(Consistency)指事务应确保数据从一个一致的状态转变为另一个一致的状态。
隔离性(Isolation)指多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
持久性(Durability)指已提交的事务修改数据会被持久保存。
在单一数据节点中,事务仅限于对单一数据库资源的访问控制,称之为本地事务。几乎所有的成熟的关系型数据库都提供了对本地事务的原生支持。 但是在基于微服务的分布式应用环境下,越来越多的应用场景要求对多个服务的访问及其相对应的多个数据库资源能纳入到同一个事务当中,分布式事务应运而生。
1.6.1 本地事务
在不开启任何分布式事务管理器的前提下,让每个数据节点各自管理自己的事务。 它们之间没有协调以及通信的能力,也并不互相知晓其他数据节点事务的成功与否。 本地事务在性能方面无任何损耗,但在强一致性以及最终一致性方面则力不从心。
1.6.2 XA强一致事务
XA协议最早的分布式事务模型是X/OPEN国际联盟提出的模型,简称XA协议。
基于XA协议实现的分布式事务对业务侵入很小。 它最大的优势就是对使用方透明,用户可以像使用本地事务一样使用基于XA协议的分布式事务。 XA协议能够严格保障事务ACID特性。
严格保障事务ACID特性是一把双刃剑。 事务执行在过程中需要将所需资源全部锁定,它更加适用于执行时间确定的短事务。 对于长事务来说,整个事务进行期间对数据的独占,将导致对热点数据依赖的业务系统并发性能衰退明显。 因此,在高并发的性能至上场景中,基于XA协议的分布式事务并不是最佳选择。
1.6.3 柔性事务
如果将实现了ACID的事务要素的事务称为刚性事务的话,那么基于BASE事务要素的事务则称为柔性事务。BASE是基本可用、柔性状态和最终一致性这三个要素的缩写。
基本可用(Basically Available)保证分布式事务参与方不一定同时在线。
柔性状态(Soft state)则允许系统状态更新有一定的延时,这个延时对客户来说不一定能够察觉。
而最终一致性(Eventually consistent)通常是通过消息可达的方式保证系统的最终一致性。
1.7 读写分离
1.7.1 读写分离核心概念
1.7.1.1 主库
添加、更新以及删除数据操作所使用的数据库,目前仅支持单主库。
1.7.1.2 从库
查询数据操作所使用的数据库,可支持多从库。
1.7.1.3 主从同步
将主库的数据异步的同步到从库的操作。由于主从同步的异步性,从库与主库的数据会短时间内不一致。
1.7.1.4 负载均衡策略
通过负载均衡策略将查询请求疏导至不同从库。
1.7.1.5 Config Map
配置读写分离数据源的元数据,可通过调用ConfigMapContext.getInstance()获取ConfigMap中的masterSlaveConfig数据。例:如果机器权重不同则流量可能不同,可通过ConfigMap配置机器权重元数据。
1.7.2 核心功能
提供了一主多从的读写分离配置,可独立使用,也可配合分库分表使用
同一线程且同一数据库连接内,如有写入操作,以后的读操作均从主库读取,用于保证数据一致性。
Spring命名空间
基于Hint的强制主库路由。
1.7.3 不支持范围
主库和从库的数据同步。
主库和从库的数据同步延迟导致的数据不一致。
主库双写或多写。
二、Sharding-JDBC实际操作
核心依赖:
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
2.1 配置分片算法前准备操作
在配置Sharding-JDBC分片算法之前,肯定都需要做的一些配置,配置数据源,数据节点,设置主键及生成算法这种基础的配置,有了这些配置之后,我们才能继续配置分片算法。
2.1.1 配置数据源
我这里设置了两个库,配置了两个数据源。
2.1.2 配置数据节点
$->是Sharding为我们提供的行表达式,这个表达式意思就是我们配置的数据节点为m0库course0表,m0库course1表,m1库course0表和m1库course1表,一共四个节点,这样的配置就很简便,我设置的库和表比较少,直接列举出来好像也行,但是如果分了很多库,很多表,逐个列举就会非常麻烦。
#数据节点
spring.shardingsphere.sharding.tables.course.actual-data-nodes=m$->{0..1}.course$->{0..1}
2.1.3 配置全局主键及生成策略
下面为course表设置的主键是cid,用了雪花算法保证全局主键唯一。
#设置主键
spring.shardingsphere.sharding.tables.course.key-generator.column=cid
#主键生成策略,雪花算法
spring.shardingsphere.sharding.tables.course.key-generator.type=SNOWFLAKE
2.2 精准分片算法实践
2.2.1 配置类
首先要设置分片键设置逻辑表名为course的分表策略,这里就需要我们自定义算法类了
分库策略类似,将table-strategy改为database-strategy,设置一个分库策略的自定义算法类
#分片键
spring.shardingsphere.sharding.tables.course.table-strategy.standard.sharding-column=user_id
#分表策略-精准
spring.shardingsphere.sharding.tables.course.table-strategy.standard.precise-algorithm-class-name=com.mine.sharding.algorithm.MyPreciseTableShardingAlgorithm
2.2.2 自定义算法类
实现Sharding提供的精准分片接口,重写一下doSharding方法,接下来的几个分片算法,也都是差不多的操作。
public class MyPreciseTableShardingAlgorithm implements PreciseShardingAlgorithm<Long> {
//select * from course where cid = '' or cid in ('','')
@Override
public String doSharding(Collection<String> collection, PreciseShardingValue<Long> preciseShardingValue) {
String logicTableName = preciseShardingValue.getLogicTableName();
String columnName = preciseShardingValue.getColumnName();
Long cidValue = preciseShardingValue.getValue();
//实现course$->{cid%2}
BigInteger shardingValueB = BigInteger.valueOf(cidValue);
BigInteger resB = shardingValueB.mod(new BigInteger("2"));
String key = logicTableName+resB;
if (collection.contains(key)){
return key;
}
throw new UnsupportedOperationException("route:"+key+" is not supported,please check your config");
}
}
2.3 范围分片算法实践
2.3.1 配置类
#分片键
spring.shardingsphere.sharding.tables.course.table-strategy.standard.sharding-column=user_id
#分表策略-范围
spring.shardingsphere.sharding.tables.course.table-strategy.standard.range-algorithm-class-name=com.mine.sharding.algorithm.MyRangeTableShardingAlgorithm
2.3.2 自定义算法类
public class MyRangeTableShardingAlgorithm implements RangeShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> collection, RangeShardingValue<Long> rangeShardingValue) {
Long upperValue = rangeShardingValue.getValueRange().upperEndpoint();
Long lowerValue = rangeShardingValue.getValueRange().lowerEndpoint();
String logicTableName = rangeShardingValue.getLogicTableName();
//course_$->{cid%2}
return Arrays.asList(logicTableName+"0",logicTableName+"1");
}
}
2.4 复合分片算法实践
什么是复合,或者是复杂,不再是单一条件了,可以看到,配置类中的sharding-column加s了,不再是单数了
这种复合的分片策略,支持多个分片键,按多个分片键组合出复杂的分片策略
2.4.1 配置类
#分片键,可多个
spring.shardingsphere.sharding.tables.course.table-strategy.complex.sharding-columns=cid,user_id
#分表策略
spring.shardingsphere.sharding.tables.course.table-strategy.complex.algorithm-class-name=com.mine.sharding.algorithm.MyComplexTableShardingAlgorithm
2.4.2 自定义算法类
public class MyComplexTableShardingAlgorithm implements ComplexKeysShardingAlgorithm<Long> {
@Override
public Collection<String> doSharding(Collection<String> collection, ComplexKeysShardingValue<Long> complexKeysShardingValue) {
Range<Long> cidRange = complexKeysShardingValue.getColumnNameAndRangeValuesMap().get("cid");
Collection<Long> userIdCol = complexKeysShardingValue.getColumnNameAndShardingValuesMap().get("user_id");
Long cidUpper = cidRange.upperEndpoint();
Long cidLower = cidRange.lowerEndpoint();
List<String> result = new ArrayList<>();
for (Long userId : userIdCol) {
BigInteger userIdB = BigInteger.valueOf(userId);
BigInteger target = userIdB.mod(new BigInteger("2"));
result.add(complexKeysShardingValue.getLogicTableName()+target);
}
return result;
}
}
2.5 Hint分片算法(强制路由)实践
2.5.1 配置类
配置文件中,没有指定分片键,为什么呢,大家可以看名字,强制路由,强制了,我也就没必要再预先设置一个分片键了,在程序中干预,指定SQL的目标数据库和表信息。
#Hint在配置中不指定分片键
#分表策略
spring.shardingsphere.sharding.tables.course.table-strategy.hint.algorithm-class-name=com.mine.sharding.algorithm.MyHintTableShardingAlgorithm
2.5.2 自定义算法类
public class MyHintTableShardingAlgorithm implements HintShardingAlgorithm<Integer> {
/**
* Hint限制:
* 不支持union查询
* 不支持多层子查询
* 不支持函数计算
*/
@Override
public Collection<String> doSharding(Collection<String> collection, HintShardingValue<Integer> hintShardingValue) {
String key = hintShardingValue.getLogicTableName() + hintShardingValue.getValues().toArray()[0];
if (collection.contains(key)) {
return Arrays.asList(key);
}
throw new UnsupportedOperationException("route:" + key + " is not supported,please check your config");
}
}
2.6.3 测试类
Hint算法如何在程序中干预呢,我们写一个简单的测试方法
我们可以看到,是通过HintManager来指定,很方便
但是也有一点需要注意的地方,这个是线程安全的,用完之后得关掉,不能带到一下个线程里面。
/**
* Hint
* 强制路由
*/
@Test
public void queryCourseLHint() {
HintManager hintManager = HintManager.getInstance();
hintManager.addTableShardingValue("course", 0);
List<Course> courses = courseMapper.selectList(null);
courses.forEach(System.out::println);
hintManager.close();
}
2.6 广播表实践
在所有的数据源里面,做同样的操作,就是说每个数据源都会保存同样的,全量的数据
spring.shardingsphere.sharding.broadcast-tables=dict
2.7 绑定表实践
对分片键相同的表做关联查询,最主要就是要避免笛卡尔积
spring.shardingsphere.sharding.binding-tables[0]=user,dict
2.8 读写分离实践
前提,在数据库层面配置好主从库
6.8.1 配置主从库数据源
2.8.2 配置主从节点读写分离
当然,这里也需要指定主键及生成算法
#主从配置、读写分离
spring.shardingsphere.sharding.master-slave-rules.ds0.master-data-source-name=m
spring.shardingsphere.sharding.master-slave-rules.ds0.slave-data-source-names=s
spring.shardingsphere.sharding.tables.student.actual-data-nodes=ds0.student
spring.shardingsphere.sharding.tables.student.key-generator.column=sid
spring.shardingsphere.sharding.tables.student.key-generator.type=SNOWFLAKE
这次的内容到这里就结束了,作者水平有限,文章不足之处敬请指出
Best Regards