ID是数据的惟一标识,传统的作法是利用UUID和数据库的自增ID,在互联网企业中,大部分公司使用的都是Mysql,而且由于须要事务支持,因此一般会使用Innodb存储引擎,UUID太长以及无序,因此并不适合在Innodb中来做为主键,自增ID比较合适,可是随着公司的业务发展,数据量将愈来愈大,须要对数据进行分表,而分表后,每一个表中的数据都会按本身的节奏进行自增,颇有可能出现ID冲突。这时就须要一个单独的机制来负责生成惟一ID,生成出来的ID也能够叫作分布式ID,或全局ID。下面来分析各个生成分布式ID的机制。 java
第一种方案仍然仍是基于数据库的自增ID,须要单独使用一个数据库实例,在这个实例中新建一个单独的表:mysql
表结构以下:git
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;
复制代码
可使用下面的语句生成并获取到一个自增IDgithub
begin;
replace into SEQUENCE_ID (stub) VALUES ('anyword');
select last_insert_id();
commit;
复制代码
stub字段在这里并无什么特殊的意义,只是为了方便的去插入数据,只有能插入数据才能产生自增id。而对于插入咱们用的是replace,replace会先看是否存在stub指定值同样的数据,若是存在则先delete再insert,若是不存在则直接insert。redis
这种生成分布式ID的机制,须要一个单独的Mysql实例,虽然可行,可是基于性能与可靠性来考虑的话都不够,业务系统每次须要一个ID时,都须要请求数据库获取,性能低,而且若是此数据库实例下线了,那么将影响全部的业务系统。算法
为了解决数据库可靠性问题,咱们可使用第二种分布式ID生成方案。sql
若是咱们两个数据库组成一个主从模式集群,正常状况下能够解决数据库可靠性问题,可是若是主库挂掉后,数据没有及时同步到从库,这个时候会出现ID重复的现象。咱们可使用双主模式集群,也就是两个Mysql实例都能单独的生产自增ID,这样可以提升效率,可是若是不通过其余改造的话,这两个Mysql实例极可能会生成一样的ID。须要单独给每一个Mysql实例配置不一样的起始值和自增步长。docker
第一台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,要怎么操做呢? 第一,mysql一、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,4,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的分配以下图:
根据这个算法的逻辑,只须要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用能够直接使用该工具方法来获取分布式ID,只需保证每一个业务应用有本身的工做机器id便可,而不须要单独去搭建一个获取分布式ID的应用。
snowflake算法实现起来并不难,提供一个github上用java实现的:github.com/beyondfengy…
在大厂里,其实并无直接使用snowflake,而是进行了改造,由于snowflake算法中最难实践的就是工做机器id,原始的snowflake算法须要人工去为每台机器去指定一个机器id,并配置在某个地方从而让snowflake今后处获取机器id。
可是在大厂里,机器是不少的,人力成本太大且容易出错,因此大厂对snowflake进行了改造。
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地址:Leaf
美团的Leaf也是一个分布式ID生成框架。它很是全面,即支持号段模式,也支持snowflake模式。号段模式这里就不介绍了,和上面的分析相似。
Leaf中的snowflake模式和原始snowflake算法的不一样点,也主要在workId的生成,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每一个应用在使用Leaf-snowflake时,在启动时都会都在Zookeeper中生成一个顺序Id,至关于一台机器对应一个顺序节点,也就是一个workId。
总得来讲,上面两种都是自动生成workId,以让系统更加稳定以及减小人工成功。
这里额外再介绍一下使用Redis来生成分布式ID,其实和利用Mysql自增ID相似,能够利用Redis中的incr命令来实现原子性的自增与返回,好比:
127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增长1,并返回
(integer) 2
127.0.0.1:6379> incr seq_id // 增长1,并返回
(integer) 3
复制代码
使用redis的效率是很是高的,可是要考虑持久化的问题。Redis支持RDB和AOF两种持久化的方式。
RDB持久化至关于定时打一个快照进行持久化,若是打完快照后,连续自增了几回,还没来得及作下一次快照持久化,这个时候Redis挂掉了,重启Redis后会出现ID重复。
AOF持久化至关于对每条写命令进行持久化,若是Redis挂掉了,不会出现ID重复的现象,可是会因为incr命令过得,致使重启恢复数据时间过长。
想学习更多的分布式技术,请添加微信公众号:1点25