ShardingSphere应用详解

499 阅读22分钟

ShardingSphere应用详解

引言

以互联网系统中常用的MySQL数据库为例,虽然单表存储的数据原则上能够达到上亿条级别,但这时访问性能就会变得很差。即使采用各种调优策略,通常效果也微乎其微。业界普遍认为,MySQL数据库的单表容量在1000万条以下是一种最佳状态,一旦超过这个量级,就需要考虑采用其他方案。

分库分表

针对关系型数据库,我们可以考虑采用分库分表的方案来解决单表瓶颈问题,这是目前互联网行业处理海量数据的通用方法。分库分表方案更多的是对关系型数据库数据存储和访问机制的一种补充,而不是颠覆。

从定义上讲,垂直分库是指按照业务将表进行分类,然后分布到不同的数据库上。每个库可以位于不同的服务器上,其核心理念是专库专用。而从实现上讲,垂直分库的实现在很大程度上取决于业务的规划和系统边界的划分。例如,用户数据的独立拆分就需要考虑到系统用户体系与其他业务模块之间的关联关系,而不是说简单地创建一个用户库即可。在高并发场景下,垂直分库能够在一定程度上提高I/O访问效率和数据库连接次数,并解决单机硬件资源的瓶颈问题。

尽管垂直拆分实现起来比较简单,但并不能解决单表数据量过大这一核心问题。所以在实际应用中,我们往往需要在垂直拆分的基础上添加水平拆分。例如,我们可以对用户库中的用户信息按照用户ID进行取模,然后分别存储在不同的数据库中,这就是水平分库的常见做法,如图所示。

可以看到,水平分库是把同一个表中的数据按照一定规则拆分到不同的数据库中,每个数据库同样可以位于不同的服务器上。这种方案往往能解决单库存储量及性能瓶颈问题,但由于同一个表被分配在不同的数据库中,访问数据需要额外的路由工作,大大提高了系统复杂度。这里所谓的“规则”实际上就是一系列的算法,常见的算法如下。

(1)取模算法

取模的方式有很多,如前文介绍的按照用户 ID 进行取模,也可以通过表的一列字段进行hash求值来取模。

(2)范围限定算法

范围限定算法也很常见,如可以按照年份、时间等策略路由到目标数据库或表。

(3)预定义算法

预定义算法是指事先规划好具体数据库或表的数量,然后直接路由到指定数据库或表中。

按照水平分库的思路,我们也可以对用户库中的用户表进行水平拆分,其示意图如图所示。也就是说,水平分表是在同一个数据库内,把同一个表的数据按照一定规则拆分到多个表中。

读写分离

读写分离是解决数据访问瓶颈的另一个技术体系。读写分离与数据库主从架构有关。MySQL数据库提供了完善的主从架构,能够确保主数据库(主库)与从数据库(从库)之间的数据同步,基于主从架构,我们就可以按照操作要求对读操作和写操作进行分离,从而提高数据库的访问效率。读写分离的基本原理图如图所示。

从上图可以看到,数据库集群有一个主库和一个从库,主库和从库之间通过同步机制实现两者数据的一致性。在互联网系统中,普遍认为对数据库的读操作的频率要远远高于写操作,所以瓶颈往往出现在读操作上。通过读写分离,我们就可以把读操作分离出来并在独立的从库上进行。在现实中的主从架构中,主库和从库的数量,尤其是从库的数量,都可以根据数据量的大小进行扩充。

读写分离主要解决的是高并发下的数据库访问其也是一种常用的解决方案,但并不是终极解决方案。终极解决方案还是要分库分表,我们按照用户ID等规则进行分库或分表。分库分表与读写分离之间的关系并不是互斥的,而是相辅相成的,我们完全可以在分库分表的基础上引入读写分离机制。分库分表整合读写分离结构图如图所示。

分库分表解决方案和代表性框架

当数据库引入分库分表和读写分离,系统的数据存储架构就会变得复杂。与拆分前的单库单表相比,我们面临着以下问题。

● 如何对多数据库进行高效治理

● 如何进行跨节点关联查询

● 如何实现跨节点的分页和排序操作

● 如何生成全局唯一的主键

● 如何确保事务一致性

● 如何对数据进行迁移等

如果没有很好的工具来支持数据存储和访问,那么数据一致性将很难得到保障,这就需要引出实现分库分表的主流解决方案和代表性框架。

基于前面关于分库分表的讨论,我们可以将其抽象为一个核心概念,这个概念就是分片(Sharding),即无论是分库还是分表,都是把数据划分成不同的数据片,并存储在不同的目标对象中。而具体的分片方式就会涉及实现分库分表的不同解决方案。

如果要列举业界关于分库分表的框架,大致分成三大类型,即客户端分片、代理服务器分片及分布式数据库。

1.客户端分片

所谓客户端分片,相当于在数据库的客户端就完成了分片规则的实现。显然,这种方式将分片管理的工作进行了前置,客户端管理维护所有的分片逻辑,并决定每次执行SQL语句所对应的目标数据库和数据表。

客户端分片这一解决方案也有不同的表现形式,其中最为简单的方式就是应用层分片,也就是说在应用程序中直接维护分片信息。客户端分片结构图如图。

在具体实现上,客户端分片在实现上通常会进行进一步的抽象,其方法是重写JDBC协议,也就是说在JDBC协议层面嵌入分片规则。这样,业务开发人员还是使用与JDBC规范完全兼容的一套API来操作数据库,但这套API自动完成了分片操作,从而实现对业务代码的零侵入。基于JDBC规范重写机制的客户端分片结构图如图。

对于客户端分片,典型的中间件包括阿里巴巴的 TDDL 及ShardingSphere。因为TDDL并没有开源,无法知道其使用了哪种客户端分片方案。而对ShardingSphere来说,它是重写JDBC规范以实现客户端分片的典型实现框架。

2. 代理服务器分片

代理服务器分片的解决方案也比较明确,就是采用了代理机制,也就是说在应用层和数据库层之间添加一个代理层。有了代理层之后,我们就可以把分片规则集中维护在代理层中,对外提供与JDBC兼容的API并给到应用层。这样,应用层的业务开发人员就不用关心具体的分片规则,而只需要完成业务逻辑的实现。代理服务器分片结构图如图所示。

代理服务器分片的优点是解决了业务开发人员对分片规则的管理工作,缺点是添加了一层代理层,所以带来了一些问题,比如,因为新增了一层网络传输对性能所产生的影响。

对于代理服务器分片,常见的开源框架有Cobar及Mycat。而在 ShardingSphere 3.X版本中,也添加了Sharding-Proxy模块来实现代理服务器分片。

3. 分布式数据库

在技术发展过程中,关系型数据库的主要问题在于缺乏分布式特性,也就说,缺乏在分布式环境下对大数量、高并发访问的有效数据处理机制。例如,我们知道事务是关系型数据库的本质特征之一,但在分布式环境下,如果想要基于MySQL等传统关系型数据库来实现事务,则会面临巨大的挑战。

幸好,以TiDB为代表的分布式数据库的兴起赋予了关系型数据库一定程度的分布式特性。在这些分布式数据库中,数据分片及分布式事务将是其内置的基础功能,对业务开发人员而言是完全透明的。业务开发人员只需要使用TiDB对外提供的JDBC 接口,就像使用MySQL等传统关系型数据库一样。

从这个角度讲,我们也可以认为ShardingSphere是一种分布式数据库中间件。它除了提供标准化的数据分片解决方案,也实现了分布式事务和数据库治理功能。

ShardingSphere

ShardingSphere功能列表

ShardingSphere结构

  1. 图中黄色部分表示的是Sharding-JDBC的入口API,采用工厂方法的形式提供。 目前有ShardingDataSourceFactory和MasterSlaveDataSourceFactory两个工厂类。 ShardingDataSourceFactory支持分库分表、读写分离操作 MasterSlaveDataSourceFactory支持读写分离操作

  2. 图中蓝色部分表示的是Sharding-JDBC的配置对象,提供灵活多变的配置方式。ShardingRuleConfiguration是分库分表配置的核心和入口,它可以包含多个TableRuleConfiguration和MasterSlaveRuleConfiguration。 TableRuleConfiguration封装的是表的分片配置信息,有5种配置形式对应不同的Configuration类型。MasterSlaveRuleConfiguration封装的是读写分离配置信息。

  3. 图中红色部分表示的是内部对象,由Sharding-JDBC内部使用,应用开发者无需关注。Sharding-JDBC通过ShardingRuleConfiguration和MasterSlaveRuleConfiguration生成真正供ShardingDataSource和MasterSlaveDataSource使用的规则对象。ShardingDataSource和MasterSlaveDataSource实现了DataSource接口,是JDBC的完整实现方案。

ShardingRuleConfiguration所需要配置的规则比较多,我们可以通过一张图片进行简单说明,并列举了每个配置项的名称、类型及个数关系,如图。

  1. TableRuleConfiguration

从命名上看,TableRuleConfiguration是表分片规则配置,但事实上这个类同时包含了对分库和分表两种场景的设置。TableRuleConfiguration包含很多重要的配置项。

(1)actualDataNodes

actualDataNodes表示真实的数据节点,由数据源名+表名组成,支持行表达式。例如,ds${0.1}user${0.1}就是比较典型的一种配置方式。

(2)databaseShardingStrategyConfig

databaseShardingStrategyConfig表示分库策略,缺省表示使用默认分库策略,这里的默认分库策略就是 ShardingRuleConfiguration 中的 defaultDatabaseShardingStrategyConfig 配置。

(3)tableShardingStrategyConfig

和 databaseShardingStrategyConfig一样,tableShardingStrategyConfig表示分表策略,缺省表示使用默认分表策略,这里的默认分表策略同样来自 ShardingRuleConfiguration 中的defaultTableShardingStrategyConfig配置。

(4)keyGeneratorConfig

keyGeneratorConfig表示分布式环境下的自增列生成器配置。ShardingSphere集成了雪花算法等分布式ID的生成器实现。

  1. ShardingStrategyConfiguration

databaseShardingStrategyConfig 和 tableShardingStrategyConfig 的类型都是一个ShardingStrategyConfiguration对象。在ShardingSphere中,ShardingStrategyConfiguration 实际上是一个空接口,具有一系列的实现类,每个实现类都表示一种分片的具体策略,如图2-4所示。

对ShardingStrategyConfiguration来说,通常需要指定一个分片列shardingColumn 及一个或多个分片算法 ShardingAlgorithm。当然也有例外,例如 HintSharding-StrategyConfiguration直接使用数据库的Hint机制实现强制路由,而不需要分片列。

  1. KeyGeneratorConfiguration

对一个自增列来说,KeyGeneratorConfiguration 先要指定一个列名 column。同时,因为ShardingSphere内置了一些自增列的实现机制,如雪花算法(SnowFlake)及通用唯一识别码(UUID),所以可以通过一个type配置项讲行指定。最后,我们可以利用Properties 配置项指定自增值生成过程中所需要的相关属性配置。

  1. 配置读写分离

在数据库主从架构中,因为从库一般会有多个,所以当执行一条面向从库的 SQL 语句时,需要实现一套负载均衡机制来完成对目标丛库的路由。ShardingSphere 默认提供了随机(Random)和轮询(RoundRobin)两种负载均衡算法来完成这一目标。

另外,由于主库和从库之间存在一定的同步时延和数据不一致情况,所以在有些场景下,我们可能更希望从主库中获取最新的数据。ShardingSphere同样考虑到了这方面的需求,可以通过Hint机制来实现对主库的强制路由。

在 ShardingSphere 中,通过配置获取支持读写分离的MasterSlaveDataSource,而MasterSlaveDataSource的创建依赖于MasterSlaveDataSourceFactory工厂类,代码如下

public final class MasterSlaveDataSourceFactory {
    public static DataSource createDataSource(final Map<String, DataSource> dataSourceMap, 
        final MasterSlaveRuleConfiguration masterslaveRuleConfig,final Properties props)throws SQLException {
    return new MasterSlaveDataSource(dataSourceMap, new MasterSlaveRule(masterSlaveRuleConfig), props);
}

可以看到,在createDataSource()方法中传入了3个参数,除了熟悉的dataSourceMap 和props,还有一个MasterSlaveRuleConfiguration,而MasterSlaveRuleConfiguration包含了所有我们需要配置的读写分离信息,代码如下

Public class MasterSlaveRuleConfiguration implements RuleConfiguration//读写分离数据源名称
    private final String name;//主库数据源名称
    private final String masterDataSourceName;
}

ShardingSphere中表概念

  1. 真实表 数据库中真实存在的物理表。例如b_order0、b_order1
  2. 逻辑表 相同结构的水平拆分数据库(表)的逻辑名称,是 SQL 中表的逻辑标识。 例:订单数据根据主键尾数拆分为 10 张表,分别是 t_order_0 到 t_order_9,他们的逻辑表名为 t_order
  3. 数据节点 数据分片的最小单元,由数据源名称和真实表组成。 例:ds_0.t_order_0。 逻辑表与真实表的映射关系,可分为均匀分布和自定义分布两种形式。
  4. 绑定表

指分片规则一致的一组分片表。 使用绑定表进行多表关联查询时,必须使用分片键进行关联,否则会出现笛卡尔积关联或跨库关联,从而影响查询效率。 例如:t_order 表和 t_order_item 表,均按照 order_id 分片,并且使用 order_id 进行关联,则此两张表互为绑定表关系。 绑定表之间的多表关联查询不会出现笛卡尔积关联,关联查询效率将大大提升。 举例说明,如果 SQL 为:

SELECT i.* FROM t_order o JOIN t_order_item i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

在不配置绑定表关系时,假设分片键 order_id 将数值 10 路由至第 0 片,将数值 11 路由至第 1 片,那么路由后的 SQL 应该为 4 条,它们呈现为笛卡尔积:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_0 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

在配置绑定表关系,并且使用 order_id 进行关联后,路由的 SQL 应该为 2 条:

SELECT i.* FROM t_order_0 o JOIN t_order_item_0 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);
SELECT i.* FROM t_order_1 o JOIN t_order_item_1 i ON o.order_id=i.order_id WHERE o.order_id in (10, 11);

需要注意的是,如果想要达到这种效果,则互为绑定表的各个表的分片键要完全相同。例如,在上面的这些SQL语句中,我们不难看出,这个需要完全相同的分片键就是record_id。

  1. 广播表 广播表(BroadcastTable)全局表,也就是它会存在于多个库中冗余,避免跨库查询问题。比如省份、字典等一些基础数据,为了避免分库分表后关联表查询这些基础数据存在跨库问题,所以可以把这些数据同步给每一个数据库节点,这个就叫广播表。

ShardingSphere数据分片实现

分片策略与分片算法

分片策略是 ShardingSphere 分片引擎的核心概念,直接影响最终的路由结果,而分片策略的实施又依赖于分片算法。

我们先来看一下分片策略ShardingStrategy的定义。ShardingStrategy位于sharding-core-common代码工程的org.apache.shardingsphere.core.strategy.route包中,其定义代码如下

public interface ShardingStrategy {
    //获取分片
    Collection getshardingColumns();
    //执行分片
    collection doSharding(CollectionavailableTargetNames, Collection shardingValues);
}

ShardingStrategy 包含两个核心方法一个用于指定分片的Column,另一个负责执行分片并返回目标DataSource和Table。ShardingSphere为我们提供了一系列的分片策略实例,类层结构如图所示。

每个ShardingStrategy 中都会包含另一个与路由相关的核心概念,即分片算法 ShardingAlgorithm。ShardingAlgorithm 是一个空接口,它包含了4个继承接口,PreciseShardingAlgorithm接口、RangeShardingAlgorithm接口、ComplexKeysSharding-Algorithm 接口和HintShardingAlgorithm 接口,这4个接口又分别具有一些实现类。

需要注意的是,ShardingStrategy与ShardingAlgorithm之间并不是一对一的对应关系。在一个ShardingStrategy中,可以同时使用多个ShardingAlgorithm来完成具体的路由执行策略。ShardingStrategy和ShardingAlgorithm的类层结构关系如图所示。

从关系上讲,分片策略包含了分片算法和分片键,即:

分片策略 = 分片算法 + 分片键

分片键

  用于分片的数据库字段,是将数据库(表)水平拆分的关键字段。例:将订单表中的订单主键的尾数取模分片,则订单主键为分片字段。 SQL中如果无分片字段,将执行全路由,性能较差。 除了对单分片字段的支持,ShardingSphere也支持根据多个字段进行分片。

分片算法(ShardingAlgorithm)

  由于分片算法和业务实现紧密相关,因此并未提供内置分片算法,而是通过分片策略将各种场景提炼出来,提供更高层级的抽象,并提供接口让应用开发者自行实现分片算法。目前提供4种分片算法。

  1. 精确分片算法PreciseShardingAlgorithm

用于处理使用单一键作为分片键的=与IN进行分片的场景。

  1. 范围分片算法RangeShardingAlgorithm

用于处理使用单一键作为分片键的BETWEEN AND、>、<、>=、<=进行分片的场景。

  1. 复合分片算法ComplexKeysShardingAlgorithm

用于处理使用多键作为分片键进行分片的场景,多个分片键的逻辑较复杂,需要应用开发者自行处理其中的复杂度。

  1. Hint分片算法HintShardingAlgorithm

用于处理使用Hint行分片的场景。对于分片字段非SQL决定,而由其他外置条件决定的场景,可使用SQL Hint灵活的注入分片字段。例:内部系统,按照员工登录主键分库,而数据库中并无此字段。SQL Hint支持通过Java API和SQL注释两种方式使用。

分片策略

  包含分片键和分片算法,由于分片算法的独立性,将其独立抽离。真正可用于分片操作的是分片键 + 分片算法,也就是分片策略。目前提供5种分片策略。

  1. 标准分片策略(Standard-ShardingStrategy)

是ShardingSphere最常用的一种分片策略,提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。StandardShardingStrategy只支持单分片键,提供PreciseShardingAlgorithm和RangeShardingAlgorithm两个分片算法。PreciseShardingAlgorithm是必选的,用于处理=和IN的分片。RangeShardingAlgorithm是可选的,用于处理BETWEEN AND, >, <, >=, <=分片,如果不配置RangeShardingAlgorithm,SQL中的BETWEEN AND将按照全库路由处理。

  1. 复合分片策略(ComplexShardingStrategy)

复合分片策略。提供对SQL语句中的=, >, <, >=, <=, IN和BETWEEN AND的分片操作支持。ComplexShardingStrategy支持多分片键,由于多分片键之间的关系复杂,因此并未进行过多的封装,而是直接将分片键值组合以及分片操作符透传至分片算法,完全由应用开发者实现,提供最大的灵活度。

  1. 行表达式分片策略(InlineShardingStrategy)

使用Groovy的表达式,提供对SQL语句中的=和IN的分片操作支持,只支持单分片键。对于简单的分片算法,可以通过简单的配置使用,从而避免繁琐的Java代码开发,如: t_user_$->{u_id % 8} 表示t_user表根据u_id模8,而分成8张表,表名称为t_user_0到t_user_7。

  1. Hint分片策略(HintShardingStrategy)

通过Hint指定分片值而非从SQL中提取分片值的方式进行分片的策略。

  1. 不分片策略(NoneShardingStrategy)

不分片的策略。

分片策略配置

对于分片策略存有数据源分片策略和表分片策略两种维度,两种策略的API完全相同。

  1. 数据源分片策略

用于配置数据被分配的目标数据源。

  1. 表分片策略

用于配置数据被分配的目标表,由于表存在与数据源内,所以表分片策略是依赖数据源分片策略结果的。

强制路由与Hint机制

强制路由与一般的分库分表路由的不同之处在于,它并没有使用任何的分片键和分片策略。我们知道通过解析SQL语句提取分片键并设置分片策略进行分片是ShardingSphere 对重写JDBC规范的实现方式。如果没有分片键,则只能访问所有的数据库和数据表进行全路由。显然,这种处理方式也不大合适。有时,我们需要为执行SQL语句开一个“后门”,允许在没有分片键的情况下同样可以在外部设置目标数据库和数据表,这就是强制路由的设计理念。

Apache ShardingSphere 使用 ThreadLocal 管理分片键值进行强制路由。可以通过编程的方式向HintManager 中添加分片值,该分片值仅在当前线程内生效。 Apache ShardingSphere 还可以通过 SQL 中增加注释的方式进行强制路由。

Hint 的主要使用场景:

  • 分片字段不存在 SQL 和数据库表结构中,而存在于外部业务逻辑。
  • 读写分离操作,如果强制在主库进行某些数据操作,比如需实时查询数据,避免主从同步数据延迟。
  • 强制在指定数据库进行某些数据操作。

使用 Hint 分片

  1. 编写Hint算法类
// 泛型 Long 代表传入的 参数类型为 Long
public class MyHintShardingAlgorithm implements HintShardingAlgorithm<Long> {
    @Override
    public Collection<String> doSharding(
            Collection<String> availableTargetNames,
            HintShardingValue<Long> shardingValue) {
        // 添加分库或分表路由逻辑
        Collection<String> result = new ArrayList<>();
        for (String each : availableTargetNames){ //代表:分片目标,对哪些数据库、表分片。如果是对分库路由,表示ds0,ds1;
            for (Long value : shardingValue.getValues()){ // 代表:分片值; 可以HintManager设置多个分片值,所以是个集合。
                if(each.endsWith(String.valueOf(value % 2))){ // 分库路由,只需要模2,指定是路由到ds0库,还是ds1库
                    result.add(each);
                }
            }
        }
        return result;
    }
}
  1. 配置规则 Hint 分片算法需要用户实现 org.apache.shardingsphere.sharding.api.sharding.hint.HintShardingAlgorithm 接口。 Apache ShardingSphere 在进行路由时,将会从 HintManager 中获取分片值进行路由操作。
spring:
  shardingsphere:
    datasource: xxx
    sharding:
      tables:
        city:
          database-strategy:
            hint:
              algorithm-class-name: com.demo.hint.MyHintShardingAlgorithm
  1. 获取 HintManager HintManager hintManager = HintManager.getInstance(); 并添加分片键值 • 使用 hintManager.addDatabaseShardingValue 来添加数据源分片键值。 • 使用 hintManager.addTableShardingValue 来添加表分片键值。 分库不分表情况下,强制路由至某一个分库时,可使用hintManager.setDatabaseShardingValue方式添加分片。
  2. 清除分片键值 分片键值保存在 ThreadLocal 中,所以需要在操作结束时调用 hintManager.close() 来清除 Thread‐Local 中的内容。 hintManager 实现了 AutoCloseable 接口,可推荐使用 try with resource 自动关闭

 在业务代码中执行查询前使用HintManager指定执行策略值

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RunBoot.class)
public class TestHintAlgorithm {

    @Resource
    private CityRepository cityRepository;

    @Test
    public void test1(){
        HintManager hintManager = HintManager.getInstance();
        // 只对库路由,则只需要hintManager.setDatabaseShardingValue操作
        hintManager.setDatabaseShardingValue(1L); //强制路由到ds${xx%2}
        List<City> list = cityRepository.findAll();
        list.forEach(city->{
            System.out.println(city.getId()+" "+city.getName()+" "+city.getProvince());
        });
    }
}

补充示例


// Sharding database and table with using HintManager
String sql = "SELECT * FROM t_order";
try (HintManager hintManager = HintManager.getInstance();
    Connection conn = dataSource.getConnection();
    PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
    hintManager.addDatabaseShardingValue("t_order", 1);
    hintManager.addTableShardingValue("t_order", 2);
    try (ResultSet rs = preparedStatement.executeQuery()) {
        while (rs.next()) {
        // ...
        }
    }
}
// Sharding database without sharding table and routing to only one database with using HintManager
String sql = "SELECT * FROM t_order";
try (HintManager hintManager = HintManager.getInstance();
    Connection conn = dataSource.getConnection();
    PreparedStatement preparedStatement = conn.prepareStatement(sql)) {
    hintManager.setDatabaseShardingValue(3);
    try (ResultSet rs = preparedStatement.executeQuery()) {
        while (rs.next()) {
        // ...
        
        }
    }
}
分片配置示例
spring:
  shardingsphere:
    props:
      sql:
        show: '# 是否开启 SQL 显示,默认值: false'

    datasource:
      names: '# 数据源名称,多数据源以逗号分隔'

      <data-source-name>:
        driver-class-name: '# 数据库驱动类名'
        password: '# 数据库密码'
        type: '# 数据库连接池类名称'
        url: '# 数据库 url 连接'
        username: '# 数据库用户名'
        xxx: '# 数据库连接池的其它属性'

    # 分片配置
    sharding:
      tables:
        <logic-table-name>:
          actual-data-nodes: '# 由数据源名 + 表名组成,以小数点分隔。多个表以逗号分隔,支持 inline 表达式。缺省表示使用已知数据源与逻辑表名称生成数据节点,用于广播表(即每个库中都需要一个同样的表用于关联查询,多为字典表)或只分库不分表且所有库的表结构完全一致的情况'
          # 分库策略,缺省表示使用默认分库策略,以下的分片策略只能选其一
          database-strategy:
            # 用于单分片键的标准分片场景
            standard:
              sharding-column: '# 分片列名称'
              precise-algorithm-class-name: '# 精确分片算法类名称,用于 = 和 IN。该类需实现PreciseShardingAlgorithm接口并提供无参数的构造器'
              range-algorithm-class-name: '# 范围分片算法类名称,用于 BETWEEN,可选。该类需实现RangeShardingAlgorithm接口并提供无参数的构造器'
            # 用于多分片键的复合分片场景
            complex:
              sharding-columns: '# 分片列名称,多个列以逗号分隔'
              algorithm-class-name: '# 复合分片算法类名称。该类需实现 ComplexKeysShardingAlgorithm接口并提供无参数的构造器'
            # 行表达式分片策略
            inline:
              sharding-column: '# 分片列名称'
              algorithm-expression: '# 分片算法行表达式,需符合 groovy 语法'
            # Hint 分片策略
            hint:
              algorithm-class-name: '# Hint 分片算法类名称。该类需实现 HintShardingAlgorithm 接口并提供无参数的构造器'

          # 分表策略,同分库策略
          table-strategy:
            xxx: '# 省略'

          key-generator:
            column: '# 自增列名称,缺省表示不使用自增主键生成器'
            type: '# 自增列值生成器类型,缺省表示使用默认自增列值生成器。可使用用户自定义的列值生成器或选择内置类型:SNOWFLAKE/UUID'

      # 绑定表规则列表
      binding-tables:
        - '# 绑定表规则列表'
        - '# 绑定表规则列表'

      # 广播表规则列表
      broadcast-tables:
        - '# 广播表规则列表'
        - '# 广播表规则列表'

分布式主键

ShardingSphere不仅提供了内置的分布式主键生成器,例如UUID、SNOWFLAKE,还抽离出分布式主键生成器的接口,方便用户自行实现自定义的自增主键生成器。

内置主键生成器:

  1. UUID

采用UUID.randomUUID()的方式产生分布式主键。

  1. SNOWFLAKE(SnowflakeShardingKeyGenerator)

在分片规则配置模块可配置每个表的主键生成策略,默认使用雪花算法,生成64bit的长整型数据,是 ShardingSphere 默认的分布式主键生成策略。

SnowFlake算法是Twitter开源的分布式ID生成算法,其核心思想是使用一个64bit 的long型的数字作为全局唯一ID,且ID引入了时间戳,基本能够保持自增。SnowFlake 算法在分布式系统中的应用十分广泛,SnowFlake算法中的64bit 详细结构具有一定的规范,如图。

PS: 行表达式(Line Expression)

行表达式是 ShardingSphere 一种用于实现简化和统一配置信息的工具,在日常开发过程中的应用非常广泛。它的使用方式非常直观,只需要在配置中使用${expression}表达室或$->{expression}表达式即可。例如,ds${0.1}user${0.1}就是一个行表达式,用来设置可用的数据源或数据表名称。基于行表达式语法,${begin.end}表示的是一个从begin到end的范围,而多个${expression}之间可以用"."符号进行连接,表示多个表达式数值之间的一种笛卡儿积关系。如果采用图形化的表现形式,则ds${0.1}user${0.1}表达式最终会被解析成如图所示的结果。

读写分离

读写分离配置示例

spring:
  shardingsphere:
    props:
      sql:
        show: '# 是否开启 SQL 显示,默认值: false'

    datasource:
      names: '# 数据源名称,多数据源以逗号分隔'
      <data-source-name>:
        driver-class-name: '# 数据库驱动类名'
        password: '# 数据库密码'
        type: '# 数据库连接池类名称'
        url: '# 数据库 url 连接'
        username: '# 数据库用户名'
        xxx: '# 数据库连接池的其它属性'

    # 分片配置
    sharding:
      # 读写分离配置
      master-slave-rules:
        <master-slave-data-source-name>:
          master-data-source-name: '# 主库数据源名称'
          slave-data-source-names:
            - '# 从库数据源名称列表'
            - '# 从库数据源名称列表'
          load-balance-algorithm-class-name: '# 从库负载均衡算法类名称。该类需实现MasterSlaveLoadBalanceAlgorithm接口且提供无参数构造器'
          load-balance-algorithm-type: '# 从库负载均衡算法类型,可选值: ROUND_ROBIN, RANDOM。若`load-balance-algorithm-class-name`存在则忽略该配置'

敏感数据加解密

数据脱敏本质上就是一种加解密技术的应用场景,自然少不了对各种加解密算法和技术的封装。传统的加解密方式有两种一种是对称加密,如 DEA 和 AES另一种是非对称加密,如RSA。

ShardingSphere 内部也抽象了一个 ShardingEncryptor 组件专门封装各种加解密操作,代码如下

public interface ShardingEncryptor extends TypeBasedSPI {
    //初始化
    void init();
    //加密
    String encrypt(Object plaintext);
    //解密
    Object decrypt(String ciphertext);
}

目前,ShardingSphere 内置了 AESShardingEncryptor和 MD5ShardingEncryptor 两个具体的ShardingEncryptor实现类。因为ShardingEncryptor扩展了TypeBasedSPI接口,所以开发人员完全可以基于微内核架构模式和JDK所提供的SPI机制来实现与动态加载自定义的各种ShardingEncryptor。

配置项说明

spring:
  shardingsphere:
    props:
      query:
        with:
          cipher:
            column: true # 查询是否使用密文列
    # 数据脱敏
    encrypt:
      encryptors:
        <encryptor-name>:
          type: '# 加解密器类型,可自定义或选择内置类型: MD5/AES'
          props:
            <property-name>: '#属性配置,注意:使用 AES 加密器,需要配置 AES 加密器的 KEY 属性: aes.key.value'
      tables:
        <table-name>:
          columns:
            <logic-column-name>:
              plainColumn: '# 存储明文的字段'
              cipherColumn: '# 存储密文的字段'
              assistedQueryColumn: '# 辅助查询字段,针对 ShardingQueryAssistedEncryptor 类型的加解密器进行辅助查询'
              encryptor: '# 加密器名字'

具体配置示例

spring:
  shardingsphere:
    # 属性配置
    props:
      # 是否开启 SQL 显示,默认值: false
      sql:
        show: true
    # 数据源配置,可配置多个
    datasource:
      # 本案例中配置了两个数据源,分别对应刚才创建的两个 MySQL 容器
      names: ds0,ds1
      ds0:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://192.168.116.131:3310/db_device_0?useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong&useSSL=false
        username: root
        password: '123456'
      ds1:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://192.168.116.131:3311/db_device_0?useUnicode=true&characterEncoding=utf-8&serverTimezone=Hongkong&useSSL=false
        username: root
        password: '123456'
        hikari:
          maximum-pool-size: 20
    # 分片规则配置
    sharding:
      # 绑定表规则列表
      binding-tables: tb_order,tb_order_item
      # 默认数据库分片策略,同分库策略
      default-database-strategy:
        inline:
          # 分片算法行表达式,需符合 groovy 语法
          # 此处根据 user_id 分片
          # 如果 user_id 为奇数则落入奇数库即 ds1 匹配的数据源
          # 如果 user_id 为偶数则落入偶数库即 ds0 匹配的数据源
          algorithm-expression: ds$->{user_id % 2}
          # 分片列名称
          sharding-column: user_id
      # 数据分片规则配置,可配置多个
      tables:
        # 逻辑表名称
        tb_order:
          # 由数据源名 + 表名组成,以小数点分隔
          actual-data-nodes: ds$->{0..1}.tb_order_$->{0..1}
          # 分表策略,同分库策略
          table-strategy:
            inline:
              # 此处根据 order_id 分片
              # 如果 order_id 为奇数则落入奇数表即 tb_order_1
              # 如果 order_id 为偶数则落入偶数表即 tb_order_0
              algorithm-expression: tb_order_$->{order_id % 2}
              # 分片列名称
              sharding-column: order_id
            key-generator:
              column: id
              type: SNOWFLAKE

        tb_order_item:
          actual-data-nodes: ds$->{0..1}.tb_order_item_$->{0..1}
          table-strategy:
            inline:
              algorithm-expression: tb_order_item_$->{order_id % 2}
              sharding-column: order_id

      encryptRule:
        encryptors:
          encryptor_aes:
            type: aes
            props:
              aes.key.value: "xxxxx"
        tables:
          tb_order:
            columns:
              the_identity:
                cipherColumn: the_identity
                encryptor: encryptor_aes

注意这里几个可以配置默认的项, 这几项配置了,在表的配置里如果不配置,则用的是默认配置项。

default-datasource-name: # 未配置分片规则的表将通过默认数据源定位
default-database-strategy: # 默认数据库分片策略
default-table-strategy: # 默认表分片策略
default-key-generator: # 默认的主键生成算法,如果没有设置,默认为 SNOWFLAKE 算法

自定义分片算法TODO

ShardingSphere中的分布式事务

关于如何实现分布式事务,业界也存在一些通用的实现机制。针对不同的实现机制,也诞生了一些供应商和开发工具。因为这些开发工具在使用方式上和实现原理上都有较大的差异性,所以开发人员的诉求在于,希望有一套统一的解决方案能够屏蔽这些差异。在设计上,ShardingSphere从一开始就充分考虑到了开发人员的这些诉求。

1. ShardingSphere中的事务类型

在 ShardingSphere 中,所支持的事务类型定义代码如下

public enum TransactionType {
    LOCAL,XA,BASE
}

可以看到,除了本地事务,还提供了针对分布式事务的两种实现方案,分别是XA 事务和柔性事务。

XA 事务提供基于两阶段提交协议的实现机制。所谓两阶段提交,就是分成两个阶段一个是准备阶段另一个是执行阶段。在准备阶段中,协调者发起一个提议分别询问各参与者是否接受在执行阶段中,协调者根据参与者的反馈,提交或终止事务。如果参与者全部同意则提交事务,只要有一个参与者不同意就终止事务,如图所示。

目前,关于如何实现XA事务,业界也提供了一些主流的工具库,包括Atomikos、Narayana 和 Bitronix。ShardingSphere 对这三种工具库都进行了集成,并默认使用Atomikos来完成两阶段提交。

XA事务是典型的强一致性事务,也就是完全遵循事务的ACID设计原则。与XA 事务的“刚性”不同,柔性事务则遵循BASE设计原则,追求的是最终一致性。这里的 BASE 来源于基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventual Consistency)3个概念。

关于如何实现基于BASE设计原则的柔性事务,业界也提供了一些优秀的框架,如阿里巴巴的Seata。ShardingSphere内部也集成了对Seata的支持。当然,我们也可以根据需要集成其他分布式事务类开源框架,并基于微内核架构模式嵌入shardingsphere运行环境中。

2. ShardingSphere中的事务抽象

在 ShardingSphere 中,抽象了一个分片事务管理器 ShardingTransactionManager。ShardingTransactionManager接口位于sharding-transaction-core代码工程的org.apache. shardingsphere.transaction.spi包中,代码如下∶

public interface ShardingTransactionManager extends AutoCloseable{
    //根据数据库类型和ResourceDataSource进行初始化
    void init(DatabaseType databaseType, Collection resourceDataSources);
    //获取TransactionType
    TransactionType getTransactionType();
    //判断是否在事务中
    boolean isInTransaction();
    //获取支持事务的Connection
    Connection getConnection(String dataSourceName) throws SQLException;
    //开始事务
    void begin();
    //提交事务
    void commit();
}

1. XA事务的基本概念和原理

XA是由X/Open组织提出的两阶段提交协议,是一种分布式事务的规范。XA规范主要定义了面向全局的事务管理器TransactionManager(TM)和面向局部的资源管理器ResourceManager(RM)之间的接口。XA 接口是双向的系统接口,在TransactionManager及一个或多个ResourceManager之间形成通信桥梁。通过这样的设计,TransactionManager控制着全局事务,管理事务生命周期,并协调资源。而ResourceManager负责控制和管理包括数据库相关的各种实际资源。XA的整体结构及TransactionManager和ResourceManager之间的交互过程如图所示。

2.BASE 柔性事务实现方案

ShardingSphere实现的BASE柔性事务是基于阿里巴巴的Seata框架。与XA不同,Seata框架中的一个分布式事务包含3个角色,除了XA中的TransactionManagg (TM)和ResourceManager(RM),还有一个事务协调器TransactionCoordinator(TC)。

  • Transaction Coordinator (TC): 事务协调器,维护全局事务的运行状态,负责协调并驱动全局事务的提交或回滚。
  • Transaction Manager (TM): 控制全局事务的边界,负责开启一个全局事务,并最终发起全局提交或全局回滚的决议。
  • Resource Manager (RM): 控制分支事务,负责分支注册、状态汇报,并接收事务协调器的指令,驱动分支(本地)事务的提交和回滚。

而基于Seata框架,一个分布式事务的执行流程包含五大步骤:

1.TM向TC申请开启一个全局事务并生成全局唯一XID

2.XID在调用链路的上下文中进行传播

3.RM向TC注册分支事务,执行这个分支事务并提交

4.TM根据TC所有分支事务执行情况,发起全局提交或回滚决议

5.TC调度XID下管辖的全部分支事务完成提交或回滚请求

分布式事务配置

  引入 Maven 依赖

<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-jdbc-core-spring-boot-starter</artifactId>
    <version>${shardingsphere.version}</version>
</dependency>
<!-- 使用 XA 事务时,需要引入此模块 -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-transaction-xa-core</artifactId>
    <version>${shardingsphere.version}</version>
</dependency>
<!-- 使用 BASE 事务时,需要引入此模块 -->
<dependency>
    <groupId>org.apache.shardingsphere</groupId>
    <artifactId>shardingsphere-transaction-base-seata-at</artifactId>
    <version>${shardingsphere.version}</version>
</dependency>

  配置事务管理器

@Configuration
@EnableTransactionManagement
public class TransactionConfiguration {
    @Bean
    public PlatformTransactionManager txManager(final DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean
    public JdbcTemplate jdbcTemplate(final DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}

  使用分布式事务 , Apache ShardingSphere 默认的 XA 事务管理器为 Atomikos。

@Transactional 
// 支持 TransactionType.LOCAL,TransactionType.XA,TransactionType.BASE
@ShardingSphereTransactionType(TransactionType.XA) 
public void insert(){
    jdbcTemplate.execute("INSERT INTO t_order (user_id, status) VALUES (?, ?)",
        (PreparedStatementCallback<Object>)ps->{
        ps.setObject(1,i);
        ps.setObject(2,"init");
        ps.executeUpdate();
    });
}