您的当前位置:首页正文

分布式唯一ID生成(四):tinyid

2024-11-30 来源:个人技术集锦

本系列



前言

tinyid的主要特性有:

  • 生成全局唯一的64位数字ID
  • 趋势递增的id:趋势递增的意思是,id是递增但不一定是连续的
  • 支持生成1,3,5,7,9…序列的ID
  • 支持配置多个db,每次随机从一个db获取号段,提高可用性
  • 支持client获取一批ID,然后本地发号,提升性能

适用场景只要求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:代表业务类型,不同业务的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高的问题,可以参考


关于鉴权,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:

idbiz_typemax_idstepdeltaremainderversion
1bizA10001000300

db2:

idbiz_typemax_idstepdeltaremainderversion
1bizA10001000310

db3:

idbiz_typemax_idstepdeltaremainderversion
1bizA10001000320

那么:
从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
显示全文