分布式ID详解(一站式)

分布式ID需要满足的条件:

  • 全局唯一:不能出现重复的ID
  • 高性能,高可用:生成ID的速度快,以及需要接近100%可用
  • 趋势递增:由于大多数的数据库使用B-树按索引有序存储数据,主键ID递增可以保证新增记录时不会发生页分裂,保证写入性能
  • 信息安全:如果ID连续或者规则明显,恶意用户或竞争对手爬取信息会很方便。因此一些场景,如订单会要求id不规则。

分布式ID的几种实现:

1. UUID

UUID是一个128位的全球唯一标识符,使用32个16进制位,用-分为五段,形式为8-4-4-4-12。通常格式:xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx;M:版本号(如 1、3、4、5);V2几乎不用,较冷门。

  • UUID v1: f81d4fae-7dec-11d0-a765-00a0c91e6bf6(时间 + MAC)
  • UUID v3: 3c5e56b7-0c39-3a76-8575-b8f6efdb01b0(MD5 哈希)
  • UUID v4: 550e8400-e29b-41d4-a716-446655440000(随机生成)
  • UUID v5: 21f7f8de-8051-5b89-8680-0195ef798b6a(SHA-1 哈希)

优点:本地生成,性能非常高

缺点:V1版本可能会导致mac地址泄漏;UUID太长,不适合作为数据库主键(主键应当越短越好);UUID无序,新增记录导致索引变动频繁,严重影响写入性能。

2. 雪花算法(SnowFlake)

Twitter公司采用并开源的一种算法,能在分布式系统中产生全局唯一且趋势递增的64位ID

| 1 | 41位时间戳 | 10位机器ID | 12位序列号 |
部分位数说明
符号位1始终为 0
时间戳41当前时间戳(一般是相对某个起始时间的毫秒数)
机器 ID10数据中心 + 机器编号
序列号12同一毫秒内的并发序列(0~4095),故理论QPS可达到409.6w/s

优点:本地生成,生成的ID趋势递增,高性能

缺点:强依赖于机器时钟,如果发生时钟回拨,将导致ID重复或服务不可用。

注意:机器ID需要唯一分配,防止冲突

3. 数据库自增ID实现

**用一个专门的表生成自增ID,提供给其他表使用。**以MySQL为例,创建下面的这张表,当需要一个ID时,向表中插入一条记录返回主键id即可。

CREATE TABLE generate_id {
 id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
 content CHAR(1) NOT NULL DEFAULT '' COMMENT '无实际意义',
 PRIMARY KEY (id),
} ENGINE=INNODB;

缺点:依赖数据库服务,有单点故障的风险,且有非常明显的性能瓶颈。

解决方式有两个:采用数据库集群号段模式(后面的美团组件就有用到)

数据库集群:

节点1生成的ID:1、4、7、10......
节点2生成的ID:2、5、8、11......
节点3生成的ID:3、6、9、12......

约定好起始值和步长,步长就是机器的个数。
缺点:水平扩展比较困难,因为起始值和步长都是预先设定好的。

号段模式:

REATE TABLE `segment_id` (
  `biz_tag` VARCHAR(32) NOT NULL DEFAULT '' COMMENT '业务类型',
  `max_id` BIGINT(20) NOT NULL DEFAULT '1' COMMENT '当前最大id',
  `step` INT(11) NOT NULL COMMENT '号段步长',
  `version` INT(20) NOT NULL COMMENT '版本号',
  `update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`biz_tag`)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

一次请求将从数据库获取一批自增ID,减小访库次数,降低数据库读写压力。当需要ID时,先发起查询,然后更新max_id,更新成功则表示获取到新号段[max_id, max_id+step)

4. 开源组件-美团leaf

leaf是美团基础研发平台推出的一个分布式ID生成服务,有Segment(号段)和SnowFlake两种模式

Segment模式

Leaf 启动时,从 DB 中取出一段 ID 区间(如 1000~1999);本地内存生成 ID;当接近用完时,后台异步去 DB 预取下一段。数据库表结构如下:

CREATE TABLE leaf_alloc (
  biz_tag VARCHAR(128) NOT NULL PRIMARY KEY,
  max_id BIGINT NOT NULL,
  step INT NOT NULL,
  update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
每种业务一个 biz_tag(如订单、用户),分开管理。

Leaf采用了双Buffer异步更新的策略,保证无论何时,都能有一个Buffer的号段可以正常对外提供服务。

动态调整step:T位号段的更新周期

T < 15min,nextStep = step * 2,对应高QPS

15min < T < 30min,nextStep = step,对应正常QPS

T > 30min,nextStep = step / 2,对应低QPS

SnowFlake模式

基于上面的雪花算法实现,做了一些改进

使用zookeeper来分配机器ID

Leaf依靠zookeeper为各节点生成递增的workerId

leaf节点首次启动时,会连接zookeeper并在/snowflake/{leaf.name}/forever下创建永久的有序节点,节点序号是workid;是一个key-value对,keyip+port+workidvalue为是一个json,记录ipport以及时间戳。

若不是首次启动,leaf节点会连接zookeeper读取/snowflake/{leaf.name}/forever的所有节点,根据ip+port找到对应的key,再从key中获取workid

一旦获取到workid,便会存储到本地文件中;当启动leaf节点时zookeeper故障了,那么可以从本地文件中读取workid

应对时钟回拨

启动时检查:leaf节点启动后,会每隔3秒更新节点数据(更新key-valuevalue的时间戳),上传当前服务器的时间,该时间用来leaf节点启动时与机器的本地时间作比较;若本地时间小,说明发生时间回拨;则抛异常,启动失败

运行时检查:每次生成ID都会将当前时间与上一个ID的时间进行比较,若当前时间小,说明发生时钟回拨,若回拨时间小于5ms,说明系统时间只是轻微不一致,采取容忍策略,让线程等待一段时间后重试,等待时间为回拨时间的两倍;若大于5ms,出于安全考虑,返回负数,组织生成ID

总结:

虽然做了一些努力,但是Leaf并没有完全解决时钟回拨问题。我们看下面两个场景:

启动前,服务器时间进行了回拨;启动时连接Zookeeper失败,会使用本地文件中保存的workerId此时跳过了时间检查将启动成功,可能会造成ID重复。

Leaf节点上报给zookeeper的时间戳是2024-01-16 08:15:00.000,最后一次生成的ID时间戳是2024-01-16 08:15:02.999,还没来得及再次上报zk本地时间,该节点宕机了。在启动之前,发生了时钟回拨,该节点重启时本地时间为2024-01-16 08:15:01.000;大于zookeeper中记录的时间戳,允许启动。但是,接下来两秒内生成的ID,都可能是之前已经生成过的。(时钟回拨后的时间小于最后一次生成ID的时间,但是大于上报给zookeeper的时间)

Leaf运行中发现回拨超过5ms,会返回负数。

使用方式:
拉取源码或使用 jar 包;
部署数据库(Segment 模式)或 ZooKeeper(Snowflake 模式);
配置业务标签;
通过 API 获取 ID:
	HTTP 接口:GET /api/segment/get/{key}
	Java SDK 调用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liubo666_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值