文章目录
- 本系列
- 前言
- 号段模式
- 多DB支持
- tinyid-client
本系列
- 漫谈分布式唯一ID
- 分布式唯一ID生成(二):leaf
- 分布式唯一ID生成(三):uid-generator
- 分布式唯一ID生成(四):tinyid(本文)
前言
tinyid的主要特性有:
- 生成全局唯一的64位数字ID
- 趋势递增的id:趋势递增的意思是,id是递增但不一定是连续的
- 支持生成1,3,5,7,9…序列的ID
- 支持配置多个db,每次随机从一个db获取号段,提高可用性
- 支持client获取一批ID,然后本地发号,提升性能
适用场景:只要求ID是数字,趋势递增的系统
不适用场景:类似于订单的业务,因为生成的ID大部分是连续的,容易被扫库、或者推算出订单量等信息
本文侧重介绍leaf上没有的一些特性
源码:https://github.com/didi/tinyid
号段模式
号段模式的分布式ID需要在db中记录上一次分配到哪了,号段有多长等信息。表结构如下:
CREATE TABLE `tiny_id_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增主键', `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '业务类型,唯一', `begin_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '开始id,仅记录初始值,无其他含义。初始化时begin_id和max_id应相同', `max_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '当前最大id', `step` int(11) DEFAULT '0' COMMENT '步长', `delta` int(11) NOT NULL DEFAULT '1' COMMENT '每次id增量', `remainder` int(11) NOT NULL DEFAULT '0' COMMENT '余数', `create_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '创建时间', `update_time` timestamp NOT NULL DEFAULT '2010-01-01 00:00:00' COMMENT '更新时间', `version` bigint(20) NOT NULL DEFAULT '0' COMMENT '版本号', PRIMARY KEY (`id`), UNIQUE KEY `uniq_biz_type` (`biz_type`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'id信息表';
重点介绍这些字段:
- biz_type:代表业务类型,不同业务的id隔离
- max_id:上一个号段最大分配到哪个ID了
- step:号段的长度,可以根据每个业务的qps来设置一个合理的长度
- version:乐观锁,每次更新都加上version,保证并发更新的正确性
- delta和remainder用于支持多DB,下文分析
通过乐观锁的方式获取号段:
先查出biz_type对应的maxId,step,version:
select id, biz_type, begin_id, max_id, step, delta, remainder, create_time, update_time, version
from tiny_id_info where biz_type = ?
查到max_id后,将其更新为 max_id + step
,执行更新sql:
update tiny_id_info set max_id= ?, update_time=now(), version=version+1
where id=? and max_id=? and version=? and biz_type=?
如果更新成功,就获得了 [ max_id+1 : max_id + step ]
这个区间的号段
和leaf一样,tinyid也在内存中维护了双buffer,默认为当前号段消耗到 20%
时,就异步去db加载下一个号段。这样当前号段用完时,能马上切换到写一个号段,解决TP999高的问题,可以参考分布式唯一ID生成(二):leaf
关于鉴权,tinyid把权限数据存储到了另一张表tiny_id_token,和leaf一样,提前把这部分数据全量加载到本地内存,请求到来时直接在内存中鉴权,大大提高性能,
多DB支持
当只有一个db时,有严重的单点问题,无法做到高可用。tinyId支持多个DB,每次获取号段时,可以从任意一个db上获取,因此只要有一个db都能让服务可用
那么如果从多个DB都获取到了同一号段,我们怎么保证生成的id不重呢?tinyid是这么做的,引入了 步长delta
和 余数remainder
的概念
- delta:代表从号段中每次获取ID增加的步长
- remainder:代表当前号段只能获得
% delta,余数为remainder
的ID
假设在3个db中分别有如下记录
db1:
id | biz_type | max_id | step | delta | remainder | version |
---|---|---|---|---|---|---|
1 | bizA | 1000 | 1000 | 3 | 0 | 0 |
db2:
id | biz_type | max_id | step | delta | remainder | version |
---|---|---|---|---|---|---|
1 | bizA | 1000 | 1000 | 3 | 1 | 0 |
db3:
id | biz_type | max_id | step | delta | remainder | version |
---|---|---|---|---|---|---|
1 | bizA | 1000 | 1000 | 3 | 2 | 0 |
那么:
从db1拿到号段生成的的序列为:0,3,6,9...
从db2拿到号段生成的的序列为:1,4,7,10...
从db3拿到号段生成的的序列为:2,5,8,11...
对应源码如下:
public void init() { if (isInit) { return; } synchronized (this) { if (isInit) { return; } long id = currentId.get(); /** * 例如:delta=3, remainder在3个db上分别为0,1,2 * 从db1拿到的序列为:0,3,6,9... * 从db2拿到的序列为:1,4,7,10... * 从db3拿到的序列为:2,5,8,11... */ if (id % delta == remainder) { isInit = true; return; } for (int i = 0; i <= delta; i++) { id = currentId.incrementAndGet(); if (id % delta == remainder) { // 避免浪费 减掉系统自己占用的一个id currentId.addAndGet(0 - delta); isInit = true; return; } } }
}
tinyid-client
使用http获取一个id,存在网络开销,是否可以本地生成id?
为此提供了tinyid-client,可以向tinyid-server发送请求来获取可用号段,之后在本地构建双号段、本地发号
最终架构图如下:
优点为:
- 性能大大提升,如此id生成则变成纯本地操作
- client和server可以跨机房部署,因为一个号段只用调一次server,就算跨机房延迟高也不会影响业务
- 可用性也大大提升,因为本地缓存了一部分ID,可以容忍tinyid-server一段时间宕机
- 降低对tiny-server的压力,访问tiny-server的频率从变为原来的1/step
缺点为:
- 如果client启动频繁,可能浪费很多id