本文介绍常见分布式ID方案的实现和原理。分布式ID方案大致可以分为两类,一类的基于数据库的实现,规定起始位置和步长,来实现趋势递增,保障ID不会重叠。一类是类snowflake的实现,将固定位数的字符串划分为不同的段,每一段代表不同的含义,基本上分段中包括:机器序列、时间戳、自增序列,这种方案需要考虑时钟回拨的情况,同时可以通过双buffer的设计来改进ID生产单点性能不足的问题。
一、概述
为什么需要分布式ID设计?
在单体应用的时代,生成的业务数据保存在单个库表中,通过AUTO_INCREMENT=1就能保障业务数据的主键线性不重复的增长,但在分布式应用的时代,由于保存的业务数据不一定在单个库表中,对于唯一主键的需求无法通过单个数据库来满足,所以就需要一个统c一的分布式ID生成方案。
分布式ID是什么?
分布式ID的常见用途是单一业务数据可能会保存在不同的数据库中,需要生产一个统一的主键ID,这中分布式ID的特征是:
- 全局唯一性:生成的ID保障全局唯一性,在单机环境下主键ID具有的特征在分布式环境下也要具备;
- 趋势有序:对于生成的ID需要趋势递增或者趋势递减;
- 信息安全:对于生成ID的方案来说,不能暴露用户的个人信息,比如设备MAC地址;或者通过生成的分布式ID分布规律能够推导出时间范围内分布式ID的个数,这个对于商业上来说有一定风险;
二、常见方案
1、UUID实现
UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000,到目前为止业界一共有5种方式生成UUID,详情见IETF发布的UUID规范 A Universally Unique IDentifier (UUID) URN Namespace。
优点:
- 性能非常高:本地生成,没有网络消耗。
缺点:
- 不易于存储:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
- 信息不安全:基于MAC地址生成UUID的算法可能会造成MAC地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。
- ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用:(1)MySQL官方有明确的建议主键要尽量越短越好[4],36个字符长度的UUID不符合要求。(2) 对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
实现:
可以直接使用jdk自带的UUID,原始生成的是带中划线的,如果不需要,可自行去除。
public class UUIDService {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
String rawUUID = UUID.randomUUID().toString();
System.out.println(rawUUID);
//去除"-"
String uuid = rawUUID.replaceAll("-", "");
System.out.println(uuid);
}
}
}
2、基于MySQL数据库实现
对于基于MySQL数据库来生成分布式ID有两种思路,(1)一种是创建专门生成分布式ID的表来实现,(2)另一种是对分布式环境下多个数据库中划分范围不重叠的ID。
对于方案(1),需要先创建数据库表sequence_id,其中stub字段是无意义的占位字段,id字段是生产的分布式ID。
CREATE TABLE `sequence_id` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`stub` char(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
再通过REPLACE INTO更新语句可以生产分布式ID,该语句作用是如果主键id不存在则insert,如果主键id存在则update。
BEGIN;
REPLACE INTO sequence_id ( stub )
VALUES
( 'stub' );
SELECT
LAST_INSERT_ID();
COMMIT;
对于方案(2),我们考虑的角度是分布式环境下多个数据库生产分布式ID会造成冲突,原因是多个数据库采用相同的起始位置,在相同的范围区间内生成分布式ID,这样会导致生成的ID重叠。如果我们规定每个数据库采用不同的起始位置在不同的范围区间内生成ID则可以避免这个问题。在MySQL中,规定字段auto_increment_increment
和auto_increment_offset
来保证ID自增。
-
auto_increment_offset
:表示自增长字段开始的位置,不同的数据库其起始位置不同; -
auto_increment_increment
:表示自增长字段每次递增的步长,规定分布式环境下数据库具有相同的步长;
优点:
- 非常简单,利用现有数据库系统的功能实现,成本小,ID号单调自增,存储消耗空间小。
缺点:
- 由于每次生产ID都需要请求MySQL,但鉴于MySQL数据库的性能,该种方案可能存在单点问题,所以该种方案的并发量可能不大;
- 对于方案(2),由于设计的步长正好是数据库的数量,所以设计好起始位置和步长后再扩展数据库就无法生产不重叠的ID,所以该种方案的扩展性不强;
- 存在信息安全问题,由于利用数据库生产的ID都是趋势递增的,所以能很简单的算出时间段内生成的ID数量,对于订单业务来说,很容易就会被掌握真实订单量。
3、基于Redis的KV数据库实现
通过redis来实现分布式ID,主要是通过:incr sequence_id_biz_type
原子自增命令来实现,由于redis是单线程读写的特性,保障生成ID都是串行执行的,所以能保证分布式ID的唯一性。这种方案和基于MySQL数据库实现方案一样,需要依赖外部组件,系统复杂性的增加意味着系统稳定性的下降,但相对MySQL方案redis方案生成ID的性能会更高。这种方案的缺点是由于redis是内存数据库,在断电和重启之后数据会失效,由于redis具有RDB和AOF的持久化数据机制,在一定程度可以解决该问题,但依旧无法完全保障数据的一致性。
4、雪花算法snowflake实现
雪花算法snowflake是一种划分命名空间来生成分布式ID的实现方式,他是Twitter 开源的分布式 ID 生成算法(twitter-archive/snowflake:github.com/twitter-arc…
- 第1bit位占用1bit,其值始终是0,可看做是符号位不使用。
- 第2bit位开始的41位是时间戳,41-bit位可表示2^41个数,每个数代表毫秒,那么snowflake算法可用的时间年限是
(1L