1 分布式ID概述
1.1 分布式ID的诉求
- 在复杂的分布式系统中,往往需要对并发的、大量、分布式写入/存储的新增记录进行唯一标识。
比如,在对一个订单表进行了分库分表操作,这时候数据库的自增ID显然不能作为某个订单的唯一标识。
-
除了全局唯一性的诉求外,还有其他分布式场景对分布式ID的一些要求:
- 趋势递增: 由于多数RDBMS使用B-tree的数据结构来存储索引数据,在主键的选择上我们应该尽量使用有序的主键保证写入性能。
- 单调递增: 保证下一个ID一定大于上一个ID,例如排序需求。
- 信息安全 :如果ID是连续的,恶意用户的数据扒取就非常容易做了;如果是订单号就更危险了,可以直接知道系统的交易量。所以在一些应用场景下,会需要ID无规则。
-
同时,除了对ID编号自身的要求,业务还对ID生成系统的可用性要求极高。
想象一下,如果ID生成系统瘫痪,这就会带来一场灾难。
就不同的场景及要求,市面诞生了很多分布式ID解决方案。
本文针对多个分布式ID解决方案进行介绍,包括其优缺点、使用场景及代码示例。
1.1 UUID
1.1.1 UUID的定义
UUID
(Universally Unique IDentifier
)全局唯一标识符,用于标识信息元素,是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。
通常平台会提供生成的API。按照开发软件基金会(OSF)制定的标准计算,用到了
以太网卡地址
、纳秒级时间
、芯片ID码
和许多可能的数字
-
UUID
是一种软件构建的标准,同时也为开放软件基金会
组织在分布式计算环境领域
的一部分。 -
UUID的作用是【让分布式系统中的所有元素,都能有唯一的辨识信息,而不需要通过中央控制端来做辨识信息的指定】。类似于我们的身份证是表明我们唯一身份的标识。如此一来,每个人都可以创建不与其它人冲突的UUID。在这样的情况下,就不需要考虑数据库创建时的名称重复问题。目前最广泛应用的UUID,是微软公司的全局唯一标识符(GUID Globally Unique IDentifier),而其它重要的应用,则有Linux ext2/ext3文件系统,LUKS加密分区、GNOME、KDE、Mac OS X等等。
-
UUID的标准形式包含32个16进制数字,被分为5个组,以4-4-4-12的形式显示。
- 当前日期和时间,UUID的第一个部分与时间有关,如果你在生成一个UUID之后,过几秒又生成一个UUID,则第一个部分不同,其余相同。
- 时钟序列
- 全局唯一的IEEE机器识别号,如果有网卡,从网卡MAC地址获得,没有网卡以其它方式获得
1.1.2 UUID的特点
UUID具有以下优点:
- 唯一性:UUID的生成算法保证在同一时空中不会产生重复的ID,可以作为唯一标识符。
- 不可预测性:UUID的生成算法是基于随机数的,不可预测,可以用于防止信息泄露。
- 分布式:UUID的生成算法不依赖于中央管理机构,可以在分布式环境下使用。
在分布式环境中,使用UUID作为唯一标识符有一定的优势,因为UUID的生成算法不依赖于中央管理机构,不会产生冲突,也不需要进行同步操作。
UUID的缺点:
- 长度较长(耗费存储空间):UUID的唯一缺陷在于生成的结果串会比较长。
但是,UUID的缺点是生成的ID比较长,占用存储空间比较大,不适合作为索引或数据库主键。
标准的UUID格式为: XXXXXXXX-XXXX-XXXX-XXXXXXXXXXXX(8-4-4-4-12(32位)),其中每个X是0-9或者a-f范围内的一个十六进制的数字,可以从cflib下载CreateGUID() UDF记性转换。
简单的UUID可以通过ColdFusion中的CreateUUID()函数很简单生成格式为: XXXXXXXX-XXXX-XXXX-XXXXXXXXXXXXXXXX(8-4-4-16)。
1.1.3 UUID的优点 vs. ID的缺点
- UUID
- 保证【全局唯一】:出现需要数据拆分、合并存储的时候,能够达到全局整体的唯一性
- 支持在【分布式系统】中使用
原因:每个节点都可以生成自己的UUID,而不需要与其他节点协调。
- ID
- ID可能会出现【重复】的情况,尤其是在分布式系统中。
- ID的生成顺序是递增的,这可能会导致某些行锁定,从而影响系统的【并发能力】。
1.1.4 UUID的缺点 vs ID的优点
-
UUID
- UUID比ID更长,需要更多的存储空间。(1个UUID就占用32位/4byte),一般会选择VARCHAR(36),若你建的索引越多,影响越严重)
- UUID生成的顺序是随机的,而ID生成的顺序是递增的。=> 这意味着: 在使用UUID作为主键时,数据表中的记录可能不会按照时间顺序排序,这可能会影响查询速度
- 影响插入(INSERT)速度
-
ID
- ID是一种自增长的整数,与UUID相比,ID更加【节省存储空间】(它只需要4个字节)
- ID的生成顺序是递增的,这意味着:数据表中的记录可按照时间顺序排序,这有助于【提高查询速度】
1.1.5 为什么UUID能够成为主键(Primary Key)?
其实在InnoDB存储引擎(Mysql)下,自增长的ID做主键性能已经达到了最佳。
不论是存储和读取速度都是最快的,而且占的存储空间也是最小的。
但是在我们实际的项目中会存在问题,历史数据表的主键id会与数据表的id重复,两张自增id做主键的表合并时,id一定会有冲突,但如果各自的id还关联了其他表,这就很不好操作了。
如果使用了UUID,生成的ID不仅是表独立的,而且还是库独立的。
对以后的数据操作很有好处,可以说是彻底解决了【历史数据】和【新数据】之间的冲突问题,这也是为什么UUID可以选择成为主键的原因,但是它也有缺点。
1.1.6 UUID与Java
UUID.randomUUID().toString()
是 java (JDK 1.5
以上的版本)提供的一个自动生成主键的方法,它生成的是以为32位的数字和字母组合的字符,中间还参杂着4个-
符号。
作用:它可以作为数据库表的标识列来增加,比序列增长更加方便。当然还可以用来拼接作为路径,或者图片的前缀名等等。
- Java中可以使用
java.util.UUID
类来生成UUID:
JDK 1.5 +
@Test
public void uuidTest(){
//jdk1.5+
UUID uuid = UUID.randomUUID();
System.out.println(uuid.toString());
// out: 5a0c3541-4be1-424f-bc4a-4addbcd62328
}
1.1.7 UUID与MySQL
1.1.7.1 UUID做数据库唯一主键的注意事项
- 如果是主从即M-S模式,最好是不使用mysql自带函数UUID来生成唯一主键
- 因为主表生成的UUID要再关联从表时:需要再去数据库查出这个UUID,需要多进行一次数据库交互,且在这个时间差里面主表很有可能还有数据生成,这样就很容易导致关联的UUID出错。
- 如果真要使用UUID,可以在服务端中生成后,直接存储到DB里,这时主从的UUID就是一样的了。
1.1.7.2 UUID做为表主键的最佳实践方案
- InnoDB引擎表是基于B+树的索引组织表
-
B+树: B+树是为磁盘或者其他直接存取辅助设备而设计的一种平衡查找树,在B+树中,所有记录节点都是按健值的大小顺序存放在同一层的叶节点中,各叶节点指针进行连接。
-
InnoDB主索引: 叶节点包含了完整的数据记录。这种索引叫做聚焦索引。InnoDB的索引能提供一种非常快速的主键查找性能。不过,它的辅助索引也会包含主键列,所以,如果主键定义的比较大,其他索引也将很大。如果想在表上定义很多索引,则争取尽量把主键定义得小一些。InnoDB不会压缩索引。
-
聚焦索引这种实现方式使得按照主键的搜索十分高效,但是辅助索引需要检索两遍索引: 首先检索辅助索引获得主键,然后用主键道主索引中检索获得记录。
-
方案:
如果InnoDB表的数据写入顺序能和B+树索引的叶子节点顺序一致的话,这时候存取效率是最高的。为了存储和查询性能应该使用自增长ID做主键。
对于InnoDB的主索引,数据会按照主键进行排序,由于UUID的无序性,InnoDB会产生巨大的IO压力,此时不适合使用UUID做物理主键,可以把它作为逻辑主键,物理主键依然使用自增ID。为了全局的唯一性,应该使用UUID做索引关联其他表或者做外键。
-- ID继续为主键, UUID为外键
CREATE TABLE `system_roles` (
`id` INT(11) NOT NULL AUTO_INCREMENT COMMENT "序号",
`rid` VARCHAR(36) DEFAULT NULL COMMENT "角色UUID",
`name` VARCHAR(64) DEFAULT NULL COMMENT "角色名",
`describe` VARCHAR(64) DEFAULT NULL COMMENT "角色描述",
`status` BOOLEAN DEFAULT true COMMENT "角色状态",
`created_at` datetime DEFAULT NULL COMMENT '创建时间',
`updated_at` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
FOREIGN KEY (`rid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1.1.X 小结:UUID
UUID完全可以满足分布式唯一标识,但是在实际应用过程中一般不采用,有如下几个原因:
- 存储成本高: UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。
- 信息不安全: 基于MAC地址生成的UUID算法会暴露MAC地址,曾经梅丽莎病毒的制造者就是根据UUID寻找的。
- 不符合MySQL主键要求: MySQL官方有明确的建议主键要尽量越短越好,因为太长对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能。
- 大多数情况下,UUID:
- 本身不作为数据库主键使用
- 不作为查询条件
- 多用于消息队列传输使用,以用于各种消息的辨别
1.2 数据库自增ID
利用Mysql的特性ID自增,可以达到数据唯一标识,但是分库分表后只能保证一个表中的ID的唯一,而不能保证整体的ID唯一。
为了避免这种情况,我们有以下两种方式解决该问题。
1.2.1 方案1:主键表
- 方法1:主键表
通过单独创建主键表维护唯一标识,作为ID的输出源可以保证整体ID的唯一。举个例子:
创建一个主键表
CREATE TABLE `unique_id` (
`id` bigint NOT NULL AUTO_INCREMENT,
`biz` char(1) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `biz` (`biz`)
) ENGINE = InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET =utf8;
业务通过更新操作来获取ID信息,然后添加到某个分表中。
BEGIN;
REPLACE INTO unique_id (biz) values ('o') ;
SELECT LAST_INSERT_ID();
COMMIT;
1.2.2 方案2:设置ID自增步长
- 方法2:设置ID自增步长
我们可以设置Mysql主键自增步长,让分布在不同实例的表数据ID做到不重复,保证整体的唯一。
如下,可以设置Mysql实例1步长为1,实例1步长为2。
# 查看主键自增的属性
show variables like '%increment%'
显然,这种方式在并发量比较高的情况下,如何保证扩展性其实会是一个问题。
1.3 号段模式
号段模式是当下分布式ID生成器的主流实现方式之一。其原理如下:
- 号段模式每次从数据库取出一个号段范围,加载到服务内存中。业务获取时ID直接在这个范围递增取值即可。
- 等这批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,新的号段范围是(
max_id
,max_id +step
]。 - 由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新。
例如 (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 '版本号,是一个乐观锁,每次都更新version,保证并发时数据的正确性',
PRIMARY KEY (`id`)
)
这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。
但同样也会存在一些缺点比如:服务器重启,单点故障会造成ID不连续。
1.4 Redis INCR
1.4.1 定义
- 依赖redis的数据源,通过redis的incr/incrby自增院子操作命令,能保证生成id肯定是唯一有序的,本质生成方式与数据库一致。
- 基于全局唯一ID的特性,我们可以通过Redis的INCR命令来生成全局唯一ID。
1.4.2 特点
-
优点:
- 整体吞吐量比数据库要高;
-
缺点:
- Redis是基于内存的数据库,其实例或集群宕机后,找回最新的ID值有点困难。
- 由于使用自增,对外容易暴露业务数据总量。
-
适合场景
- 计数场景,如:
- 用户访问量
- 订单流水号(日期+流水号)
- 计数场景,如:
1.4.3 实现源码
Redis分布式ID的简单案例
/**
* Redis 分布式ID生成器
*/
@Component
public class RedisDistributedId {
@Autowired
private StringRedisTemplate redisTemplate;
private static final long BEGIN_TIMESTAMP = 1659312000l;
/**
* 生成分布式ID
* 符号位 时间戳[31位] 自增序号【32位】
* @param item
* @return
*/
public long nextId(String item){
// 1.生成时间戳
LocalDateTime now = LocalDateTime.now();
// 格林威治时间差
long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
// 我们需要获取的 时间戳 信息
long timestamp = nowSecond - BEGIN_TIMESTAMP;
// 2.生成序号 --》 从Redis中获取
// 当前当前的日期
String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
// 获取对应的自增的序号
Long increment = redisTemplate.opsForValue().increment("id:" + item + ":" + date);
return timestamp << 32 | increment;
}
}
1.5 雪花算法(Twitter)
1.5.0 背景
据国家大气研究中心的查尔斯·奈特称,一般的雪花大约由10^19个水分子组成。
在雪花形成过程中,会形成不同的结构分支,所以说大自然中不存在两片完全一样的雪花,每一片雪花都拥有自己漂亮独特的形状。
雪花算法表示生成的id如雪花般独一无二。
snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。
其核心思想是:使用41bit作为毫秒数,10bit作为机器的ID(5个bit是数据中心,5个bit的机器ID),12bit作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是0。
1.5.1 定义
- Snowflake,雪花算法是有Twitter开源的分布式ID生成算法,以划分命名空间的方式将64bit位的Long型数字分割成了多个部分,每个部分都有具体的不同含义。
https://github.com/twitter-archive/snowflake
- 在Java中64Bit位的整数是Long类型,所以在Java中Snowflake算法生成的ID就是long来存储的。具体如下:
- 第一部分(1bit): 符号位。不使用
由于 long 类型在 java 中带符号的,最高位为符号位,正数为 0,负数为 1,且实际系统中所使用的ID一般都是正数,所以最高位为 0。
- 第二部分(41bit): 41位的时间戳(毫秒级),41bit位可表示241个数,每个数代表的是毫秒。
那么,雪花算法的时间年限是
(2^41)/(1000×60×60×24×365)=69
年
需要注意的是此处的 41 位时间戳并非存储当前时间的时间戳,而是存储时间戳的差值(当前时间戳 - 起始时间戳),这里的起始时间戳一般是ID生成器开始使用的时间戳,由程序来指定,所以41位毫秒时间戳最多可以使用 (1 << 41) / (1000x60x60x24x365) = 69年 。
-
第三部分: 10bit表示是机器数,即
2^ 10 = 1024
台机器,通常不会部署这么多机器。- 包括5位数据标识位和5位机器标识位。
- 这10位决定了分布式系统中最多可以部署 1 << 10 = 1024 个节点。
- 超过这个数量,生成的ID就有可能会冲突。
-
第四部分: 12bit位是毫秒内的自增序列。
- 这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096 个ID
2^12=4096
- 这 12 位计数支持每个节点每毫秒(同一台机器,同一时刻)最多生成 1 << 12 = 4096 个ID
-
加起来刚好64位,为一个Long;理论上snowflake方案的QPS约为
409.6w/s
1.5.2 特点
-
雪花算法ID优点:
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
- 按时间有序,一般不会造成ID碰撞
- 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
- 可以根据自身业务特性分配bit位,非常灵活。
- 高性能、低延迟
- 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
-
雪花算法ID缺点:
- 强依赖机器时钟。如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
1.5.3 雪花算法与Java实现
- 雪花算法案例代码:
public class SnowflakeIdWorker {
// ==============================Fields===========================================
/**
* 开始时间截 (2020-11-03,一旦确定不可更改,否则时间被回调,或者改变,可能会造成id重复或冲突)
*/
private final long twepoch = 1604374294980L;
/**
* 机器id所占的位数
*/
private final long workerIdBits = 5L;
/**
* 数据标识id所占的位数
*/
private final long datacenterIdBits = 5L;
/**
* 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
*/
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 支持的最大数据标识id,结果是31
*/
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/**
* 序列在id中占的位数
*/
private final long sequenceBits = 12L;
/**
* 机器ID向左移12位
*/
private final long workerIdShift = sequenceBits;
/**
* 数据标识id向左移17位(12+5)
*/
private final long datacenterIdShift = sequenceBits + workerIdBits;
/**
* 时间截向左移22位(5+5+12)
*/
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
/**
* 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作机器ID(0~31)
*/
private long workerId;
/**
* 数据中心ID(0~31)
*/
private long datacenterId;
/**
* 毫秒内序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的时间截
*/
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 构造函数
*
*/
public SnowflakeIdWorker() {
this.workerId = 0L;
this.datacenterId = 0L;
}
/**
* 构造函数
*
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
// ==============================Methods==========================================
/**
* 获得下一个ID (该方法是线程安全的)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(
String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
//如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
//毫秒内序列溢出
if (sequence == 0) {
//阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
//时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
//上次生成ID的时间截
lastTimestamp = timestamp;
//移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift) //
| (datacenterId << datacenterIdShift) //
| (workerId << workerIdShift) //
| sequence;
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
*
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒为单位的当前时间
*
* @return 当前时间(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
/**
* 随机id生成,使用雪花算法
*
* @return
*/
public static String getSnowId() {
SnowflakeIdWorker sf = new SnowflakeIdWorker();
String id = String.valueOf(sf.nextId());
return id;
}
//=========================================Test=========================================
/**
* 测试
*/
public static void main(String[] args) {
SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(id);
}
}
}
1.5.4 雪花算法与容器化部署
1.5.4.1 容器化部署环境存在的问题
-
容器化部署环境下存在的问题:
- 容器化无状态部署机器ID不可在部署前预先获取IP
- 以前项目使用物理机器部署时,我们可以根据机器的IP分配对就的机器id
- 可是现在都是使用容器化部署,一般都是部署成无状态模式,无法事前预先获取workId;
- 容器化无状态部署机器ID不可在部署前预先获取IP
-
docker Pod 每次重启,即会新分配 Pod IP
-
docker Pod 可能会重启多次(乃至成千上万次)
-
需要为当前存活的、在线的 docker Pod 分配 WorkId
-
当一个 docker Pod 死掉时,应释放其 WorkerId
- 如果不释放这个WorkId,则:
- 存在多个 Pod 共用一个 WorkerId,那么将造成:
- Worker 取数空间的浪费
- 最终生成的ID可能存在重复(虽然概率极小)
- 存在多个 Pod 共用一个 WorkerId,那么将造成:
- 如果不释放这个WorkId,则:
1.5.4.2 解决思路
-
思路1:
- 既然难以严格、均匀分配WorkerId,那么索性:
- Worker ID:将【Worker取数空间】缩小至1bit、扩大【序列号】的取数空间;
- 即:同一服务的多个Pod,共用1个默认的WorkerId(例如:0L)
- 数据中心ID:根据部署在不同区域,可在配置文件中提前人工定义
- 弊端:
- 存在多个 Pod 共用一个 WorkerId,则:最终生成的ID可能存在重复(虽然概率较小)
- Worker ID:将【Worker取数空间】缩小至1bit、扩大【序列号】的取数空间;
- 既然难以严格、均匀分配WorkerId,那么索性:
-
思路2:
- 应用程序启动时,从0开始依次检查注册到MySQL的 worker id及worker是否依旧存活?
- 若存活时,则:检查id,自增1
- 若不存活,则:分配worker id为该id,并删除原worker信息
- 应用程序启动时,从0开始依次检查注册到MySQL的 worker id及worker是否依旧存活?
-- DROP DATABASE IF EXISTS `xxxx`;
-- CREATE DATABASE `xxxx` ;
-- use `xxxx`;
-- DROP TABLE IF EXISTS `DS_ID_WORKER`;
CREATE TABLE `{appDb}_worker_node` (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '数据库自增ID/auto increment id',
service_name VARCHAR(128) NOT NULL COMMENT '应用服务名称/service name. eg: bdp-data-service',
service_instance VARCHAR(128) NOT NULL COMMENT '服务实例名称/service instance. eg: bdp-data-service-parent-backend-service-86c6894d44-m7ljf',
host_name VARCHAR(64) NOT NULL COMMENT '主机名/host name. eg: bdp-data-service-parent-backend-service-86c6894d44-m7ljf',
ip VARCHAR(64) NOT NULL COMMENT '网络地址/ip address, eg: 123.34.67.23',
port INT UNSIGNED NOT NULL COMMENT '服务实例的主端口/port. eg: 8080',
node_type INT NOT NULL COMMENT '节点类型/node type: ACTUAL(实际物理主机) or CONTAINER(虚拟化容器)',
launch_time TIMESTAMP NOT NULL COMMENT '应用服务的启动时间/launch time of service instance',
running_status VARCHAR(64) NOT NULL COMMENT '服务实例运行状态/running status of service instance: RUNNING(running)/运行中或未检查; STOPPED/已停止运行',
worker_id INT NOT NULL COMMENT '工作机器ID/worker node server id. eg: 0,3,...,31',
data_center_id INT NOT NULL COMMENT '数据中心ID/data center id. eg: 0,3,...,31',
data_center_code VARCHAR(128) NOT NULL COMMENT '数据中心代码/data center code. eg: HUAWEI_CLOUD-CN-TEST',
create_time TIMESTAMP NOT NULL COMMENT '创建时间/created time',
create_by VARCHAR(15) NOT NULL DEFAULT '-1' COMMENT '创建人/creater. default: -1(super administrator)',
update_time TIMESTAMP NOT NULL COMMENT '更新时间/modified time',
update_by VARCHAR(15) NOT NULL DEFAULT '-1' COMMENT '更新人/updater. default: -1(super administrator)',
is_delete INT NOT NULL COMMENT '删除标识/delete flag:0-false/undelete(default value)/未删除(默认值),1-true/deleted/已删除',
PRIMARY KEY(id)
) ENGINE = InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Worker Node for SNOWFLAKE(雪花算法) | 1)1个微服务实例仅能占用1个workerId;2)有效的WorkerId仅能被1个微服务实例占用';
-- 添加业务唯一标识索引约束
ALTER TABLE `{appDb}_worker_node` ADD UNIQUE INDEX index_unique_for_business_key_on_{appDb}_worker_node (service_name,running_status,worker_id,data_center_id,is_delete);
9:45
1.6 美团(Leaf : Leaf-segement & Leaf-snowflake)
由美团开发,开源项目链接:
https://github.com/Meituan-Dianping/Leaf
-
Leaf同时支持号段模式和snowflake算法模式,可以切换使用。
-
snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。
-
号段模式是对直接用数据库自增ID充当分布式ID的一种优化,减少对数据库的频率操作。相当于从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,业务服务将号段在本地生成1~1000的自增ID并加载到内存。
1.7 百度(Uidgenerator)
源码地址:
- https://github.com/baidu/uid-generator
中文文档地址: - https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
UidGenerator是百度开源的Java语言实现,基于Snowflake算法的唯一ID生成器。它是分布式的,并克服了雪花算法的并发限制。
单个实例的QPS能超过 600,0000。需要的环境:JDK8+,MySQL(用于分配WorkerId)。
百度的Uidgenerator对结构做了部分的调整,具体如下:
时间部分只有28位,这就意味着UidGenerator默认只能承受8.5年(2^28-1/86400/365
)。
不过,UidGenerator可以适当调整delta seconds、worker node id和sequence占用位数。
-- DROP DATABASE IF EXISTS `xxxx`;
-- CREATE DATABASE `xxxx` ;
-- use `xxxx`;
CREATE TABLE WORKER_NODE (
ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '数据库自增ID/auto increment id',
HOST_NAME VARCHAR(64) NOT NULL COMMENT '主机名/host name',
PORT VARCHAR(64) NOT NULL COMMENT '应用程序的主端口/port',
TYPE INT NOT NULL COMMENT '节点类型/node type: ACTUAL or CONTAINER',
LAUNCH_DATE DATE NOT NULL COMMENT '启动日期/launch date',
MODIFIED TIMESTAMP NOT NULL COMMENT '修改时间/modified time',
CREATED TIMESTAMP NOT NULL COMMENT '创建时间/created time',
PRIMARY KEY(ID)
)
COMMENT='DB WorkerID Assigner for Baidu UID Generator',ENGINE = INNODB;
1.8 滴滴(TinyID)
由滴滴开发,开源项目链接:
- https://github.com/didi/tinyid
Tinyid是在美团(Leaf)的leaf-segment算法基础上升级而来,不仅支持了数据库多主节点模式,还提供了tinyid-client客户端的接入方式,使用起来更加方便。
但和美团(Leaf)不同的是,Tinyid只支持号段一种模式不支持雪花模式。
Tinyid提供了两种调用方式,一种基于Tinyid-server提供的http方式,另一种Tinyid-client客户端方式。
2 总结比较
方案 | 优点 | 缺点 |
---|---|---|
UUID | 代码实现简单、没有网络开销,性能好 | 占用空间大、无序 |
数据库自增ID | 利用数据库系统的功能实现,成本小、ID自增有序 | 并发性能受Mysql限制、强依赖DB,当DB异常时整个系统不可用,致命 |
Redis INCR | 性能优于数据库、ID有序 | 解决单点问题带来的数据一致性等问题使得复杂度提高 |
雪花算法 | 不依赖数据库等第三方系统,性能也是非高、可以根据自身业务特性分配bit位,非常灵活 | 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。 |
号段模式 | 数据库的压力小 | 单点故障ID不连续 |
Leaf、Uidgenerator、TinyID | 高性能、高可用、接入简单 | 依赖第三方组件如ZooKeeper、Mysql |
X 参考文献 & 推荐文献
- 关于UUID.randomUUID() - CSDN
- MySQL中使用UUID和ID的优缺点对比(选择合适的方式提高数据表性能) - 前端老白
- 分布式ID之UUID详解 - Zhihu/万能知识大社区
- UUID VS ID - CSDN
- 九种分布式ID解决方案,总有一款适合你! - Weixin/芋道源码
- Mysql 自增id、uuid与雪花id - CSDN
- 别乱用UUID了,自增ID和UUID性能差距你测试过吗? - Weixin/Java知音
- 分布式ID详解(5种分布式ID生成方案) - CSDN
- 分布式ID生成方案 - CSDN
- 分布式唯一ID几种生成方案 - CSDN
- 面试官:讲讲雪花算法,越详细越好 - Zhihu
- 【SnowFlake】雪花算法(Java版本) - CSDN
- 一种适合容器化部署的雪花算法ID生成器 - 掘金
本文暂时没有评论,来添加一个吧(●'◡'●)