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