前言
业务量小于500W或数据容量小于2G的时候单独一个mysql即可提供服务,再大点的时候就进行读写分离也可以应付过来。但当主从同步也扛不住的时候就需要分表分库了,但分库分表后需要有一个唯一ID来标识一条数据,且这个唯一ID还必须有规则,能辅助我们解决分库分表的一些问题。
数据库的自增ID显然不能满足需求;特别一点的如订单、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的,那么这个全局唯一ID就叫分布式ID。
分布式ID需满足那些条件
- 全局唯一:基本要求就是必须保证ID是全局性唯一的。
- 高性能:高可用低延时,ID生成响应要快。
- 高可用:无限接近于100%的可用性
- 好接入:遵循拿来主义原则,在系统设计和实现上要尽可能的简单
- 趋势递增:最好趋势递增,这个要求就得看具体业务场景了,一般不严格要求
UUID
UUID 是指Universally Unique Identifier,翻译为中文是通用唯一识别码,UUID 的目的是让分布式系统中的所有元素都能有唯一的识别信息。形式为 8-4-4-4-12,总共有 36个字符。用起来非常简单
public static void main(String[] args) {
String uuid = UUID.randomUUID().toString().replaceAll("-","");
System.out.println(uuid);
}
输出结果 99a7d0925b294a53b2f4db9d5a3fb798,但UUID却并不适用于实际的业务需求。订单号用UUID这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;而对于数据库来说用作业务主键ID,它不仅是太长还是字符串,存储性能差查询也很耗时,所以不推荐用作分布式ID。
优点: 生成足够简单,本地生成无网络消耗,具有唯一性
缺点: 无序的字符串,不具备趋势自增特性,没有具体的业务含义。如此长的字符串当MySQL主键并非明智选择。
数据库自增ID
基于数据库的auto_increment自增ID完全可以充当分布式ID,具体实现:需要一个单独的MySQL实例用来生成ID,建表结构如下:
CREATE TABLE SoWhat_ID (
`id` bigint(20) unsigned NOT NULL auto_increment,
`value` char(10) NOT NULL default '',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
) ENGINE=MyISAM;
当我们需要一个ID的时候,向表中插入一条记录返回主键ID,但这种方式有一个比较致命的缺点,访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!
优点 : 实现简单,ID单调自增,数值类型查询速度快
缺点: DB单点存在宕机风险,无法扛住高并发场景
数据库集群模式
前面说了单点数据库方式不可取,那对上边的方式做一些高可用优化,换成主从模式集群。害怕一个主节点挂掉没法用,那就做双主模式集群,也就是两个Mysql实例都能单独的生产自增ID。那这样还会有个问题,两个MySQL实例的自增ID都从1开始,会生成重复的ID怎么办?解决方案:设置起始值和自增步长
MySQL_1 配置:
set @@auto_increment_offset = 1; -- 起始值
set @@auto_increment_increment = 2; -- 步长
MySQL_2 配置:
set @@auto_increment_offset = 2; -- 起始值
set @@auto_increment_increment = 2; -- 步长
这样两个MySQL实例的自增ID分别就是:
1、3、5、7、9
2、4、6、8、10
但是如果两个还是无法满足咋办,需要进行扩容呢?
增加第三台MySQL实例需要人工修改一、二两台MySQL实例的起始值和步长,把第三台机器的ID起始生成位置设定在比现有最大自增ID的位置远一些,但必须在一、二两台MySQL实例ID还没有增长到第三台MySQL实例的起始ID值的时候,否则自增ID就要出现重复了,必要时可能还需要停机修改。
优点: 解决DB单点问题
缺点: 不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。
Redis模式
Redis 也同样可以实现,原理就是Redis 是单线程的,因此我们可以利用redis的incr命令实现ID的原子性自增。
127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id // 增加1,并返回递增后的数值
(integer) 2
实现思路,大致和上面讲数据库的原理类似。用redis实现需要注意一点,要考虑到redis持久化的问题。redis有两种持久化方式RDB和AOF。
号段模式
号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库(当然这边存储层也可用其他的,比如redis、Mongdb等)批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。表结构如下:
CREATE TABLE id_generator (
`id` int(10) NOT NULL,
`max_id` bigint(20) NOT NULL COMMENT '当前最大id',
`step` int(20) NOT NULL COMMENT '号段的步长',
`biz_type` int(20) NOT NULL COMMENT '业务类型',
`version` int(20) NOT NULL COMMENT '版本号',
PRIMARY KEY (`id`)
)
- max_id :当前最大的可用id
- step :代表号段的长度
- biz_type :代表不同业务类型
- version :是一个乐观锁,每次都更新version,保证并发时数据的正确性
等这批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作
update max_id= max_id + step
update成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]。
由于多业务端可能同时操作,所以采用版本号 version 乐观锁方式更新。
优点: 这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多
缺点: 如果遇到了双十一或者秒杀类似的活动还是会对数据库有比较高的访问,且如果再申请新号段的时候,遇到数据库不可用时,ID生成也会出现问题。
雪花算法模式
SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,为什么叫雪花算法呢?众所周知世界上没有一对相同的雪花。雪花算法基本上保持自增的。
这 64 个 bit 中,其中1个bit是不用的,然后用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。举例如上图:
-
第一个部分是1个bit:0,这个是无意义的。因为二进制里第一个 bit 位如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。
-
第二个部分是41个bit:表示的是时间戳。单位是毫秒。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示 69 年的时间。
-
第三个部分是10个bit: 5个bit代表机房,意思就是最多代表25个机房(32个机房)。另5个bit代表机器id,意思就是每个机房里可以代表25个机器(32台机器)
-
第四个部分是12个bit:表示自增序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号。12bit可以代表的最大正整数是2^12-1=4096,也就是说可以用这个12bit代表的数字来区分同一个毫秒内的4096个不同的id。
总结的来说,就是用一个 64 bit位来设置不同的标志位,区分每一个 id。
SnowFlake 算法的实现代码 https://github.com/souyunku/SnowFlake
优点
- 高性能,本地通过位运算生成,效率快
- 高可用,本地生成无节点宕机情况发生
- 容量大,每秒中能生成数百万的自增ID
- ID自增:存入数据库中,索引效率高
缺点
- 依赖与系统时间的一致性,如果系统时间被回调,或者改变可能会造成id冲突或者重复。
实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化成业务ID或者业务系统实例数
后面讲讲业内一些公司基于以上几种最基本的ID生成方式,进行的部分优化。
百度uid-generator
uid-generator是由百度技术部开发,基于Snowflake算法实现,与原始snowflake算法不同在于,uid-generator支持自定义时间戳、workId和 序列号等各部分的位数。提供了两种方式,看下面分析
DefaultUidGenerator
DefaultUidGenerator 采用用户自定义workId的生成策略。大致原理如下:
需要新增一个WORKER_NODE表,当应用启动时会向数据库表中插入一条数据,插入成功后返回的自增ID就是workId。
对于时钟回拨的问题,DefaultUidGenerator采用了比较简单粗暴的方式,直接抛出错误
由上图可知,UidGenerator的时间部分只有28位,这就意味着UidGenerator默认只能承受8.5年(2^28-1/86400/365)。当然,根据你业务的需求,UidGenerator可以适当调整delta seconds、worker node id和sequence占用位数。
CachedUidGenerator
CachedUidGenerator是UidGenerator的重要改进实现,workerId的获取方式也和DefaultUidGenerator是一样。核心优化的两个点是:
- 利用RingBuffer数据结构预先生成若干个分布式ID并保存。
- 通过时间值递增得到时间值(lastSecond.incrementAndGet()),而不是System.currentTimeMillis()这种方式,保证了时间回拨的问题。
注意的是,CachedUidGenerator分布式ID中的时间信息可能并不是这个ID真正产生的时间点。
github地址:https://github.com/baidu/uid-generator
美团(Leaf)
Leaf支持号段模式和雪花算法模式,可以切换使用。
号段模式
思想和我们上面讲的一致,它存储采用数据库。
雪花算法模式
Leaf的snowflake模式依赖于ZooKeeper,不同于原始snowflake算法是在workId的生成上。Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。对于时间回拨的问题,美团采取的策略是等待5ms后重新获取,如果发现时间还未追上,那么进行告警。
github地址:https://github.com/Meituan-Dianping/Leaf
参考文献:https://mp.weixin.qq.com/s/SrGDE8G9QC_UZlTRcOIcDw
http://www.360doc.com/content/20/0404/21/47036874_903879107.shtml
公众号:sowhat1412