1.需求来源几乎所有的业务系统都有生成唯一记录标识的需求,例如:消息标识:message-id订单标识:order-id帖子标识:tiezi-id本记录标识在数据库中往往是主键,会在数据库上建立一个聚簇索引(clusterindex),即在物理存储上按这个字段排序。对这条记录标识的查询往往有分页或排序的业务需求,例如:拉***一页消息selectmessage-id/orderbytime/limit100拉***一页订单selectorder-id/orderbytime/limit100拉***一页postsselecttiezi-id/orderbytime/limit100所以经常有time字段,在time字段上建立普通索引(非聚簇索引)。普通索引存储的是指向实际记录的指针,其访问效率会比聚簇索引慢。如果recordidentifiers在生成的时候基本上是按时间排序的,那么time字段的索引查询可以省略:selectmessage-id/(orderbymessage-id)/limit100强调,这样做的前提是message-id的生成趋势时间基本上增加。这就引出了记录标识生成的两个核心需求(也就是上面提到的三个XXX-id):全局唯一且有序的趋势这也是本文要讨论的核心问题:如何高效生成全局唯一的趋势和顺序ID。二、常用方法、不足及优化方法一:利用数据库的auto_increment生成全局唯一的增量ID。优点:简单,利用数据库已有的功能,保证唯一性和增量步骤。缺点:可用性难以保证:常见于数据库架构是一主多从+读写分离。生成自增ID是写请求。如果主库挂了,就不行了。扩展性差,性能上限:因为写入是单点,数据库主库的写入性能决定了ID生成性能的上限,难以扩展改善方法:主库冗余,避免写入单点数据水平切分,保证每个主库生成的ID不重复。如上图,一个写库变成三个写库,每个写库设置不同的auto_increment初值,相同的增长步长保证每个库生成的ID不同(上图中0号库生成0、3、6、9...,库1生成1、4、7、10,库2生成2、5、8、11...)改进后的架构保证了可用性,但缺点是:损失了ID生成的“绝对增量”:先访问0库生成0,3再访问1库生成1,可能会导致ID生成在极短时间内绝对不增加(这个问题不大,目标是趋势增加,不是绝对增加)数据库的写压力还是很大的,每次ID都要访问数据库为了解决上面两个问题,引入第二种通用??方案。方法二:单点批量ID生成服务分布式系统之所以难,最重要的原因之一就是“没有全局时钟,很难保证绝对定时”。如果要保证绝对定时,只能使用单点服务,使用本地时钟来保证“绝对定时”。数据库写压力大,因为每次生成ID都要访问数据库,可以批量降低数据库写压力。如上图所述,数据库采用双主来保证可用性,数据库中只存储当前ID的最大值,比如0。ID生成服务假设每次批量取6个ID,服务访问数据库,将当前ID的最大值修改为5,这样应用访问ID生成服务请求ID,ID生成服务不需要每次访问数据库。ID0、1、2、3、4、5依次分布。发送ID后,将ID的最大值改为11,可以重新分配ID6、7、8、9、10、11,这样数据库的压力就降低到原来的1/6原来的。优点:保证了ID生成的绝对递增顺序,大大减轻了数据库的压力。ID生成每秒可以生成几万个或者几十万个ID。缺点:服务还是单点。继续生成id可能不连续,中间会有空洞(服务内存存储0,1,2,3,4,5,数据库中max-id为5,分配到3时,服务重启,下次会从6开始分配,4和5变空了,不过这个问题不算太大)虽然每秒可以生成几万或者几十万的ID,但是有毕竟还是性能极限,不能横向扩展。改进方法:单点服务常见的高可用优化方案是“备用服务”,也称为“影子服务”,所以我们可以通过以下方法来优化上述缺点(一):如上图所示,对外提供的服务就是主服务,还有一个一直处于待机状态的影子服务,当主服务挂了,影子服务在上面。这个切换过程对调用者是透明的,可以自动完成。常用的技术是vip+keepalived,这里就不展开了。另外,ID-gen-service也可以实现水平扩展来解决上面的缺点(3),但是会造成一致性问题。具体解决方法见《浅谈CAS在分布式ID生成方案上的应用》。方法三:无论uuid/guid是通过数据库生成还是通过服务生成,业务端Application都需要远程调用,比较耗时。有没有办法在本地生成ID,性能高,延迟低?Uuid是一种常见的解决方案:stringID=GenUUID();优点:本地生成ID,不需要远程调用,扩展时延低性能好,基本没有性能上限缺点:不能保证趋势会增加。uuid太长,经常用字符串表示。建立索引查询作为主键是低效的。常见的优化方案是“转为两个uint64整型存储”或者“减半存储”(减半后不能保证唯一性)方法四:取当前毫秒uuid是一种本地算法,生成性能高,但不能保证趋势会增加,作为字符串ID检索效率低。有没有本地算法可以保证增加?算法呢?获取当前毫秒数是一种常见的解决方案:uint64ID=GenTimeMS();优点:本地生成ID,无需远程调用,时延低。生成的ID趋于增加。生成的ID为整数,建立索引后查询效率高缺点:如果并发超过1000,会产生重复的ID。这个缺点是致命的,不能保证ID的唯一性。当然,使用微秒可以降低碰撞的概率,但每秒最多只能生成1,000,000个ID。多了就会有冲突,所以用微秒并不能从根本上解决问题。方法五:Snowflake-like算法Snowflake是twitter开源的一种分布式ID生成算法。它的核心思想是一个long类型的ID:41bit作为毫秒数,10bit作为机器号,12bit作为毫秒内的序号。可以生成1000*(2^12),即400W个ID,完全可以满足业务需求。借鉴snowflake的思想,结合各个公司的业务逻辑和并发度,可以实现自己的分布式ID生成算法。例如,假设某公司ID生成器服务需求如下:单机峰值并发量小于1W,预计未来5年内单机峰值并发量小于10W年。机房2个,未来5年机房数量预计在4个以内。100台目前有5个业务线有ID生成需求,预计业务线数量为以后不到10...分析过程如下:然后上线),假设系统至少运行10年,至少需要10年*365天*24小时*3600秒*1000毫秒=320*10^9,差不多39位预留给单机每秒毫秒数的峰值并发量小于10W,即单机每毫秒的峰值并发量为小于100。几乎每毫秒为序列号保留7位。5年内机房数量少于4个,机房预留2位。每个机房不到100台机器。每个机房预留7位作为服务器标识。业务线数小于10条,业务线标识预留4位。这样设计的64位标识,可以保证每个业务线、每个机房、每台机器生成的ID都不一样。同一台机器,每毫秒产生的ID是不同的。在同一台机器上,同一毫秒内,通过序号来区分生成的ID,保证生成的ID是不同的。把毫秒数放在***里,保证生成的ID不一样。ID的缺点是递增趋势:由于“没有全局时钟”,每个服务器分配的ID绝对是递增的,但是从全局来看,生成的ID只是呈递增趋势(有的服务器早,有的服务器晚))【本文为专栏作家《58神剑》原创稿件,转载请联系原作者】点此阅读更多本作者好文
