分布式 ID 的生成方案
为什么需要分布式 ID?
拿MySQL数据库举个栗子:
在我们业务数据量不大的时候,单库单表完全可以支撑现有业务,数据再大一点搞个MySQL主从同步读写分离也能对付。
但随着数据日渐增长,主从同步也扛不住了,就需要对数据库进行分库分表,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求,例如 user0 和 user1 两张表生成的 userid 如果采用自增 ID,会导致重复;特别一点的如订单、优惠券也都需要有唯一ID
做标识。此时一个能够生成全局唯一ID
的系统是非常必要的。那么这个全局唯一ID
就叫分布式ID
。
那么分布式ID需要满足那些条件?
- 全局唯一:必须保证ID是全局性唯一的,基本要求。
- 高性能:高可用低延时,ID生成响应要块,否则反倒会成为业务瓶颈。
- 高可用:100%的可用性是骗人的,但是也要无限接近于100%的可用性。
- 好接入:要秉着拿来即用的设计原则,在系统设计和实现上要尽可能的简单。
- 趋势递增:最好趋势递增,这个要求就得看具体业务场景了,一般不严格要求。
分布式 ID 的生成方案
- UUID
- 数据库自增ID
- 数据库多主模式
- 号段模式
- Redis
- 雪花算法(SnowFlake)
UUID
存在 3 个版本:V1、V4、V7:
UUID 版本 | 生成方式 | 是否有序 | 典型用途 |
---|---|---|---|
v1 | 时间戳 + 节点 + 序列 | 部分有序 | 系统日志、追踪 ID |
v4 | 随机数 | 否 | 分布式唯一标识、traceId |
v7 | 时间戳 + 随机数 | 部分有序 | 数据库主键、可排序 ID |
为什么说 UUID V1 和 V7是部分有序?
因为高位是时间戳,这代表着不同时间生成的 UUID 是严格递增的,同一时间生成的 UUID 则是乱序的。
UUID
的生成简单到只有一行代码,输出结果 c2b8c2b9e46c47e3b30dca3b0d447718
,但UUID却并不适用于实际的业务需求。像用作订单号UUID
这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;而对于数据库来说用作业务主键ID
,它不仅是太长还是字符串,存储性能差查询也很耗时,所以不推荐用作分布式ID
。
数据库自增 ID
基于数据库的auto_increment
自增ID完全可以充当分布式ID
,具体实现:需要一个单独的MySQL实例用来生成ID,建表结构如下:
1 | CREATE DATABASE `SEQ_ID`; |
当我们需要一个ID的时候,向表中插入一条记录返回主键ID
,但这种方式有一个比较致命的缺点,访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!
基于数据库集群模式
害怕一个主节点挂掉没法用,那就做多主模式集群,也就是多个Mysql实例都能单独的生产自增ID。
那这样还会有个问题,两个MySQL实例的自增ID都从1开始,会生成重复的ID怎么办?
解决方案:设置起始值
和自增步长
。
但这样会有一个致命的问题,怎么扩容?
增加第三台MySQL
实例需要人工修改一、二两台MySQL实例
的起始值和步长,把第三台机器的ID
起始生成位置设定在比现有最大自增ID
的位置远一些,但必须在一、二两台MySQL实例
ID还没有增长到第三台MySQL实例
的起始ID
值的时候,否则自增ID
就要出现重复了,必要时可能还需要停机修改。
号段模式
号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。表结构如下:
1 | CREATE TABLE id_generator ( |
biz_type :代表不同业务类型
max_id :当前最大的可用id
step :代表号段的长度
version :是一个乐观锁,每次都更新version,保证并发时数据的正确性
id | biz_type | max_id | step | version |
---|---|---|---|---|
1 | 101 | 1000 | 2000 | 0 |
等这批号段ID用完,再次向数据库申请新号段,对max_id
字段做一次update
操作,update max_id= max_id + step
,update成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]
。
1 | update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX |
由于多业务端可能同时操作,所以采用版本号version
乐观锁方式更新,这种分布式ID
生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。
基于Redis模式
Redis
也同样可以实现,原理就是利用redis
的 incr
命令实现ID的原子性自增。
1 | 127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1 |
用redis
实现需要注意一点,要考虑到redis持久化的问题。redis
有两种持久化方式RDB
和AOF
:
RDB
会定时打一个快照进行持久化,假如连续自增但redis
没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。AOF
会对每条写命令进行持久化,即使Redis
挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis
重启恢复的数据时间过长。
基于雪花算法(Snowflake)模式
雪花算法(Snowflake)是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。
Snowflake
生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。
Snowflake ID组成结构:正数位
(占1比特)+ 时间戳
(占41比特)+ 机器ID
(占5比特)+ 数据中心
(占5比特)+ 自增值
(占12比特),总共64比特组成的一个Long类型。
- 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
- 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L 60 60 24 365) = 69年
- 工作机器id(10bit):也被叫做
workId
,这个可以灵活配置,机房或者机器号组合都可以。 - 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID
根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。