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。java
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秒 | 10万 | - | - | - | - | - |
1分 | 16.6万 | 1000万 | - | - | - | - |
1小时 | 27.7万 | 1666万 | 10亿 | - | - | - |
1天 | 115万 | 6944万 | 41.66亿 | 1000亿 | - | - |
1月 | 165万 | 9920万 | 59.5亿 | 3571.43亿 | 10万亿 | - |
1年 | 3215万 | 19亿 | 1157亿 | 2.7万亿 | 83万亿 | 1000万亿 |
以1分钟为例进行说明,假设初始值为2016011815341200000,一分钟后宕机瞬间重启,对应的初始值为2016011815351200000,这个差额是10000000,对应的每秒交易量为16.6万。 从上图来看,对于这样的规则,能承载的交易量仍是很大的,当今世界最繁忙的交易系统,也不会超过这个极限状况下的极值,也就是说,这个规则就目前来讲,具备广泛适用性。 而在实际生产中,瞬间重启是不存在的,随着重启时间向后推移,新的初始值会和原来的最大值拉开差距,更不可能出现冲突了。 关于性能,我在一台2011年的旧笔记本上进行测试,很容易就能达到50K/s的序列生成速度,这个仍是能够的,可是这是在开启预留的前提下实现的,若是不开启预留,性能可能降低到13K/s。在一个具体的集群环境下,一般不会拿Ignite单独创建服务作ID分发中心,因此实际环境下性能能不能知足需求,开发者须要自行进行测试,评估而后作选择。另外,开启了预留会致使最终生成的ID可能不是随时间线性增加的,这个也须要注意。网络
前述的基于Ignite的分布式ID生成器,优势是实现简单,将一个jar包嵌入应用后ID生成系统和应用生命周期一致,设置了备份后不存在单点故障,数值线性递增可比较大小,规则按照业务定制后能够作得更短,若是转成十六进制后,会很是短,不依赖数据库,不对数据库产生压力,缺点可能就是性能以及一些特定的业务需求了。 生成全局惟一ID的需求是刚性的,尤为是分布式环境中,问题显得尤其复杂。当前,这方面的实现方案很是多,通用的不通用的,本文不作详细的论述,只作简单的列举:数据结构
分布式ID生成策略有不少的实现方案,各有优缺点,本文又提出了一个基于Apache Ignite的新方案,应该说没有最完美的,只有最符合实际业务需求的,开发者须要作的就是作详细的、综合的比较,而后选择最适合本身的方案。分布式