当前位置:   article > 正文

ShardingSphere分库分表核心原理精讲第六节 分布式主键和解析引擎

shardingsphere分库分表核心原理精讲第六节

14 分布式主键:ShardingSphere 中有哪些分布式主键实现方式?

本课时我将为你讲解 ShardingSphere 中的分布式主键实现方式。

在传统数据库软件开发过程中,主键自动生成技术是基本需求。各个数据库对该需求也提供了相应的支持,比如 MySQL 的自增键,Oracle 的自增序列等。而在分片场景下,问题就变得有点复杂,我们不能依靠单个实例上的自增键来实现不同数据节点之间的全局唯一主键,这时分布式主键的需求就应运而生。ShardingSphere 作为一款优秀的分库分表开源软件,同样提供了分布式主键的实现机制,今天,我们就对这一机制的基本原理和实现方式展开讨论。

ShardingSphere 中的自动生成键方案

在介绍 ShardingSphere 提供的具体分布式主键实现方式之前,我们有必要先对框架中抽象的自动生成键 GeneratedKey 方案进行讨论,从而帮助你明确分布式主键的具体使用场景和使用方法。

ShardingSphere 中的 GeneratedKey

GeneratedKey 并不是 ShardingSphere 所创造的概念。如果你熟悉 Mybatis 这种 ORM 框架,对它就不会陌生。事实上,我们在《数据分片:如何实现分库、分表、分库+分表以及强制路由(上)?》中已经介绍了在 Mybatis 中嵌入 GeneratedKey 的实现方法。通常,我们会在 Mybatis 的 Mapper 文件中设置 useGeneratedKeys 和 keyProperty 属性:

    <insert id="addEntity" useGeneratedKeys="true" keyProperty="recordId" > 
        INSERT INTO health_record (user_id, level_id, remark)  
        VALUES (#{userId,jdbcType=INTEGER}, #{levelId,jdbcType=INTEGER},  
             #{remark,jdbcType=VARCHAR}) 
    </insert> 
  • 1
  • 2
  • 3
  • 4
  • 5

在执行这个 insert 语句时,返回的对象中自动包含了生成的主键值。当然,这种方式能够生效的前提是对应的数据库本身支持自增长的主键。

当我们使用 ShardingSphere 提供的自动生成键方案时,开发过程以及效果和上面描述的完全一致。在 ShardingSphere 中,同样实现了一个 GeneratedKey 类。请注意,该类位于 sharding-core-route 工程下。我们先看该类提供的 getGenerateKey 方法:

    public static Optional<GeneratedKey> getGenerateKey(final ShardingRule shardingRule, final TableMetas tableMetas, final List<Object> parameters, final InsertStatement insertStatement) { 
        //找到自增长列 
     Optional<String> generateKeyColumnName = shardingRule.findGenerateKeyColumnName(insertStatement.getTable().getTableName()); 
        if (!generateKeyColumnName.isPresent()) { 
            return Optional.absent(); 
        } 
         
        //判断自增长类是否已生成主键值 
        return Optional.of(containsGenerateKey(tableMetas, insertStatement, generateKeyColumnName.get()) 
                ? findGeneratedKey(tableMetas, parameters, insertStatement, generateKeyColumnName.get()) : createGeneratedKey(shardingRule, insertStatement, generateKeyColumnName.get())); 
	} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这段代码的逻辑在于先从 ShardingRule 中找到主键对应的 Column,然后判断是否已经包含主键:如果是则找到该主键,如果不是则生成新的主键。今天,我们的重点是分布式主键的生成,所以我们直接来到 createGeneratedKey 方法:

private static GeneratedKey createGeneratedKey(final ShardingRule shardingRule, final InsertStatement insertStatement, final String generateKeyColumnName) { 
        GeneratedKey result = new GeneratedKey(generateKeyColumnName, true); 
        for (int i = 0; i < insertStatement.getValueListCount(); i++) { 
            result.getGeneratedValues().add(shardingRule.generateKey(insertStatement.getTable().getTableName())); 
        } 
        return result; 
	} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在 GeneratedKey 中存在一个类型为 LinkedList 的 generatedValues 变量,用于保存生成的主键,但实际上,生成主键的工作转移到了 ShardingRule 的 generateKey 方法中,我们跳转到 ShardingRule 类并找到这个 generateKey 方法:

    public Comparable<?> generateKey(final String logicTableName) { 
        Optional<TableRule> tableRule = findTableRule(logicTableName); 
        if (!tableRule.isPresent()) { 
            throw new ShardingConfigurationException("Cannot find strategy for generate keys."); 
        } 
         
        //从TableRule中获取ShardingKeyGenerator并生成分布式主键 
        ShardingKeyGenerator shardingKeyGenerator = null == tableRule.get().getShardingKeyGenerator() ? defaultShardingKeyGenerator : tableRule.get().getShardingKeyGenerator(); 
        return shardingKeyGenerator.generateKey(); 
	} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

首先,根据传入的 logicTableName 找到对应的 TableRule,基于 TableRule 找到其包含的 ShardingKeyGenerator,然后通过 ShardingKeyGenerator 的 generateKey 来生成主键。从设计模式上讲,ShardingRule 也只是一个外观类,真正创建 ShardingKeyGenerator 的过程应该是在 TableRule 中。而这里的 ShardingKeyGenerator 显然就是真正生成分布式主键入口,让我们来看一下。

ShardingKeyGenerator

接下来我们分析 ShardingKeyGenerator 接口,从定义上看,该接口继承了 TypeBasedSPI 接口:

public interface ShardingKeyGenerator extends TypeBasedSPI {     
    Comparable<?> generateKey(); 
} 
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

来到 TableRule 中,在它的一个构造函数中找到了 ShardingKeyGenerator 的创建过程:

shardingKeyGenerator = containsKeyGeneratorConfiguration(tableRuleConfig) 
                ? new ShardingKeyGeneratorServiceLoader().newService(tableRuleConfig.getKeyGeneratorConfig().getType(), tableRuleConfig.getKeyGeneratorConfig().getProperties()) : null; 
  • 1
  • 2
  • 1
  • 2

这里有一个 ShardingKeyGeneratorServiceLoader 类,该类定义如下:

public final class ShardingKeyGeneratorServiceLoader extends TypeBasedSPIServiceLoader<ShardingKeyGenerator> { 
     
    static { 
        NewInstanceServiceLoader.register(ShardingKeyGenerator.class); 
    } 
     
    public ShardingKeyGeneratorServiceLoader() { 
        super(ShardingKeyGenerator.class); 
    } 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

回顾上一课时的内容,我们不难理解 ShardingKeyGeneratorServiceLoader 类的作用。ShardingKeyGeneratorServiceLoader 继承了 TypeBasedSPIServiceLoader 类,并在静态方法中通过 NewInstanceServiceLoader 注册了类路径中所有的 ShardingKeyGenerator。然后,ShardingKeyGeneratorServiceLoader 的 newService 方法基于类型参数通过 SPI 创建实例,并赋值 Properties 属性。

通过继承 TypeBasedSPIServiceLoader 类来创建一个新的 ServiceLoader 类,然后在其静态方法中注册相应的 SPI 实现,这是 ShardingSphere 中应用微内核模式的常见做法,很多地方都能看到类似的处理方法。

我们在 sharding-core-common 工程的 META-INF/services 目录中看到了具体的 SPI 定义:

1.png

分布式主键 SPI 配置

可以看到,这里有两个 ShardingKeyGenerator,分别是 SnowflakeShardingKeyGenerator 和 UUIDShardingKeyGenerator,它们都位于org.apache.shardingsphere.core.strategy.keygen 包下。

ShardingSphere 中的分布式主键实现方案

在 ShardingSphere 中,ShardingKeyGenerator 接口存在一批实现类。除了前面提到的 SnowflakeShardingKeyGenerator 和UUIDShardingKeyGenerator,还实现了 LeafSegmentKeyGenerator 和 LeafSnowflakeKeyGenerator 类,但这两个类的实现过程有些特殊,我们一会再具体展开。

UUIDShardingKeyGenerator

我们先来看最简单的 ShardingKeyGenerator,即 UUIDShardingKeyGenerator。UUIDShardingKeyGenerator 的实现非常容易理解,直接采用 UUID.randomUUID() 的方式产生分布式主键:

public final class UUIDShardingKeyGenerator implements ShardingKeyGenerator { 
     
    private Properties properties = new Properties(); 
     
    @Override 
    public String getType() { 
        return "UUID"; 
    } 
     
    @Override 
    public synchronized Comparable<?> generateKey() { 
        return UUID.randomUUID().toString().replaceAll("-", ""); 
    } 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
SnowflakeShardingKeyGenerator

再来看 SnowFlake(雪花)算法,SnowFlake 是 ShardingSphere 默认的分布式主键生成策略。它是 Twitter 开源的分布式 ID 生成算法,其核心思想是使用一个 64bit 的 long 型数字作为全局唯一 ID,且 ID 引入了时间戳,基本上能够保持自增。SnowFlake 算法在分布式系统中的应用十分广泛,SnowFlake 算法中 64bit 的详细结构存在一定的规范:

2.png

64bit 的 ID 结构图

在上图中,我们把 64bit 分成了四个部分:

  • 符号位

第一个部分即第一个 bit,值为 0,没有实际意义。

  • 时间戳位

第二个部分是 41 个 bit,表示的是时间戳。41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,一年所使用的毫秒数是365 * 24 * 60 * 60 * 1000,即 69.73 年。 也就是说,ShardingSphere 的 SnowFlake 算法的时间纪元从 2016 年 11 月 1 日零点开始,可以使用到 2086 年 ,相信能满足绝大部分系统的要求。

  • 工作进程位

第三个部分是 10 个 bit,表示工作进程位,其中前 5 个 bit 代表机房 id,后 5 个 bit 代表机器id。

  • 序列号位

第四个部分是 12 个 bit,表示序号,也就是某个机房某台机器上在一毫秒内同时生成的 ID 序号。如果在这个毫秒内生成的数量超过 4096(即 2 的 12 次幂),那么生成器会等待下个毫秒继续生成。

因为 SnowFlake 算法依赖于时间戳,所以还需要考虑时钟回拨这种场景。所谓时钟回拨,是指服务器因为时间同步,导致某一部分机器的时钟回到了过去的时间点。显然,时间戳的回滚会导致生成一个已经使用过的 ID,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工作。ShardingSphere 中最大容忍的时钟回拨毫秒数的默认值为 0,可通过属性设置。

了解了 SnowFlake 算法的基本概念之后,我们来看 SnowflakeShardingKeyGenerator 类的具体实现。首先在 SnowflakeShardingKeyGenerator 类中存在一批常量的定义,用于维护 SnowFlake 算法中各个 bit 之间的关系,同时还存在一个 TimeService 用于获取当前的时间戳。而 SnowflakeShardingKeyGenerator 的核心方法 generateKey 负责生成具体的 ID,我们这里给出详细的代码,并为每行代码都添加注释:

    @Override 
    public synchronized Comparable<?> generateKey() { 
         //获取当前时间戳 
        long currentMilliseconds = timeService.getCurrentMillis(); 
         
        //如果出现了时钟回拨,则抛出异常或进行时钟等待 
        if (waitTolerateTimeDifferenceIfNeed(currentMilliseconds)) { 
            currentMilliseconds = timeService.getCurrentMillis(); 
        } 
         
        //如果上次的生成时间与本次的是同一毫秒 
        if (lastMilliseconds == currentMilliseconds) { 
         //这个位运算保证始终就是在4096这个范围内,避免你自己传递的sequence超过了4096这个范围 
            if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) { 
              //如果位运算结果为0,则需要等待下一个毫秒继续生成 
                currentMilliseconds = waitUntilNextTime(currentMilliseconds); 
            } 
        } else {//如果不是,则生成新的sequence 
            vibrateSequenceOffset(); 
            sequence = sequenceOffset; 
        } 
        lastMilliseconds = currentMilliseconds; 
         
        //先将当前时间戳左移放到完成41个bit,然后将工作进程为左移到10个bit,再将序号为放到最后的12个bit 
        //最后拼接起来成一个64 bit的二进制数字 
        return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence; 
	} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

可以看到这里综合考虑了时钟回拨、同一个毫秒内请求等设计要素,从而完成了 SnowFlake 算法的具体实现。

LeafSegmentKeyGenerator 和 LeafSnowflakeKeyGenerator

事实上,如果实现类似 SnowflakeShardingKeyGenerator 这样的 ShardingKeyGenerator 是比较困难的,而且也属于重复造轮子。因此,尽管 ShardingSphere 在 4.X 版本中也提供了 LeafSegmentKeyGenerator 和 LeafSnowflakeKeyGenerator 这两个 ShardingKeyGenerator 的完整实现类。但在正在开发的 5.X 版本中,这两个实现类被移除了。

目前,ShardingSphere 专门提供了 OpenSharding 这个代码仓库来存放新版本的 LeafSegmentKeyGenerator 和 LeafSnowflakeKeyGenerator。新版本的实现类直接采用了第三方美团提供的 Leaf 开源实现。

Leaf 提供两种生成 ID 的方式,一种是号段(Segment)模式,一种是前面介绍的 Snowflake 模式。无论使用哪种模式,我们都需要提供一个 leaf.properties 文件,并设置对应的配置项。无论是使用哪种方式,应用程序都需要设置一个 leaf.key:

# for keyGenerator key 
leaf.key=sstest 
  
# for LeafSnowflake 
leaf.zk.list=localhost:2181 
  • 1
  • 2
  • 3
  • 4
  • 5

如果使用号段模式,需要依赖于一张数据库表来存储运行时数据,因此需要在 leaf.properties 文件中添加数据库的相关配置:

# for LeafSegment 
leaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC&useSSL=false 
leaf.jdbc.username=root 
leaf.jdbc.password=123456 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

基于这些配置,我们就可以创建对应的 DataSource,并进一步创建用于生成分布式 ID 的 IDGen 实现类,这里创建的是基于号段模式的 SegmentIDGenImpl 实现类:

//通过DruidDataSource构建数据源并设置属性 
DruidDataSource dataSource = new DruidDataSource(); 
                dataSource.setUrl(properties.getProperty(LeafPropertiesConstant.LEAF_JDBC_URL)); 
                dataSource.setUsername(properties.getProperty(LeafPropertiesConstant.LEAF_JDBC_USERNAME)); 
                dataSource.setPassword(properties.getProperty(LeafPropertiesConstant.LEAF_JDBC_PASSWORD)); 
dataSource.init(); 
                 
//构建数据库访问Dao组件 
IDAllocDao dao = new IDAllocDaoImpl(dataSource); 
//创建IDGen实现类 
this.idGen = new SegmentIDGenImpl(); 
//将Dao组件绑定到IDGen实现类 
 ((SegmentIDGenImpl) this.idGen).setDao(dao); 
this.idGen.init(); 
this.dataSource = dataSource; 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

一旦我们成功创建了 IDGen 实现类,可以通过该类来生成目标 ID,LeafSegmentKeyGenerator 类中包含了所有的实现细节:

Result result = this.idGen.get(properties.getProperty(LeafPropertiesConstant.LEAF_KEY)); 
return result.getId(); 
  • 1
  • 2
  • 1
  • 2

介绍完 LeafSegmentKeyGenerator 之后,我们再来看 LeafSnowflakeKeyGenerator。LeafSnowflakeKeyGenerator 的实现依赖于分布式协调框架 Zookeeper,所以在配置文件中需要指定 Zookeeper 的目标地址:

# for LeafSnowflake 
leaf.zk.list=localhost:2181 
  • 1
  • 2

创建用于 LeafSnowflake 的 IDGen 实现类 SnowflakeIDGenImpl 相对比较简单,我们直接在构造函数中设置 Zookeeper 地址就可以了:

IDGen idGen = new SnowflakeIDGenImpl(properties.getProperty(LeafPropertiesConstant.LEAF_ZK_LIST), 8089); 
  • 1

同样,通过 IDGen 获取模板 ID 的方式是一致的:


idGen.get(properties.getProperty(LeafPropertiesConstant.LEAF_KEY)).getId(); 
  • 1

显然,基于 Leaf 框架实现号段模式和 Snowflake 模式下的分布式 ID 生成方式非常简单,Leaf 框架为我们屏蔽了内部实现的复杂性。

从源码解析到日常开发

相比 ShardingSphere 中其他架构设计上的思想和实现方案,分布式主键非常独立,所以今天介绍的各种分布式主键的实现方式完全可以直接套用到日常开发过程中。无论是 ShardingSphere 自身实现的 SnowflakeShardingKeyGenerator,还是基于第三方框架实现的 LeafSegmentKeyGenerator 和 LeafSnowflakeKeyGenerator,都为我们使用分布式主键提供了直接的解决方案。当然,我们也可以在这些实现方案的基础上,进一步挖掘同类型的其他方案。

总结

在分布式系统的开发过程中,分布式主键是一种基础需求。而对于与数据库相关的操作而言,我们往往需要将分布式主键与数据库的主键自动生成机制关联起来。在今天的课程中,我们就从 ShardingSphere 的自动生成键方案说起,引出了分布式主键的各种实现方案。这其中包括最简单的 UUID,也包括经典的雪花算法,以及雪花算法的改进方案 LeafSegment 和 LeafSnowflake 算法。

这里给你留一道思考题:ShardingSphere 中如何分别实现基于号段的 Leaf 以及基于 Snowflake 的 Leaf 来生成分布式 ID?

从下一课时开始,我们将进入到 ShardingSphere 分片引擎实现原理的讲解过程中,我将首先为你介绍解析引擎的执行流程,记得按时来听课。


15 解析引擎:SQL 解析流程应该包括哪些核心阶段?(上)

你好,欢迎进入第 15 课时的学习,结束了对 ShardingSphere 中微内核架构等基础设施相关实现机制的介绍后,今天我们将正式进入到分片引擎的学习。

对于一款分库分表中间件而言,分片是其最核心的功能。下图展示了整个 ShardingSphere 分片引擎的组成结构,我们已经在《12 | 从应用到原理:如何高效阅读 ShardingSphere 源码》这个课时中对分片引擎中所包含的各个组件进行了简单介绍。我们知道,对于分片引擎而言,第一个核心组件就是 SQL 解析引擎。

Drawing 0.png

对于多数开发人员而言,SQL 解析是一个陌生的话题,但对于一个分库分表中间件来说却是一个基础组件,目前主流的分库分表中间件都包含了对解析组件的实现策略。可以说,SQL 解析引擎所生成的结果贯穿整个 ShardingSphere。如果我们无法很好地把握 SQL 的解析过程,在阅读 ShardingSphere 源码时就会遇到一些障碍。

另一方面,SQL 的解析过程本身也很复杂,你在拿到 ShardingSphere 框架的源代码时,可能首先会问这样一个问题:SQL 的解析过程应该包含哪些核心阶段呢?接下来我将带你深度剖析这个话题。

从 DataSource 到 SQL 解析引擎入口

在对分片引擎的整体介绍中可以看到,要想完成分片操作,首先需要引入 SQL 解析引擎。对于刚接触 ShardingSphere 源码的同学而言,想要找到 SQL 解析引擎的入口有一定难度。这里引用在《04 | 应用集成:在业务系统中使用 ShardingSphere 的方式有哪些?》这个课时中介绍的代码示例,来分析 SQL 解析引擎的入口。

我们回顾如下所示的代码片段,这些代码片段基于 Java 语言提供了数据分片的实现方式:

//创建分片规则配置类 
ShardingRuleConfiguration shardingRuleConfig = new ShardingRuleConfiguration(); 
  • 1
  • 2
  • 1
  • 2

//创建分表规则配置类
TableRuleConfiguration tableRuleConfig = new TableRuleConfiguration(“user”, “ds 0..1. u s e r {0..1}.user 0..1.user{0…1}”);

//创建分布式主键生成配置类
Properties properties = new Properties();
result.setProperty(“worker.id”, “33”);
KeyGeneratorConfiguration keyGeneratorConfig = new KeyGeneratorConfiguration(“SNOWFLAKE”, “id”, properties);
result.setKeyGeneratorConfig(keyGeneratorConfig);
shardingRuleConfig.getTableRuleConfigs().add(tableRuleConfig);

//根据年龄分库,一共分为2个库
shardingRuleConfig.setDefaultDatabaseShardingStrategyConfig(new InlineShardingStrategyConfiguration(“sex”, “ds${sex % 2}”));

//根据用户id分表,一共分为2张表
shardingRuleConfig.setDefaultTableShardingStrategyConfig(new StandardShardingStrategyConfiguration(“id”, “user${id % 2}”));

//通过工厂类创建具体的DataSource
return ShardingDataSourceFactory.createDataSource(createDataSourceMap(), shardingRuleConfig, new Properties());

可以看到,上述代码构建了几个数据源,加上分库、分表策略以及分片规则,然后通过 ShardingDataSourceFactory 获取了目前数据源 DataSource 。显然,对于应用开发而言,DataSource 就是我们使用 ShardingSphere 框架的入口。事实上,对于 ShardingSphere 内部的运行机制而言,DataSource 同样是引导我们进入分片引擎的入口。围绕 DataSource,通过跟踪代码的调用链路,我们可以得到如下所示的类层结构图:

Drawing 2.png

上图已经引出了 ShardingSphere 内核中的很多核心对象,但今天我们只关注位于整个链路的最底层对象,即图中的 SQLParseEngine。一方面,在 DataSource 的创建过程中,最终初始化了 SQLParseEngine;另一方面,负责执行路由功能的 ShardingRouter 也依赖于 SQLParseEngine。这个 SQLParseEngine 就是 ShardingSphere 中负责整个 SQL 解析过程的入口。

从 SQL 解析引擎到 SQL 解析内核

在 ShardingSphere 中,存在一批以“Engine”结尾的引擎类。从架构思想上看,这些类在设计和实现上普遍采用了外观模式。外观(Facade)模式的意图可以描述为子系统中的一组接口提供一个一致的界面。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。该模式的示意图如下图所示:

Drawing 4.png

从作用上讲,外观模式能够起到客户端与后端服务之间的隔离作用,随着业务需求的变化和时间的演进,外观背后各个子系统的划分和实现可能需要进行相应的调整和升级,这种调整和升级需要做到对客户端透明。在设计诸如 ShardingSphere 这样的中间件框架时,这种隔离性尤为重要。

对于 SQL 解析引擎而言,情况同样类似。不同之处在于,SQLParseEngine 本身并不提供外观作用,而是把这部分功能委托给了另一个核心类 SQLParseKernel。从命名上看,这个类才是 SQL 解析的内核类,也是所谓的外观类。SQLParseKernel 屏蔽了后端服务中复杂的 SQL 抽象语法树对象 SQLAST、SQL 片段对象 SQLSegment ,以及最终的 SQL 语句 SQLStatement 对象的创建和管理过程。上述这些类之间的关系如下所示:

Drawing 6.png

1.SQLParseEngine

从前面的类层结构图中可以看到,AbstractRuntimeContext 是 SQLParseEngine 的构建入口。顾名思义,RuntimeContext 在 ShardingSphere 中充当一种运行时上下文,保存着与运行时环境下相关的分片规则、分片属性、数据库类型、执行引擎以及 SQL 解析引擎。作为 RuntimeContext 接口的实现类,AbstractRuntimeContex 在其构造函数中完成了对 SQLParseEngine 的构建,构建过程如下所示:

protected AbstractRuntimeContext(final T rule, final Properties props, final DatabaseType databaseType) { 
       … 
       parseEngine = SQLParseEngineFactory.getSQLParseEngine(DatabaseTypes.getTrunkDatabaseTypeName(databaseType)); 
       … 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

显然,这里通过工厂类 SQLParseEngineFactory 完成了 SQLParseEngine 的创建过程。工厂类 SQLParseEngineFactory 的实现如下:

public final class SQLParseEngineFactory { 
  • 1
  • 1

    private static final Map<String, SQLParseEngine> ENGINES = new ConcurrentHashMap<>();

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> SQLParseEngine <span class="hljs-title">getSQLParseEngine</span><span class="hljs-params">(<span class="hljs-keyword">final</span> String databaseTypeName)</span> </span>{
  • 1

        if (ENGINES.containsKey(databaseTypeName)) {
            return ENGINES.get(databaseTypeName);
        }
        synchronized (ENGINES) {
           //如果缓存中包含了指定数据库类型对应的SQLParseEngine,则直接返回
            if (ENGINES.containsKey(databaseTypeName)) {
                return ENGINES.get(databaseTypeName);
            }
           //创建SQLParseEngine
            SQLParseEngine result = new SQLParseEngine(databaseTypeName);
           //将新创建的SQLParseEngine放入缓存中
            ENGINES.put(databaseTypeName, result);
            return result;
        }
    }
}

从上述代码中可以看到,这里基于 ConcurrentHashMap 对象做了一层基于内存的缓存处理,SQLParseEngineFactory 的实现方式在 ShardingSphere 中具有代表性。为了提高访问性能,ShardingSphere 大量使用这种方式来构建基于内容的缓存机制。

接下来,我们来看 SQLParseEngine 类本身,该类的完整代码如下所示:

public final class SQLParseEngine { 
  • 1
  • 1

    private final String databaseTypeName;

    private final SQLParseResultCache cache = new SQLParseResultCache();

<span class="hljs-function"><span class="hljs-keyword">public</span> SQLStatement <span class="hljs-title">parse</span><span class="hljs-params">(<span class="hljs-keyword">final</span> String sql, <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> useCache)</span> </span>{ 
&nbsp;&nbsp;&nbsp; <span class="hljs-comment">//基于Hook机制进行监控和跟踪 </span>
  • 1
  • 2

        ParsingHook parsingHook = new SPIParsingHook();
        parsingHook.start(sql);
        try {
           //完成SQL的解析,并返回一个SQLStatement对象
            SQLStatement result = parse0(sql, useCache);
            parsingHook.finishSuccess(result);
            return result;
        } catch (final Exception ex) {
            parsingHook.finishFailure(ex);
            throw ex;
        }
    }

<span class="hljs-function"><span class="hljs-keyword">private</span> SQLStatement <span class="hljs-title">parse0</span><span class="hljs-params">(<span class="hljs-keyword">final</span> String sql, <span class="hljs-keyword">final</span> <span class="hljs-keyword">boolean</span> useCache)</span> </span>{ 
&nbsp;&nbsp;&nbsp; <span class="hljs-comment">//如果使用缓存,先尝试从缓存中获取SQLStatement </span>
  • 1
  • 2

        if (useCache) {
            Optional<SQLStatement> cachedSQLStatement = cache.getSQLStatement(sql);
            if (cachedSQLStatement.isPresent()) {
                return cachedSQLStatement.get();
            }
        }
        //委托SQLParseKernel创建SQLStatement
SQLStatement result = new SQLParseKernel(ParseRuleRegistry.getInstance(), databaseTypeName, sql).parse();
        if (useCache) {
            cache.put(sql, result);
        }
        return result;
    }
}

关于 SQLParseEngine 有几点值得注意:

  • 首先,这里使用了 ParsingHook 作为系统运行时的 Hook 管理,也就是我们常说的代码钩子。ShardingSphere 提供了一系列的 ParsingHook 实现,后续我们在讨论到 ShardingSphere 的链路跟踪时会对 Hook 机制进一步展开。

  • 其次,我们发现用于解析 SQL 的 parse 方法返回了一个 SQLStatement 对象。也就是说,这个 SQLStatement 就是整个 SQL 解析引擎的最终输出对象。这里同样基于 Google Guava 框架中的 Cache 类构建了一个 SQLParseResultCache,对解析出来的 SQLStatement 进行缓存处理。

最后,我们发现 SQLParseEngine 把真正的解析工作委托给了 SQLParseKernel。接下来,我们就来看这个 SQLParseKernel 类。

2.SQLParseKernel

在 SQLParseKernel 类中,发现了如下所示的三个 Engine 类定义,包括 SQL 解析器引擎 SQLParserEngine(请注意该类名与 SQLParseEngine 类名的区别)、SQLSegment 提取器引擎 SQLSegmentsExtractor 以及 SQLStatement 填充器引擎 SQLStatementFiller。

//SQL解析器引擎 
private final SQLParserEngine parserEngine; 
//SQLSegment提取器引擎 
private final SQLSegmentsExtractorEngine extractorEngine; 
//SQLStatement填充器引擎 
private final SQLStatementFillerEngine fillerEngine;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

作为外观类的 SQLParseKernel 提供了如下所示的 parse 方法,来完成 SQL 解析的整个过程,该方法中分别用到了上述三个引擎类,如下所示:

public SQLStatement parse() { 
  • 1
&nbsp;&nbsp;&nbsp;<span class="hljs-comment">//利用ANTLR4 解析SQL的抽象语法树 </span>
&nbsp;&nbsp;&nbsp;SQLAST ast = parserEngine.parse(); 

&nbsp;&nbsp;&nbsp;<span class="hljs-comment">//提取AST中的Token,封装成对应的TableSegment、IndexSegment 等各种Segment </span>
&nbsp;&nbsp;&nbsp;Collection&lt;SQLSegment&gt; sqlSegments = extractorEngine.extract(ast); 
&nbsp;&nbsp;&nbsp;Map&lt;ParserRuleContext, Integer&gt; parameterMarkerIndexes = ast.getParameterMarkerIndexes(); 

&nbsp;&nbsp;&nbsp;&nbsp;<span class="hljs-comment">//填充SQLStatement并返回 </span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="hljs-keyword">return</span> fillerEngine.fill(sqlSegments, parameterMarkerIndexes.size(), ast.getSqlStatementRule()); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1

}

SQL 解析引擎的三大阶段之如何 生成 SQLAST

上面这段代码非常符合外观类的处理风格,即把内部系统的核心类通过简单的调用方式组合在一起完成业务链路。我们对三段代码分别添加了注释,实际上,根据这些注释,我们已经可以回答在本课时开始时所提出 “SQL 解析过程应该包含哪些核心阶段?” 这一问题,即:

  • 通过 SQLParserEngine 生成 SQL 抽象语法树

  • 通过 SQLSegmentsExtractorEngine 提取 SQLSegment

  • 通过 SQLStatementFiller 填充 SQLStatement

这三个阶段便是 ShardingSphere 新一代 SQL 解析引擎的核心组成部分。其整体架构如下图所示:

Drawing 8.png

至此,我们看到由解析、提取和填充这三个阶段所构成的整体 SQL 解析流程已经完成。现在能够根据一条 SQL 语句解析出对应的 SQLStatement 对象,供后续的 ShardingRouter 等路由引擎进行使用。

本课时我们首先关注流程中的第一阶段,即如何生成一个 SQLAST(后两个阶段会在后续课时中讲解)。这部分的实现过程位于 SQLParserEngine 的 parse 方法,如下所示:

public SQLAST parse() { 
	    SQLParser sqlParser = SQLParserFactory.newInstance(databaseTypeName, sql); 
  • 1
  • 2
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="hljs-comment">//利用ANTLR4获取解析树 </span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ParseTree parseTree; 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="hljs-keyword">try</span> { 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ((Parser) sqlParser).setErrorHandler(<span class="hljs-keyword">new</span> BailErrorStrategy()); 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ((Parser) sqlParser).getInterpreter().setPredictionMode(PredictionMode.SLL); 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;parseTree = sqlParser.execute().getChild(<span class="hljs-number">0</span>); 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } <span class="hljs-keyword">catch</span> (<span class="hljs-keyword">final</span> ParseCancellationException ex) { 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; ((Parser) sqlParser).reset(); 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;((Parser) sqlParser).setErrorHandler(<span class="hljs-keyword">new</span> DefaultErrorStrategy()); 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;((Parser) sqlParser).getInterpreter().setPredictionMode(PredictionMode.LL); 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;parseTree = sqlParser.execute().getChild(<span class="hljs-number">0</span>); 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } 
&nbsp;&nbsp;&nbsp;&nbsp; <span class="hljs-keyword">if</span> (parseTree <span class="hljs-keyword">instanceof</span> ErrorNode) { 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> SQLParsingException(String.format(<span class="hljs-string">"Unsupported SQL of `%s`"</span>, sql)); 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } 

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="hljs-comment">//获取配置文件中的StatementRule </span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SQLStatementRule rule = parseRuleRegistry.getSQLStatementRule(databaseTypeName, parseTree.getClass().getSimpleName()); 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="hljs-keyword">if</span> (<span class="hljs-keyword">null</span> == rule) { 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> SQLParsingException(String.format(<span class="hljs-string">"Unsupported SQL of `%s`"</span>, sql)); 
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; } 

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="hljs-comment">//封装抽象语法树AST </span>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> SQLAST((ParserRuleContext) parseTree, getParameterMarkerIndexes((ParserRuleContext) parseTree), rule); 
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 1
  • 2

上述代码中 SQLParser 接口负责具体的 SQL 到 AST(Abstract Syntax Tree,抽象语法树)的解析过程。而具体 SQLParser 实现类的生成由 SQLParserFactory 负责,SQLParserFactory 定义如下:

public final class SQLParserFactory { 
  • 1
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> SQLParser <span class="hljs-title">newInstance</span><span class="hljs-params">(<span class="hljs-keyword">final</span> String databaseTypeName, <span class="hljs-keyword">final</span> String sql)</span> </span>{ 
 <span class="hljs-comment">//通过SPI机制加载所有扩展 </span>
 <span class="hljs-keyword">for</span> (SQLParserEntry each : NewInstanceServiceLoader.newServiceInstances(SQLParserEntry.class)) { 

    //判断数据库类型 
    <span class="hljs-keyword">if</span> (each.getDatabaseTypeName().equals(databaseTypeName)) { 
          <span class="hljs-keyword">return</span> createSQLParser(sql, each); 
     } 
 } 
 <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> UnsupportedOperationException(String.format(<span class="hljs-string">"Cannot support database type '%s'"</span>, databaseTypeName)); 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1


}

这里又引入了另一个核心接口,即 SQLParserEntry。可以看到,在 SQLParserFactory 类中,我们也使用了《13 | 微内核架构:ShardingSphere 如何实现系统的扩展性》这个课时中介绍的 NewInstanceServiceLoader 工具类来加载具体 SQLParserEntry 的实现类。

从这种实现方式上看,我们可以断定 SQLParserEntry 是一个 SPI 接口。通过查看 SQLParserEntry 所处的代码包结构,更印证了这一观点,因为该类位于 shardingsphere-sql-parser-spi 工程的 org.apache.shardingsphere.sql.parser.spi 包中。

关于 SQLParser 和 SQLParserEntry 这一对接口,还有一点值得探讨。注意到 SQLParser 接口位于 shardingsphere-sql-parser-spi 工程的 org.apache.shardingsphere.sql.parser.api 包中,所示它是一个 API 接口。

从定位上讲,SQLParser 是解析器对外暴露的入口,而 SQLParserEntry 是解析器的底层实现,两者共同构成了 SQL 解析器本身。更宽泛的,从架构设计层次上讲,API 面向高层业务开发人员,而 SPI 面向底层框架开发人员,两者的关系如下图所示。作为一款优秀的中间件框架,这种 API 和 SPI 的对应关系在 ShardingSphere 中非常普遍,也是我们正确理解 ShardingSphere 架构设计上的一个切入点。

Drawing 10.png

SQLParser 和 SQLParserEntry 这两个接口的定义和实现都与基于 ANTLR4 的 AST 生成机制有关。ANTLR 是 Another Tool for Language Recognition 的简写,是一款能够根据输入自动生成语法树的开源语法分析器。ANTLR 可以将用户编写的 ANTLR 语法规则直接生成 Java、Go 语言的解析器,在 ShardingSphere 中就使用了 ANTLR4 来生成 AST。

我们注意到 SQLParserEngine 的 parse 方法最终返回的是一个 SQLAST,该类的定义如下所示。

public final class SQLAST { 
  • 1
  • 1

    private final ParserRuleContext parserRuleContext;

    private final Map<ParserRuleContext, Integer> parameterMarkerIndexes;

    private final SQLStatementRule sqlStatementRule;
}

这里的 ParserRuleContext 实际上就来自 ANTLR4,而 SQLStatementRule 则是一个规则对象,包含了对 SQLSegment 提取器的定义。这样,我们就需要进入下一个阶段的讨论,即如何提取 SQLSegment(下一课时会讲解)。

总结

作为 ShardingSphere 分片引擎的第一个核心组件,解析引擎的目的在于生成 SQLStatement 目标对象。而整个解析引擎分成三大阶段,即生成 SQL 抽象语法树、提取 SQL 片段以及使用这些片段来填充 SQL 语句。本课时对解析引擎的整体结构以及这三个阶段中的第一个阶段进行了详细的讨论。

这里给你留一道思考题:在 ShardingSphere 中,外观模式如何应用到 SQL 解析过程中?欢迎你在留言区与大家讨论,我将一一点评解答。

本课时的内容就到这里,在下一课时中,我们将完成对 SQL 解析引擎剩余两个阶段内容的介绍,即如何提取 SQL 片段以及填充 SQL 语句,记得按时来听课。


16 解析引擎:SQL 解析流程应该包括哪些核心阶段?(下)

我们知道整个 SQL 解析引擎可以分成三个阶段(如下图所示),上一课时我们主要介绍了 ShardingSphere 中 SQL 解析引擎的第一个阶段,那么今天我将承接上一课时,继续讲解 ShardingSphere 中 SQL 解析流程中剩余的两个阶段。

Drawing 0.png

SQL 解析引擎的三大阶段

在 SQL 解析引擎的第一阶段中,我们详细介绍了 ShardingSphere 生成 SQL 抽象语法树的过程,并引出了 SQLStatementRule 规则类。今天我们将基于这个规则类来分析如何提取 SQLSegment 以及如何填充 SQL 语句的实现机制。

1.第二阶段:提取 SQL 片段

要理解 SQLStatementRule,就需要先介绍 ParseRuleRegistry 类。从命名上看,该类就是一个规则注册表,保存着各种解析规则信息。ParseRuleRegistry 类中的核心变量包括如下所示的三个 Loader 类:

	private final ExtractorRuleDefinitionEntityLoader extractorRuleLoader = new ExtractorRuleDefinitionEntityLoader(); 
     
    private final FillerRuleDefinitionEntityLoader fillerRuleLoader = new FillerRuleDefinitionEntityLoader(); 
     
    private final SQLStatementRuleDefinitionEntityLoader statementRuleLoader = new SQLStatementRuleDefinitionEntityLoader(); 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

从命名上可以看到这三个 Loader 类分别处理对 SQLStatementRule、ExtractorRule 和 FillerRule 这三种规则定义的加载。

我们先来看 SQLStatementRule,它们的定义位于 sql-statement-rule-definition.xml 配置文件中。我们以 Mysql 为例,这个配置文件位于 shardingsphere-sql-parser-mysql 工程中的 META-INF/parsing-rule-definition/mysql 目录下。我们截取该配置文件中的部分配置信息作为演示,如下所示:

<sql-statement-rule-definition> 
    <sql-statement-rule context="select" sql-statement-class="org.apache.shardingsphere.sql.parser.sql.statement.dml.SelectStatement" extractor-rule-refs="tableReferences, columns, selectItems, where, predicate, groupBy, orderBy, limit, subqueryPredicate, lock" /> 
    <sql-statement-rule context="insert" sql-statement-class="org.apache.shardingsphere.sql.parser.sql.statement.dml.InsertStatement" extractor-rule-refs="table, columns, insertColumns, insertValues, setAssignments, onDuplicateKeyColumns" /> 
    <sql-statement-rule context="update" sql-statement-class="org.apache.shardingsphere.sql.parser.sql.statement.dml.UpdateStatement" extractor-rule-refs="tableReferences, columns, setAssignments, where, predicate" /> 
    <sql-statement-rule context="delete" sql-statement-class="org.apache.shardingsphere.sql.parser.sql.statement.dml.DeleteStatement" extractor-rule-refs="tables, columns, where, predicate" /></sql-statement-rule-definition> 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

基于 ParseRuleRegistry 类进行规则获取和处理过程,涉及一大批实体对象以及用于解析 XML 配置文件的 JAXB 工具类的定义,内容虽多但并不复杂。核心类之间的关系如下图所示:

Drawing 2.png

ParseRuleRegistry 类层结构图

当获取规则之后,对于具体某种数据库类型的每条 SQL 而言,都会有一个 SQLStatementRule 对象。我们注意到每个 SQLStatementRule 都定义了一个“context”以及一个“sql-statement-class”。

这里的 context 实际上就是通过 SQL 解析所生成的抽象语法树 SQLAST 中的 ParserRuleContext,包括 CreateTableContext、SelectContext 等各种 StatementContext。而针对每一种 context,都有专门的一个 SQLStatement 对象与之对应,那么这个 SQLStatement 究竟长什么样呢?我们来看一下。

public interface SQLStatement { 
     
    //获取参数个数 
    int getParametersCount(); 
     
    //获取所有SQLSegment 
    Collection<SQLSegment> getAllSQLSegments(); 
     
    //根据类型获取一个SQLSegment 
    <T extends SQLSegment> Optional<T> findSQLSegment(Class<T> sqlSegmentType); 
     
    //根据类型获取一组SQLSegment 
    <T extends SQLSegment> Collection<T> findSQLSegments(Class<T> sqlSegmentType); 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

你可以看到,作为解析引擎最终产物的 SQLStatement ,实际上封装的是对 SQL 片段对象 SQLSegment 的获取操作。显然,对于每一个 ParserRuleContext 而言,我们最终就是构建了一个包含一组 SQLSegment 的 SQLStatement 对象,而这些 SQLSegment 的构建过程就是所谓的提取 SQLSegment 的过程。我们在配置文件中也明确看到了 SQLStatementRule 中对各种提取规则对象 ExtractorRule 的引用。

在 ShardingSphere 中内置了一大批通用的 SQLSegment,包括查询选择项(SelectItems)、表信息(Table)、排序信息(OrderBy)、分组信息(GroupBy)以及分页信息(Limit)等。这些通用 SQLSegment 都有对应的 SQLSegmentExtractor,我们可以直接在 SQLStatementRule 中进行使用。

另一方面,考虑到 SQL 方言的差异性,ShardingSphere 同样提供了针对各种数据库的 SQLSegment 的提取器定义。以 Mysql 为例,在其代码工程的 META-INF/parsing-rule-definition/mysql 目录下,存在一个 extractor-rule-definition.xml 配置文件,专门用来定义针对 Mysql 的各种 SQLSegmentExtractor,部分定义如下所示,作为一款适用于多数据库的中间件,这也是 ShardingSphere 应对 SQL 方言的实现机制之一。

<extractor-rule-definition> 
    <extractor-rule id="addColumnDefinition" extractor-class="org.apache.shardingsphere.sql.parser.core.extractor.ddl.MySQLAddColumnDefinitionExtractor" /> 
    <extractor-rule id="modifyColumnDefinition" extractor-class="org.apache.shardingsphere.sql.parser.core.extractor.ddl.MySQLModifyColumnDefinitionExtractor" /> 
    … 
</extractor-rule-definition> 
  • 1
  • 2
  • 3
  • 4
  • 5

现在,假设有这样一句 SQL:

SELECT task_id, task_name FROM health_task WHERE user_id = 'user1' AND record_id = 2  
  • 1
  • 1

通过解析,我们获取了如下所示的抽象语法树:

Drawing 4.png

抽象语法树示意图

我们发现,对于上述抽象语法树中的某些节点(如 SELECT、FROM 和 WHERE)没有子节点,而对于如 FIELDS、TABLES 和 CONDITIONS 节点而言,本身也是一个树状结构。显然,这两种节点的提取规则应该是不一样的。

因此,ShardingSphere 提供了两种 SQLSegmentExtractor,一种是针对单节点的 OptionalSQLSegmentExtractor;另一种是针对树状节点的 CollectionSQLSegmentExtractor。由于篇幅因素,这里以 TableExtractor 为例,展示如何提取 TableSegment 的过程,TableExtractor 的实现方法如下所示:

public final class TableExtractor implements OptionalSQLSegmentExtractor { 
     
    @Override 
    public Optional<TableSegment> extract(final ParserRuleContext ancestorNode, final Map<ParserRuleContext, Integer> parameterMarkerIndexes) { 
        //从Context中获取TableName节点 
     Optional<ParserRuleContext> tableNameNode = ExtractorUtils.findFirstChildNode(ancestorNode, RuleName.TABLE_NAME); 
        if (!tableNameNode.isPresent()) { 
            return Optional.absent(); 
        }         
        //根据TableName节点构建TableSegment 
        TableSegment result = getTableSegment(tableNameNode.get()); 
        //设置表的别名 
        setAlias(tableNameNode.get(), result); 
        return Optional.of(result); 
    } 
     
    private TableSegment getTableSegment(final ParserRuleContext tableNode) { 
     //从Context中获取Name节点       
        ParserRuleContext nameNode = ExtractorUtils.getFirstChildNode(tableNode, RuleName.NAME); 
        //根据Name节点获取节点的起止位置以及节点内容 
        TableSegment result = new TableSegment(nameNode.getStart().getStartIndex(), nameNode.getStop().getStopIndex(), nameNode.getText()); 
        //从Context中获取表的Owner节点,如果有的话就设置Owner 
        Optional<ParserRuleContext> ownerNode = ExtractorUtils.findFirstChildNodeNoneRecursive(tableNode, RuleName.OWNER); 
        if (ownerNode.isPresent()) { 
            result.setOwner(new SchemaSegment(ownerNode.get().getStart().getStartIndex(), ownerNode.get().getStop().getStopIndex(), ownerNode.get().getText())); 
        } 
        return result; 
    } 
     
    private void setAlias(final ParserRuleContext tableNameNode, final TableSegment tableSegment) { 
     //从Context中获取Alias节点,如果有的话就设置别名 
     Optional<ParserRuleContext> aliasNode = ExtractorUtils.findFirstChildNode(tableNameNode.getParent(), RuleName.ALIAS); 
        if (aliasNode.isPresent()) { 
            tableSegment.setAlias(aliasNode.get().getText()); 
        } 
    } 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

显然,语法树中的 Table 是一种单节点,所以 TableExtractor 继承自 OptionalSQLSegmentExtractor。对于 TableExtractor 而言,整个解析过程就是从 ParserRuleContext 中获取与表定义相关的各种节点,然后通过节点的起止位置以及节点内容来构建 TableSegment 对象。TableSegment 实现了 SQLSegment,其核心变量的定义也比较明确,如下所示:

public final class TableSegment implements SQLSegment, TableAvailable, OwnerAvailable<SchemaSegment>, AliasAvailable { 
     
    private final int startIndex;     
    private final int stopIndex;   
    private final String name;  
    private final QuoteCharacter quoteCharacter; 
    private SchemaSegment owner;  
	private String alias; 
       … 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

现在,基于以上关于提取器以及提取操作的相关概念的理解,我们来看一下 SQLSegment 提取引擎 SQLSegmentsExtractorEngine 的实现,如下所示:

public final class SQLSegmentsExtractorEngine { 
     
    //用来提取SQLAST语法树中的SQL片段 
    public Collection<SQLSegment> extract(final SQLAST ast) { 
        Collection<SQLSegment> result = new LinkedList<>(); 
         
        //遍历提取器,从Context中提取对应类型的SQLSegment,比如TableSegment         
        for (SQLSegmentExtractor each : ast.getSqlStatementRule().getExtractors()) {            
            //单节点的场景,直接提取单一节点下的内容 
            if (each instanceof OptionalSQLSegmentExtractor) { 
                Optional<? extends SQLSegment> sqlSegment = ((OptionalSQLSegmentExtractor) each).extract(ast.getParserRuleContext(), ast.getParameterMarkerIndexes()); 
                if (sqlSegment.isPresent()) { 
                    result.add(sqlSegment.get()); 
                } 
                 
            树状节点的场景,遍历提取节点下的所有子节点// 
            } else if (each instanceof CollectionSQLSegmentExtractor) { 
                result.addAll(((CollectionSQLSegmentExtractor) each).extract(ast.getParserRuleContext(), ast.getParameterMarkerIndexes())); 
            } 
        } 
        return result; 
    } 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

显然,SQLSegmentsExtractorEngine 的作用就是针对某一条 SQL,遍历 SQLStatementRule 中所配置的提取器,然后从 Context 中提取对应类型的 SQLSegment,并最终存放在一个集合对象中进行返回。

2.第三阶段:填充 SQL 语句

完成所有 SQLSegment 的提取之后,我们就来到了解析引擎的最后一个阶段,即填充 SQLStatement。所谓的填充过程,就是通过填充器 SQLSegmentFiller 为 SQLStatement 注入具体 SQLSegment 的过程。这点从 SQLSegmentFiller 接口定义中的各个参数就可以得到明确,如下所示:

public interface SQLSegmentFiller<T extends SQLSegment> { 
     
    void fill(T sqlSegment, SQLStatement sqlStatement); 
} 
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

那么问题就来了,我们如何正确把握 SQLSegmentFiller、SQLSegment 和 SQLStatement 这三者之间的处理关系呢?我们先根据某个 SQLSegment 找到对应的 SQLSegmentFiller,这部分关系在 ShardingSphere 中同样是维护在一个 filler-rule-definition.xml 配置文件中,截取部分配置项如下所示:

<filler-rule-definition> 
    <filler-rule sql-segment-class="org.apache.shardingsphere.sql.parser.sql.segment.generic.TableSegment" filler-class="org.apache.shardingsphere.sql.parser.core.filler.impl.TableFiller" /> 
    <filler-rule sql-segment-class="org.apache.shardingsphere.sql.parser.sql.segment.generic.SchemaSegment" filler-class="org.apache.shardingsphere.sql.parser.core.filler.impl.dal.SchemaFiller" /></filler-rule-definition> 
  • 1
  • 2
  • 3
  • 4
  • 5

显然,这里保存着 SQLSegment 与 SQLSegmentFiller 之间的对应关系。当然,对于不同的 SQL 方言,也同样可以维护自身的 filler-rule-definition.xml 文件。

我们还是以与 TableSegment 对应的 TableFiller 为例,来分析一个 SQLSegmentFiller 的具体实现方法,TableFiller 类如下所示:

public final class TableFiller implements SQLSegmentFiller<TableSegment> { 
     
    @Override 
    public void fill(final TableSegment sqlSegment, final SQLStatement sqlStatement) { 
        if (sqlStatement instanceof TableSegmentAvailable) { 
            ((TableSegmentAvailable) sqlStatement).setTable(sqlSegment); 
        } else if (sqlStatement instanceof TableSegmentsAvailable) { 
            ((TableSegmentsAvailable) sqlStatement).getTables().add(sqlSegment); 
        } 
    } 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

这段代码在实现上采用了回调机制来完成对象的注入。在 ShardingSphere 中,基于回调的处理方式也非常普遍。本质上,回调解决了因为类与类之间的相互调用而造成的循环依赖问题,回调的实现策略通常采用了如下所示的类层结构:

Drawing 6.png

回调机制示意图

TableFiller 中所依赖的 TableSegmentAvailable 和 TableSegmentsAvailable 接口就类似于上图中的 Callback 接口,具体的 SQLStatement 就是 Callback 的实现类,而 TableFiller 则是 Callback 的调用者。以 TableFiller 为例,我们注意到,如果对应的 SQLStatement 实现了这两个接口中的任意一个,那么就可以通过 TableFiller 注入对应的 TableSegment,从而完成 SQLSegment 的填充。

这里以 TableSegmentAvailable 接口为例,它有一组实现类,如下所示:

Drawing 8.png

TableSegmentAvailable实现类

以上图中的 CreateTableStatement 为例,该类同时实现了 TableSegmentAvailable 和 IndexSegmentsAvailable 这两个回调接口,所以就可以同时操作 TableSegment 和 IndexSegment 这两个 SQLSegment。CreateTableStatement 类的实现如下所示:

public final class CreateTableStatement extends DDLStatement implements TableSegmentAvailable, IndexSegmentsAvailable { 
     
    private TableSegment table; 
     
    private final Collection<ColumnDefinitionSegment> columnDefinitions = new LinkedList<>(); 
     
    private final Collection<IndexSegment> indexes = new LinkedList<>(); 
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

至此,我们通过一个示例解释了与填充操作相关的各个类之间的协作关系,如下所示的类图展示了这种协作关系的整体结构。

Drawing 9.png

SQLStatement类层结构图

有了上图的基础,我们理解填充引擎 SQLStatementFillerEngine 就显得比较简单了,SQLStatementFillerEngine 类的实现如下所示:

public final class SQLStatementFillerEngine { 
     
    private final ParseRuleRegistry parseRuleRegistry;     
    private final String databaseTypeName; 
    
    @SuppressWarnings("unchecked") 
    @SneakyThrows 
    public SQLStatement fill(final Collection<SQLSegment> sqlSegments, final int parameterMarkerCount, final SQLStatementRule rule) { 
     //从SQLStatementRule中获取SQLStatement实例,如CreateTableStatement 
     SQLStatement result = rule.getSqlStatementClass().newInstance(); 
        //通过断言对SQLStatement的合法性进行校验 
     Preconditions.checkArgument(result instanceof AbstractSQLStatement, "%s must extends AbstractSQLStatement", result.getClass().getName()); 
         
        //设置参数个数 
        ((AbstractSQLStatement) result).setParametersCount(parameterMarkerCount); 
       
        //添加所有的SQLSegment到SQLStatement中 
        result.getAllSQLSegments().addAll(sqlSegments); 
         
        //遍历填充对应类型的SQLSegment 
        for (SQLSegment each : sqlSegments) { 
         //根据数据库类型和SQLSegment找到对应的SQLSegmentFiller,并为SQLStatement填充SQLSegment 
            //如通过TableSegment找到获取TableFiller,然后通过TableFiller为CreateTableStatement填充TableSegment 
         Optional<SQLSegmentFiller> filler = parseRuleRegistry.findSQLSegmentFiller(databaseTypeName, each.getClass()); 
            if (filler.isPresent()) {                
              //利用SQLSegmentFiller来填充SQLStatement中的SQLSegment 
                filler.get().fill(each, result); 
            } 
        } 
        return result; 
	}  
} 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

我们对 SQLStatementFillerEngine 中的核心代码都添加了注释,注意到这里通过数据库类型以及 SQLSegment 的类型,从规则注册表 ParseRuleRegistry 中获取了对应的 SQLSegmentFiller 并完成对 SQLStatement 的填充操作。

至此,ShardingSphere 中 SQL 解析引擎的三大阶段介绍完毕。我们已经获取了目标 SQLStatement,为进行后续的路由等操作提供了基础。

从源码解析到日常开发

通过对框架源代码的学习,一方面可以帮忙我们更好地理解该框架核心功能背后的实现原理;另一方面,我们也可以吸收这些优秀框架的设计思想和实现方法,从而更好地指导日常开发工作。在本文中,我们同样总结了一组设计和实现上的技巧。

1.设计模式的应用方式

在本文中,我们主要涉及了两种设计模式的应用场景,一种是工厂模式,另一种是外观模式。

工厂模式的应用比较简单,作用也比较直接。例如,SQLParseEngineFactory 工厂类用于创建 SQLParseEngine,而 SQLParserFactory 工厂类用于创建 SQLParser。

相比工厂模式,外观类通常比较难识别和把握,因此,我们也花了一定篇幅介绍了 SQL 解析引擎中的外观类 SQLParseKernel,以及与 SQLParseEngine 之间的委托关系。

2.缓存的实现方式

缓存在 ShardingSphere 中应用非常广泛,其实现方式也比较多样,在本文中,我们就接触到了两种缓存的实现方式。

第一种是通过 ConcurrentHashMap 类来保存 SQLParseEngine 的实例,使用上比较简单。

另一种则基于 Guava 框架中的 Cache 类构建了一个 SQLParseResultCache 来保存 SQLStatement 对象。Guava 中的 Cache 类初始化方法如下所示,我们可以通过 put 和 getIfPresent 等方法对缓存进行操作:

Cache<String, SQLStatement> cache = CacheBuilder.newBuilder().softValues().initialCapacity(2000).maximumSize(65535).build();     
  • 1
  • 1
3.配置信息的两级管理机制

在 ShardingSphere 中,关于各种提取规则和填充规则的定义都放在了 XML 配置文件中,并采用了配置信息的两级管理机制。这种两级管理机制的设计思想在于,系统在提供了对各种通用规则默认实现的同时,也能够集成来自各种 SQL 方言的定制化规则,从而形成一套具有较高灵活性以及可扩展性的规则管理体系。

4.回调机制

所谓回调,本质上就是一种双向调用模式,也就是说,被调用方在被调用的同时也会调用对方。在实现上,我们可以提取一个用于业务接口作为一种 Callback 接口,然后让具体的业务对象去实现这个接口。这样,当外部对象依赖于这个业务场景时,只需要依赖这个 Callback 接口,而不需要关心这个接口的具体实现类。

这在软件设计和实现过程中是一种常见的消除业务对象和外部对象之间循环依赖的处理方式。ShardingSphere 中大量采用了这种实现方式来确保代码的可维护性,这非常值得我们学习。

小结

作为 ShardingSphere 分片引擎的第一个核心组件,解析引擎的目的在于生成 SQLStatement 目标对象。而整个解析引擎分成三大阶段,即生成 SQL 抽象语法树、提取 SQL 片段以及使用这些片段来填充 SQL 语句。本文对解析引擎的整体结构以及这三个阶段进行了详细的讨论。

最后给你留一道思考题:简要介绍 ShardingSphere 中 SQL 解析的各个阶段的输入和产出?欢迎你在留言区与大家讨论,我将一一点评解答。

现在,我们已经获取了 SQLStatement,接下来就可以用来执行 SQL 路由操作,这就是下一课时内容。


声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/757088
推荐阅读
相关标签
  

闽ICP备14008679号