JDK在1.5版本之后,提供了java.util.concurrent包,其中java.util.concurrent.atomic子包中包含了对于单一变量的线程安全的支持lock-free的编程实现。该包中的类,比如AtomicLong
,提供了和Long类型相对应的原子化操作,比如一些increment方法,基于这些功能,是可以开发出单JVM的序列生成器这样的功能的,但是对于分布式环境,则无能为力。
在Ignite中,除了提供标准的基于键-值的类似于Map的存储以外,还提供了一种分布式数据结构的实现,其中包括:IgniteAtomicLong
, IgniteSet
, IgniteQueue
, IgniteAtomicReference
, IgniteAtomicSequence
, IgniteCountDownLatch
, IgniteSemaphore
,这些类除了提供和JDK相同的功能外,就是增加了对分布式环境的支持,也就是支持集群范围内的原子化操作。
鉴于本文重点是讨论分布式ID生成器,所有下文的重点在于IgniteAtomicSequence。
IgniteAtomicSequence接口提供了分布式的原子性序列,类似于分布式原子性的Long类型,但是他的值只能增长,他特有的功能是支持预留一定范围的序列值,来避免每次序列获取下一个值时都需要的昂贵的网络消耗和缓存更新,也就是,当在一个原子性序列上执行了incrementAndGet()(或者任何其他的原子性操作),数据结构会往前预留一定范围的序列值,他会保证对于这个序列实例来说跨集群的唯一性。
这个类型的使用是非常简单的,相关代码如下:
Ignite ignite = Ignition.start(); IgniteAtomicSequence seq = ignite.atomicSequence("seqName",//序列名 0, //初始值 true//如果序列不存在则创建 ); for (int i = 0; i < 20; i++) { long currentValue = seq.get();//获取当前值 long newValue = seq.incrementAndGet();//先加1再取值 ... }
这个样例中创建的seq,初始值从0开始,然后递增,看上去很完美,但是当系统不管什么原因重启后,就又会从0开始,这显然是无法保证唯一性的,因此这个方法还是不能在生产环境下使用。
按照前述,直接按照初始值0创建IgniteAtomicSequence,是有很大风险的,无法在生产环境下使用,而且存在长度不固定问题,所以还需要进一步想办法,研究的重点在于解决初始值的问题。
因为IgniteAtomicSequence的值为long型,而在Java中long类型的最大值是9223372036854775807,这个数值长度为19位,对于实际应用来说,是一个很大的值,但是对于常见的没有环境依赖的ID生成器来说,还是比较短的。因此我们打算在这方面做文章。
因为系统重置的一个重要指标就是时间,那么我们以时间作为参照,然后加上一个扩展,可能是一个比较理想的选择,我们以如下的规则作为初始值:
时间的yyyyMMddHHmmss+00000
这个长度正好是19位,然后每次加1,因为现在是2016年,这个规则在常规应用场景中,是不会超过long类型的最大值的。
但是,这个规则存在一个风险,就是假设不考虑实际应用和实际性能,如果增加操作业务量特别大,会使这个序列值快速进位,如果某个时间节点宕机后瞬间重启,是有可能存在重启后的初始值小于原来的最大值的,这时就无法保证唯一性了。下面就对这个理论情况下的最大值做一个计算,然后开发者就会知道在自己的应用中如何改进这个规则以满足个性化需求了。
假定不考虑实际性能,我们以最简单的情况为例,就是启动后一秒钟内访问达到峰值,然后宕机后瞬间重启这种情况,这个很容易就能看出来,不需要计算,就是5个0对应的最大值10万,以此类推,考虑到时间的进位和十进制进位的不同,我们可以计算出一分钟后、一小时后、一天后、一月后、一年后宕机换算出的交易量的极大值,如下:
以1分钟为例进行说明,假设初始值为2016011815341200000,一分钟后宕机瞬间重启,对应的初始值为2016011815351200000,这个差额是10000000,对应的每秒交易量为16.6万。
从上图来看,对于这样的规则,能承载的交易量还是很大的,当今世界最繁忙的交易系统,也不会超过这个极限情况下的极值,也就是说,这个规则就目前来说,具有普遍适用性。
而在实际生产中,瞬间重启是不存在的,随着重启时间向后推移,新的初始值会和原来的最大值拉开差距,更不可能出现冲突了。
关于性能,我在一台2011年的旧笔记本上进行测试,很容易就能达到50K/s的序列生成速度,这个还是可以的,但是这是在开启预留的前提下实现的,如果不开启预留,性能可能下降到13K/s。在一个具体的集群环境下,通常不会拿Ignite单独建立服务做ID分发中心,所以实际环境下性能能不能满足需求,开发者需要自行进行测试,评估然后做选择。另外,开启了预留会导致最终生成的ID可能不是随时间线性增长的,这个也需要注意。
前述的基于Ignite的分布式ID生成器,优点是实现简单,将一个jar包嵌入应用后ID生成系统和应用生命周期一致,设置了备份后不存在单点故障,数值线性递增可比较大小,规则按照业务定制后可以做得更短,如果转成十六进制后,会非常短,不依赖数据库,不对数据库产生压力,缺点可能就是性能以及一些特定的业务需求了。
生成全局唯一ID的需求是刚性的,尤其是分布式环境中,问题显得尤为复杂。当前,这方面的实现方案非常多,通用的不通用的,本文不做详细的论述,只做简单的列举:
分布式ID生成策略有很多的实现方案,各有优缺点,本文又提出了一个基于Apache Ignite的新方案,应该说没有最完美的,只有最符合实际业务需求的,开发者需要做的就是做详细的、综合的比较,然后选择最适合自己的方案。