ShardingSphere源码解析之分布式主键

上一篇中,我们提到了在ShardingRouter的route方法中,当输入的SQLStatement是InsertStatement时,会自动生成主键,代码如下所示:

//如果是InsertStatement则自动生成主键

Optional<GeneratedKey> generatedKey = sqlStatement instanceof InsertStatement

                ? GeneratedKey.getGenerateKey(shardingRule, metaData.getTables(), parameters, (InsertStatement) sqlStatement) : Optional.<GeneratedKey>absent();

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

我们基于前面的这段代码来到GeneratedKey类,请注意该类位于sharding-core-route工程的org.apache.shardingsphere.core.route.router.sharding.keygen包下。我们先看该类提供的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()));

}

这段代码的逻辑在于先从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;

    }

在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.");

        }

        ShardingKeyGenerator shardingKeyGenerator = null == tableRule.get().getShardingKeyGenerator() ? defaultShardingKeyGenerator : tableRule.get().getShardingKeyGenerator();

        return shardingKeyGenerator.generateKey();

    }

我们首先根据传入的logicTableName找到对应的TableRule,基于TableRule找到其包含的ShardingKeyGenerator,然后通过ShardingKeyGenerator的generateKey来生成主键。从设计模式上讲,ShardingRule也只是一个门面(Facade)类,真正创建ShardingKeyGenerator的过程应该是在TableRule中。

我们来到TableRule中,终于在它的一个构造函数中找到了ShardingKeyGenerator的创建过程,代码如下所示:

shardingKeyGenerator = containsKeyGeneratorConfiguration(tableRuleConfig)

                ? new ShardingKeyGeneratorServiceLoader().newService(tableRuleConfig.getKeyGeneratorConfig().getType(), tableRuleConfig.getKeyGeneratorConfig().getProperties()) : null;

这里我们看到有一个ShardingKeyGeneratorServiceLoader类,该类定义如下:

public final class ShardingKeyGeneratorServiceLoader extends TypeBasedSPIServiceLoader<ShardingKeyGenerator> {   

    static {

        NewInstanceServiceLoader.register(ShardingKeyGenerator.class);

    }   

    public ShardingKeyGeneratorServiceLoader() {

        super(ShardingKeyGenerator.class);

    }

}

回顾《ShardingSphere源码解析之微内核架构(下)》一文中的介绍,我们不难理解ShardingKeyGeneratorServiceLoader类的作用。ShardingKeyGeneratorServiceLoader继承了TypeBasedSPIServiceLoader类,然后在静态方法中通过NewInstanceServiceLoader注册了类路径中所有的ShardingKeyGenerator。然后,ShardingKeyGeneratorServiceLoader的newService方法基于类型通过SPI创建实例,并赋值Properties属性。

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

然后,我们在sharding-core-common工程的META-INF/services目录中看到了如下所示的SPI定义:

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

在理解了SPI实例记载机制的基础上,接下来我们分析ShardingKeyGenerator接口,该接口继承了TypeBasedSPI接口,其定义如下所示:

public interface ShardingKeyGenerator extends TypeBasedSPI {   

    Comparable<?> generateKey();

}

在ShardingSphere中,ShardingKeyGenerator接口存在一批实现类,在4.0.1版本中,除了前面提到的SnowflakeShardingKeyGenerator和UUIDShardingKeyGenerator,还实现了LeafSegmentKeyGenerator和LeafSnowflakeKeyGenerator类,这两个类位于sharding-orchestration-core工程的org.apache.shardingsphere.orchestration.internal.keygen包中。

我们先来看最简单的ShardingKeyGenerator,即UUIDShardingKeyGenerator。UUIDShardingKeyGenerator的实现非常简单,直接采用UUID.randomUUID()的方式产生分布式主键。UUIDShardingKeyGenerator的代码如下所示:

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("-", "");

    }

}

然后,我们再来看SnowFlake(雪花)算法,SnowFlake是ShardingSphere默认的分布式主键生成策略。

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

在上图中,我们可以吧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算法依赖于时间戳,所以还有一种场景我们需要考虑,即时钟回拨。服务器时钟回拨会导致产生重复序列,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工作。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;       

        //先将当前时间戳左移放到完成41bit,然后将工作进程为左移到10bit,再将序号为放到最后的12bit

        //最后拼接起来成一个64 bit的二进制数字

        return ((currentMilliseconds - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (getWorkerId() << WORKER_ID_LEFT_SHIFT_BITS) | sequence;

}

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

更多内容可以关注我的公众号:程序员向架构师转型。

发布了112 篇原创文章 · 获赞 12 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/lantian08251/article/details/104569238