ID是数据的惟一标识,传统的做法是利用UUID和数据库的自增ID,在互联网企业中,大部分公司应用的都是Mysql,并且因为须要事务反对,所以通常会应用Innodb存储引擎,UUID太长以及无序,所以并不适宜在Innodb中来作为主键,自增ID比拟适合,然而随着公司的业务倒退,数据量将越来越大,须要对数据进行分表,而分表后,每个表中的数据都会按本人的节奏进行自增,很有可能呈现ID抵触。这时就须要一个独自的机制来负责生成惟一ID,生成进去的ID也能够叫做分布式ID,或全局ID。上面来剖析各个生成分布式ID的机制。

这篇文章并不会剖析的特地具体,次要是做一些总结,当前再出一些具体某个计划的文章。

数据库自增ID

第一种计划依然还是基于数据库的自增ID,须要独自应用一个数据库实例,在这个实例中新建一个独自的表:

表构造如下:

CREATE DATABASE `SEQID`;CREATE TABLE SEQID.SEQUENCE_ID (    id bigint(20) unsigned NOT NULL auto_increment,     stub char(10) NOT NULL default '',    PRIMARY KEY (id),    UNIQUE KEY stub (stub)) ENGINE=MyISAM;

能够应用上面的语句生成并获取到一个自增ID

begin;replace into SEQUENCE_ID (stub) VALUES ('anyword');select last_insert_id();commit;

stub字段在这里并没有什么非凡的意义,只是为了不便的去插入数据,只有能插入数据能力产生自增id。而对于插入咱们用的是replace,replace会先看是否存在stub指定值一样的数据,如果存在则先delete再insert,如果不存在则间接insert。

这种生成分布式ID的机制,须要一个独自的Mysql实例,尽管可行,然而基于性能与可靠性来思考的话都不够,业务零碎每次须要一个ID时,都须要申请数据库获取,性能低,并且如果此数据库实例下线了,那么将影响所有的业务零碎。

为了解决数据库可靠性问题,咱们能够应用第二种分布式ID生成计划。

数据库多主模式

如果咱们两个数据库组成一个主从模式集群,失常状况下能够解决数据库可靠性问题,然而如果主库挂掉后,数据没有及时同步到从库,这个时候会呈现ID反复的景象。咱们能够应用双主模式集群,也就是两个Mysql实例都能独自的生产自增ID,这样可能提高效率,然而如果不通过其余革新的话,这两个Mysql实例很可能会生成同样的ID。须要独自给每个Mysql实例配置不同的起始值和自增步长。

第一台Mysql实例配置:

set @@auto_increment_offset = 1;     -- 起始值set @@auto_increment_increment = 2;  -- 步长

第二台Mysql实例配置:

set @@auto_increment_offset = 2;     -- 起始值set @@auto_increment_increment = 2;  -- 步长

通过下面的配置后,这两个Mysql实例生成的id序列如下: mysql1,起始值为1,步长为2,ID生成的序列为:1,3,5,7,9,... mysql2,起始值为2,步长为2,ID生成的序列为:2,4,6,8,10,...

对于这种生成分布式ID的计划,须要独自新增一个生成分布式ID利用,比方DistributIdService,该利用提供一个接口供业务利用获取ID,业务利用须要一个ID时,通过rpc的形式申请DistributIdService,DistributIdService随机去下面的两个Mysql实例中去获取ID。

履行这种计划后,就算其中某一台Mysql实例下线了,也不会影响DistributIdService,DistributIdService依然能够利用另外一台Mysql来生成ID。

然而这种计划的扩展性不太好,如果两台Mysql实例不够用,须要新增Mysql实例来进步性能时,这时就会比拟麻烦。

当初如果要新增一个实例mysql3,要怎么操作呢? 第一,mysql1、mysql2的步长必定都要批改为3,而且只能是人工去批改,这是须要工夫的。 第二,因为mysql1和mysql2是不停在自增的,对于mysql3的起始值咱们可能要定得大一点,以给充沛的工夫去批改mysql1,mysql2的步长。 第三,在批改步长的时候很可能会呈现反复ID,要解决这个问题,可能须要停机才行。

为了解决下面的问题,以及可能进一步提高DistributIdService的性能,如果应用第三种生成分布式ID机制。

号段模式

咱们能够应用号段的形式来获取自增ID,号段能够了解成批量获取,比方DistributIdService从数据库获取ID时,如果能批量获取多个ID并缓存在本地的话,那样将大大提供业务利用获取ID的效率。

比方DistributIdService每次从数据库获取ID时,就获取一个号段,比方(1,1000],这个范畴示意了1000个ID,业务利用在申请DistributIdService提供ID时,DistributIdService只须要在本地从1开始自增并返回即可,而不须要每次都申请数据库,始终到本地自增到1000时,也就是以后号段曾经被用完时,才去数据库从新获取下一号段。

所以,咱们须要对数据库表进行改变,如下:

CREATE TABLE id_generator (  id int(10) NOT NULL,  current_max_id bigint(20) NOT NULL COMMENT '以后最大id',  increment_step int(10) NOT NULL COMMENT '号段的长度',  PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;

这个数据库表用来记录自增步长以及以后自增ID的最大值(也就是以后曾经被申请的号段的最初一个值),因为自增逻辑被移到DistributIdService中去了,所以数据库不须要这部分逻辑了。

这种计划不再强依赖数据库,就算数据库不可用,那么DistributIdService也能持续撑持一段时间。然而如果DistributIdService重启,会失落一段ID,导致ID空洞。

为了进步DistributIdService的高可用,须要做一个集群,业务在申请DistributIdService集群获取ID时,会随机的抉择某一个DistributIdService节点进行获取,对每一个DistributIdService节点来说,数据库连贯的是同一个数据库,那么可能会产生多个DistributIdService节点同时申请数据库获取号段,那么这个时候须要利用乐观锁来进行管制,比方在数据库表中减少一个version字段,在获取号段时应用如下SQL:

update id_generator set current_max_id=#{newMaxId}, version=version+1 where version = #{version}

因为newMaxId是DistributIdService中依据oldMaxId+步长算进去的,只有下面的update更新胜利了就示意号段获取胜利了。

为了提供数据库层的高可用,须要对数据库应用多主模式进行部署,对于每个数据库来说要保障生成的号段不反复,这就须要利用最开始的思路,再在刚刚的数据库表中减少起始值和步长,比方如果当初是两台Mysql,那么 mysql1将生成号段(1,1001],自增的时候序列为1,3,5,7.... mysql1将生成号段(2,1002],自增的时候序列为2,4,6,8,10...

更具体的能够参考滴滴开源的TinyId:github.com/didi/tinyid…

在TinyId中还减少了一步来提高效率,在下面的实现中,ID自增的逻辑是在DistributIdService中实现的,而实际上能够把自增的逻辑转移到业务利用本地,这样对于业务利用来说只须要获取号段,每次自增时不再须要申请调用DistributIdService了。

雪花算法

下面的三种办法总的来说是基于自增思维的,而接下来就介绍比拟驰名的雪花算法-snowflake。

咱们能够换个角度来对分布式ID进行思考,只有能让负责生成分布式ID的每台机器在每毫秒内生成不一样的ID就行了。

snowflake是twitter开源的分布式ID生成算法,是一种算法,所以它和下面的三种生成分布式ID机制不太一样,它不依赖数据库。

核心思想是:分布式ID固定是一个long型的数字,一个long型占8个字节,也就是64个bit,原始snowflake算法中对于bit的调配如下图:

  • 第一个bit位是标识局部,在java中因为long的最高位是符号位,负数是0,正数是1,个别生成的ID为负数,所以固定为0。
  • 工夫戳局部占41bit,这个是毫秒级的工夫,个别实现上不会存储以后的工夫戳,而是工夫戳的差值(以后工夫-固定的开始工夫),这样能够使产生的ID从更小值开始;41位的工夫戳能够应用69年,(1L << 41) / (1000L 60 60 24 365) = 69年
  • 工作机器id占10bit,这里比拟灵便,比方,能够应用前5位作为数据中心机房标识,后5位作为单机房机器标识,能够部署1024个节点。
  • 序列号局部占12bit,反对同一毫秒内同一个节点能够生成4096个ID

依据这个算法的逻辑,只须要将这个算法用Java语言实现进去,封装为一个工具办法,那么各个业务利用能够间接应用该工具办法来获取分布式ID,只需保障每个业务利用有本人的工作机器id即可,而不须要独自去搭建一个获取分布式ID的利用。

snowflake算法实现起来并不难,提供一个github上用java实现的:github.com/beyondfengy…

在大厂里,其实并没有间接应用snowflake,而是进行了革新,因为snowflake算法中最难实际的就是工作机器id,原始的snowflake算法须要人工去为每台机器去指定一个机器id,并配置在某个中央从而让snowflake从此处获取机器id。

然而在大厂里,机器是很多的,人力老本太大且容易出错,所以大厂对snowflake进行了革新。

百度(uid-generator)

github地址:uid-generator

uid-generator应用的就是snowflake,只是在生产机器id,也叫做workId时有所不同。

uid-generator中的workId是由uid-generator主动生成的,并且思考到了利用部署在docker上的状况,在uid-generator中用户能够本人去定义workId的生成策略,默认提供的策略是:利用启动时由数据库调配。说的简略一点就是:利用在启动时会往数据库表(uid-generator须要新增一个WORKER_NODE表)中去插入一条数据,数据插入胜利后返回的该数据对应的自增惟一id就是该机器的workId,而数据由host,port组成。

对于uid-generator中的workId,占用了22个bit位,工夫占用了28个bit位,序列化占用了13个bit位,须要留神的是,和原始的snowflake不太一样,工夫的单位是秒,而不是毫秒,workId也不一样,同一个利用每重启一次就会生产一个workId。

具体可参考github.com/baidu/uid-g…

美团(Leaf)

github地址:Leaf

美团的Leaf也是一个分布式ID生成框架。它十分全面,即反对号段模式,也反对snowflake模式。号段模式这里就不介绍了,和下面的剖析相似。

Leaf中的snowflake模式和原始snowflake算法的不同点,也次要在workId的生成,Leaf中workId是基于ZooKeeper的程序Id来生成的,每个利用在应用Leaf-snowflake时,在启动时都会都在Zookeeper中生成一个程序Id,相当于一台机器对应一个程序节点,也就是一个workId。

总结

总得来说,下面两种都是主动生成workId,以让零碎更加稳固以及缩小人工胜利。

Redis

这里额定再介绍一下应用Redis来生成分布式ID,其实和利用Mysql自增ID相似,能够利用Redis中的incr命令来实现原子性的自增与返回,比方:

127.0.0.1:6379> set seq_id 1     // 初始化自增ID为1OK127.0.0.1:6379> incr seq_id      // 减少1,并返回(integer) 2127.0.0.1:6379> incr seq_id      // 减少1,并返回(integer) 3

应用redis的效率是十分高的,然而要思考长久化的问题。Redis反对RDB和AOF两种长久化的形式。

RDB长久化相当于定时打一个快照进行长久化,如果打完快照后,间断自增了几次,还没来得及做下一次快照长久化,这个时候Redis挂掉了,重启Redis后会呈现ID反复。

AOF长久化相当于对每条写命令进行长久化,如果Redis挂掉了,不会呈现ID反复的景象,然而会因为incr命令过得,导致重启复原数据工夫过长。