本文旨在给为大家介绍,当下分布式 ID 生成的诸多方案,比较各方案间的优缺点,方便大家在不同业务场景时轻松搞定技术选型。
9 种分布式 ID 生成如下:
- UUID
- 数据库自增 ID
- 数据库多主模式
- 号段模式
- Redis
- 雪花算法(SnowFlake)
- 滴滴出品(TinyID)
- 百度 (Uidgenerator)
- 美团(Leaf)
适合人群:爱专研技术,有一定开发基础的程序员
前两天公众号有个粉丝给我留言吐槽最近面试:“小富,年前我在公司受点委屈一冲动就裸辞了,然后现在疫情严重两个多月还没找到工作,接了几个视频面试也都没下文。好多面试官问完一个问题,紧接着说还会其他解决方法吗?能干活解决 bug 不就行了吗?那还得会多少种方法?”
面试官应该是对应聘者的回答不太满意,他想听到一个他认为最优的解决方案,其实这无可厚非。同样一个 bug,能用一行代码解决问题的人和用十行代码解决问题的人,你会选哪个入职?显而易见的事情!所以看待问题还是要从多个角度出发,每种方法都有各自的利弊。
一、为什么要用分布式 ID?在说分布式 ID 的具体实现之前,我们来简单分析一下为什么用分布式 ID?分布式 ID 应该满足哪些特征?
1、什么是分布式 ID?拿 MySQL 数据库举个栗子:
在我们业务数据量不大的时候,单库单表完全可以支撑现有业务,数据再大一点搞个 MySQL 主从同步读写分离也能对付。
但随着数据日渐增长,主从同步也扛不住了,就需要对数据库进行分库分表,但分库分表后需要有一个唯一 ID 来标识一条数据,数据库的自增 ID 显然不能满足需求;特别一点的如订单、优惠券也都需要有唯一 ID
做标识。此时一个能够生成全局唯一 ID
的系统是非常必要的。那么这个全局唯一 ID
就叫分布式 ID
。
- 全局唯一:必须保证 ID 是全局性唯一的,基本要求
- 高性能:高可用低延时,ID 生成响应要块,否则反倒会成为业务瓶颈
- 高可用:100%的可用性是骗人的,但是也要无限接近于 100%的可用性
- 好接入:要秉着拿来即用的设计原则,在系统设计和实现上要尽可能的简单
- 趋势递增:最好趋势递增,这个要求就得看具体业务场景了,一般不严格要求
今天主要分析一下以下 9 种,分布式 ID 生成器方式以及优缺点:
- UUID
- 数据库自增 ID
- 数据库多主模式
- 号段模式
- Redis
- 雪花算法(SnowFlake)
- 滴滴出品(TinyID)
- 百度 (Uidgenerator)
- 美团(Leaf)
那么它们都是如何实现?以及各自有什么优缺点?我们往下看
以上图片源自网络,如有侵权联系删除
1、基于 UUID在 Java 的世界里,想要得到一个具有唯一性的 ID,首先被想到可能就是UUID
,毕竟它有着全球唯一的特性。那么UUID
可以做分布式 ID
吗?答案是可以的,但是并不推荐!
public static void main(String[] args) { String uuid = UUID.randomUUID().toString().replaceAll("-",""); System.out.println(uuid); }
UUID
的生成简单到只有一行代码,输出结果 c2b8c2b9e46c47e3b30dca3b0d447718
,但 UUID 却并不适用于实际的业务需求。像用作订单号UUID
这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;而对于数据库来说用作业务主键 ID
,它不仅是太长还是字符串,存储性能差查询也很耗时,所以不推荐用作分布式 ID
。
优点:
- 生成足够简单,本地生成无网络消耗,具有唯一性
缺点:
- 无序的字符串,不具备趋势自增特性
- 没有具体的业务含义
- 长度过长 16 字节 128 位,36 位长度的字符串,存储以及查询对 MySQL 的性能消耗较大,MySQL 官方明确建议主键要尽量越短越好,作为数据库主键
UUID
的无序性会导致数据位置频繁变动,严重影响性能。
基于数据库的auto_increment
自增 ID 完全可以充当分布式 ID
,具体实现:需要一个单独的 MySQL 实例用来生成 ID,建表结构如下:
CREATE DATABASE `SEQ_ID`;CREATE TABLE SEQID.SEQUENCE_ID ( id bigint(20) unsigned NOT NULL auto_increment, value char(10) NOT NULL default '', PRIMARY KEY (id),) ENGINE=MyISAM;
insert into SEQUENCE_ID(value) VALUES ('values');
当我们需要一个 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 扩容增加节点,这是一个比较麻烦的事。
从上图可以看出,水平扩展的数据库集群,有利于解决数据库单点压力的问题,同时为了 ID 生成特性,将自增步长按照机器数量来设置。
增加第三台MySQL
实例需要人工修改一、二两台MySQL 实例
的起始值和步长,把第三台机器的 ID
起始生成位置设定在比现有最大自增 ID
的位置远一些,但必须在一、二两台MySQL 实例
ID 还没有增长到第三台 MySQL 实例
的起始 ID
值的时候,否则自增 ID
就要出现重复了,必要时可能还需要停机修改。
优点:
- 解决 DB 单点问题
缺点:
- 不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。
号段模式是当下分布式 ID 生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增 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`))
biz_type :代表不同业务类型
max_id :当前最大的可用 id
step :代表号段的长度
version :是一个乐观锁,每次都更新 version,保证并发时数据的正确性
idbiz_typemax_idstepversion1101100020000等这批号段 ID 用完,再次向数据库申请新号段,对max_id
字段做一次update
操作,update max_id= max_id + step
,update 成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]
。
update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX
由于多业务端可能同时操作,所以采用版本号version
乐观锁方式更新,这种分布式 ID
生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。
Redis
也同样可以实现,原理就是利用redis
的 incr
命令实现 ID 的原子性自增。
127.0.0.1:6379> set seq_id 1 // 初始化自增 ID 为 1OK127.0.0.1:6379> incr seq_id // 增加 1,并返回递增后的数值(integer) 2
用redis
实现需要注意一点,要考虑到 redis 持久化的问题。redis
有两种持久化方式RDB
和AOF
RDB
会定时打一个快照进行持久化,假如连续自增但redis
没及时持久化,而这会 Redis 挂掉了,重启 Redis 后会出现 ID 重复的情况。AOF
会对每条写命令进行持久化,即使Redis
挂掉了也不会出现 ID 重复的情况,但由于 incr 命令的特殊性,会导致Redis
重启恢复的数据时间过长。
雪花算法(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
关注打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?