tinyid的主要特性有:
适用场景:只要求ID是数字,趋势递增的系统
不适用场景:类似于订单的业务,因为生成的ID大部分是连续的,容易被扫库、或者推算出订单量等信息
本文侧重介绍leaf上没有的一些特性
号段模式的分布式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对应的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高的问题,可以参考
关于鉴权,tinyid把权限数据存储到了另一张表tiny_id_token,和leaf一样,提前把这部分数据全量加载到本地内存,请求到来时直接在内存中鉴权,大大提高性能,
当只有一个db时,有严重的单点问题,无法做到高可用。tinyId支持多个DB,每次获取号段时,可以从任意一个db上获取,因此只要有一个db都能让服务可用
那么如果从多个DB都获取到了同一号段,我们怎么保证生成的id不重呢?tinyid是这么做的,引入了 步长delta
和 余数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;
}
}
}
}
使用http获取一个id,存在网络开销,是否可以本地生成id?
为此提供了tinyid-client,可以向tinyid-server发送请求来获取可用号段,之后在本地构建双号段、本地发号
最终架构图如下:
优点为:
缺点为: