分布式主键ID

目录

分布式ID

满足条件

目前主流生成方式

UUID

实现方式

基于数据库自增ID

实现方式

基于数据库集群模式

实现方式

基于数据库的号段模式

实现方式

基于Redis模式

实现方式

基于雪花算法(Snowflake)模式

实现方式

百度(uid-generator)

实现方式

美团(Leaf)

号段模式

snowflake算法模式

实现方式

滴滴(Tinyid)

接入方式

总结


分布式ID

一般在业务数据不大时,如mysql在100W以下时,单库单表来可以支撑业务;增大到1000W级以上时,可以利用mysql集群的主从同步及读写分离来应付。但业务数据量再增大数亿级时,并且高并发QPS数据级大,主从同步也抗不住了,就需要对数据库进行分表分库,此时依赖数据库的自增ID已不满足需求。需要能够生成一个全局唯一性的ID,这个ID就是分布ID。

满足条件

  • 全局唯一:必须保证ID是全局性唯一的
  • 高性能:高可用低延时,耗时少,ID生成响应快
  • 高可用:100%的可用性是很难达到的,但是也要无限接近于100%的可用性
  • 易接入:可提供API或SDK等方式接入,拿来即用

目前主流生成方式

  • UUID
  • 基于数据库自增ID
  • 基于数据库集群多主模式
  • 号段模式
  • Redis
  • 雪花算法(SnowFlake)
  • 滴滴出品(TinyID)
  • 百度 (Uidgenerator)
  • 美团(Leaf)

UUID

使用简单,但由于生成的id由字母和数字组成,且36位过长,存储性能差,不适用于实际业务需求,不推荐。

优点:

  • 生成足够简单,本地生成无网络消耗,具有唯一性

点:

  • 无序的字符串,不具备趋势自增特性
  • 长度过长16 字节128位,36位长度的字符串,存储以及查询对MySQL的性能消耗较大,作为数据库主键UUID的无序性会导致数据位置频繁变动,严重影响性能。

实现方式

可依赖hutool组件来使用

package com.cp3.shardingsphere.utils.uuid;

import cn.hutool.core.util.IdUtil;

/**
 * @Title: R
 * @Description:
 * @author: huhua
 * @date: 2021/3/13 18:04
 */
public class UUIDUtil {

    /**
     * 带-的UUID字符串
     * @return
     */
    public static String randomUUID(){
        return IdUtil.randomUUID();
    }
    /**
     * 不带-的UUID字符串,32位
     * @return
     */
    public static String simpleUUID(){
        return IdUtil.simpleUUID();
    }
}

基于数据库自增ID

基于数据库的auto_increment自增ID理论上可以充当分布式id。

实现方式

运行一个单独的MySQL实例用来生成ID,建表结构如下:

CREATE DATABASE `SEQ_ID`;
CREATE TABLE SEQID.SEQUENCE_ID (
    id bigint(20) unsigned NOT NULL auto_increment, 
    value char(10) NOT NULL default '',
    PRIMARY KEY (id),
) ENGINE=MyISAM;
insert into SEQUENCE_ID(value) VALUES ('values');

当我们获取一个ID的时候,向表中插入一条记录并返回主键。但访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐

优点:

  • 实现简单,ID单调自增,数值类型查询速度快

缺点:

  • DB单点存在宕机风险,无法扛住高并发场景

基于数据库集群模式

此方式是对单节点数据库问题,进化成主从模式集群。依靠两个Mysql实例都能单独的生产自增ID。

实现方式

设置起始值和自增步长

MySQL_1 配置:

set @@auto_increment_offset = 1;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长
//自增ID分别  1 3 5 7 9 

MySQL_2 配置:

set @@auto_increment_offset = 2;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长
//自增ID分别  2 4 6 8 0

 

对于高并发,可进行MySQL扩容增加节点,但此时起始值和自增步长需要对应修改,会影响现有功能

优点:

  • 解决DB单点问题

缺点:

  • 不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。

基于数据库的号段模式

号段模式是目前实现分布式ID生成器的一种主流实现思想,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。

实现方式

  1. 表结构说明:
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 '版本号',
  PRIMARY KEY (`id`)
)

biz_type :代表不同业务类型

max_id :当前最大的可用id

step :代表号段的长度

version :是一个乐观锁,每次都更新version,保证并发时数据的正确性

idbiz_typemax_idstepversion
1101100020000
  1. 当一批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,update max_id = max_id  step,update成功则说明新号段获取成功,新的号段范围是(max_id, max_id+step)。
update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX

由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分工式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。

基于Redis模式

基于Redis模式原理是利用Redis的原子性自增。

实现方式

根据Redis的incr命令实现ID的原子性自增。

127.0.0.1:16379> set seq_id 1     // 初始化自增ID为1
OK
127.0.0.1:16379> incr seq_id      // 增加1,并返回递增后的数值
(integer) 2

注意Redis两种持久化方式RDB、AOF

  • RDB会定时打一个快照进行持久化,假如连续自增但Redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。
  • AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。
  • 目前也支持两种方式结合方式

基于雪花算法(Snowflake)模式

雪花算法(Snowflake)是twitter公司内部分布式项目采用的ID生成算法,目前已开源,且各大公司根据Snowflake开发出各具特色的分布式生成器。

image.png

如图:41-bit的时间可以表示(1L<<41)/(1000L*3600*24*365)=69年的时间,10-bit机器可以分别表示1024台机器。如果我们对IDC划分有需求,还可以将10-bit分5-bit给IDC,分5-bit给工作机器。这样就可以表示32个IDC,每个IDC下可以有32台机器,可以根据自身需求定义。12个自增序列号可以表示2^12个ID,理论上snowflake方案的QPS约为409.6w/s,这种分配方式可以保证在任何一个IDC的任何一台机器在任意毫秒内生成的ID都是不同的。

结构

Snowflake生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8bit,也就是说一个Long类型占64个bit。

Snowflake ID组成结构:正数位(占1bit)+ 时间戳 (占41bit)+ 机器ID (占5bit)+ 数据中心 (占5bit)+ 自增值(占12bit),总共64比特组成的一个Long类型。

  • 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L 60 60 24 365) = 69年
  • 工作机器id(10bit):也被叫做workId,可以自由灵活配置,机房或者机器号组合都可以。
  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID

根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

实现方式

  1. 算法实现:
package com.cp3.shardingsphere.utils.twitter;

/**
 * @Title: IdWorker  Twitter的分布式自增ID雪花算法snowflake (Java版)
 * @Description:
 * @author: huhua
 * @date: 2021/3/29 20:59
 */
public class IdWorker {

    /**
     * 起始的时间戳
     */
    private final static long START_STMP = 1607155485176L;

    /**
     * 每一部分占用的位数
     */
    private final static long SEQUENCE_BIT = 12; //序列号占用的位数
    private final static long MACHINE_BIT = 5;   //机器标识占用的位数
    private final static long DATACENTER_BIT = 5;//数据中心占用的位数

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private long datacenterId;  //数据中心
    private long machineId;     //机器标识
    private long sequence = 0L; //控制序列号
    private long actualSequence = 0L;  // 实际使用的序列号
    private long lastStmp = -1L;//上一次时间戳

    public IdWorker(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

    /**
     * 产生下一个ID
     * @return
     */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }
        /**
         * 时间不连续出来全是偶数
         */
        if (currStmp == lastStmp) {
            //相同毫秒内,序列号自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列数已经达到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒内,序列号置为0
            sequence = 0L;
        }
        // 上面那个控制序列号sequence,控制了一毫秒内不会超过MAX_SEQUENCE(4096)个,超过会等待,直到下一毫秒才会继续
        // 上面那个序列号sequence如果一毫秒只生成一个id,那么它永远都是0,那么取模永远都是0,插入的表也就可以理解为都是0表(或规律的那几张表),达不到均匀分布在各表的目的
        // 所以用下面这个序列号actualSequence来生成均匀的取模id,达到均匀分布在各表的目的
        actualSequence = (actualSequence + 1) & MAX_SEQUENCE;

        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //时间戳部分
                | datacenterId << DATACENTER_LEFT       //数据中心部分
                | machineId << MACHINE_LEFT             //机器标识部分
                | actualSequence;                             //序列号部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private long getNewstmp() {
        return System.currentTimeMillis();
    }
}
  1. 向外提供uilts.
/**
 * @Title: IdWorkerUtils
 * @Description:
 * @author: huhua
 * @date: 2021/3/29 21:00
 */
public class IdWorkerUtils {
    private static IdWorker worker = new IdWorker(1,1);
    public static long getId() {
        return worker.nextId();
    }
}

百度(uid-generator)

百度出品,github:https://github.com/baidu/uid-generator

UidGenerator 基于Snowflake算法实现唯一ID生成器,通过借用未来时间来解决 sequence 天然存在的并发限制;采用 RingBuffer 来缓存已生成的UID,并行化 UID 的生产和消费,同时对 CacheLine 补齐,避免了由 RingBuffer 带来的硬件级「伪共享」问题。最终单机 QPS 可达 600 万。支持自定义 WorkerID 位数和初始化策略,从而适用于 Docker 等虚拟化环境下实例自动重启、漂移等场景。

uid-generator是基于Snowflake算法实现的,与原始的snowflake算法不同在于,uid-generator支持自定义时间戳、工作机器ID和 序列号 等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。

uid-generator需要与数据库配合使用,需要新增一个WORKER_NODE表。当应用启动时会向数据库表中去插入一条数据,插入成功后返回的自增ID就是该机器的workId数据由host,port组成。

组成结构

workId占用了22个bit位,时间占用了28个bit位,序列化占用了13个bit位,需要注意的是,和原始的snowflake不太一样,时间的单位是秒,而不是毫秒,workId也不一样,而且同一应用每次重启就会消费一个workId。

实现方式

  1. 创建表WORKER_NODE

运行sql脚本以导入表WORKER_NODE, 脚本如下:

DROP DATABASE IF EXISTS `xxxx`;
CREATE DATABASE `xxxx` ;
use `xxxx`;
DROP TABLE IF EXISTS WORKER_NODE;
CREATE TABLE WORKER_NODE
(
ID BIGINT NOT NULL AUTO_INCREMENT COMMENT '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 UID Generator',ENGINE = INNODB;

修改mysql.properties配置中, jdbc.url、jdbc.username和jdbc.password,确保库地址、 名称、 端口号、 用户名和密码正确。

  1. 修改Spring配置

提供了两种生成器: DefaultUidGenerator、CachedUidGenerator。如对UID生成性能有要求, 请使用CachedUidGenerator

对应Spring配置分别为: default-uid-spring.xml、cached-uid-spring.xml

DefaultUidGenerator配置

<!-- DefaultUidGenerator -->
<bean id="defaultUidGenerator" class="com.baidu.fsg.uid.impl.DefaultUidGenerator" lazy-init="false">
    <property name="workerIdAssigner" ref="disposableWorkerIdAssigner"/>
    <!-- Specified bits & epoch as your demand. No specified the default value will be used -->
    <property name="timeBits" value="29"/>
    <property name="workerBits" value="21"/>
    <property name="seqBits" value="13"/>
    <property name="epochStr" value="2021-03-28"/>
</bean>
 
<!-- 用完即弃的WorkerIdAssigner,依赖DB操作 -->
<bean id="disposableWorkerIdAssigner" class="com.baidu.fsg.uid.worker.DisposableWorkerIdAssigner" />

CachedUidGenerator配置

<!-- CachedUidGenerator -->
<bean id="cachedUidGenerator" class="com.baidu.fsg.uid.impl.CachedUidGenerator">
    <property name="workerIdAssigner" ref="disposableWorkerIdAssigner" />
 
    <!-- 以下为可选配置, 如未指定将采用默认值 -->
    <!-- Specified bits & epoch as your demand. No specified the default value will be used -->
    <property name="timeBits" value="29"/>
    <property name="workerBits" value="21"/>
    <property name="seqBits" value="13"/>
    <property name="epochStr" value="2021-03-28"/>
 
    <!-- RingBuffer size扩容参数, 可提高UID生成的吞吐量. -->
    <!-- 默认:3, 原bufferSize=8192, 扩容后bufferSize= 8192 << 3 = 65536 -->
    <property name="boostPower" value="3"></property>
 
    <!-- 指定何时向RingBuffer中填充UID, 取值为百分比(0, 100), 默认为50 -->
    <!-- 举例: bufferSize=1024, paddingFactor=50 -> threshold=1024 * 50 / 100 = 512. -->
    <!-- 当环上可用UID数量 < 512时, 将自动对RingBuffer进行填充补全 -->
    <property name="paddingFactor" value="50"></property>
 
    <!-- 另外一种RingBuffer填充时机, 在Schedule线程中, 周期性检查填充 -->
    <!-- 默认:不配置此项, 即不实用Schedule线程. 如需使用, 请指定Schedule线程时间间隔, 单位:秒 -->
    <property name="scheduleInterval" value="60"></property>
 
    <!-- 拒绝策略: 当环已满, 无法继续填充时 -->
    <!-- 默认无需指定, 将丢弃Put操作, 仅日志记录. 如有特殊需求, 请实现RejectedPutBufferHandler接口(支持Lambda表达式) -->
    <property name="rejectedPutBufferHandler" ref="XxxxYourPutRejectPolicy"></property>
 
    <!-- 拒绝策略: 当环已空, 无法继续获取时 -->
    <!-- 默认无需指定, 将记录日志, 并抛出UidGenerateException异常. 如有特殊需求, 请实现RejectedTakeBufferHandler接口(支持Lambda表达式) -->
    <property name="rejectedTakeBufferHandler" ref="XxxxYourTakeRejectPolicy"></property>
 
</bean>
 
<!-- 用完即弃的WorkerIdAssigner, 依赖DB操作 -->
<bean id="disposableWorkerIdAssigner" class="com.baidu.fsg.uid.worker.DisposableWorkerIdAssigner" />

Mybatis配置

<!-- Spring annotation扫描 -->
<context:component-scan base-package="com.baidu.fsg.uid" />
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="mapperLocations" value="classpath:/META-INF/mybatis/mapper/M_WORKER*.xml" />
</bean>
<!-- 事务相关配置 -->
<tx:annotation-driven transaction-manager="transactionManager" order="1" />
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
 <property name="dataSource" ref="dataSource" />
</bean>
<!-- Mybatis Mapper扫描 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
 <property name="annotationClass" value="org.springframework.stereotype.Repository" />
 <property name="basePackage" value="com.baidu.fsg.uid.worker.dao" />
 <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
<!-- 数据源配置 -->
<bean id="dataSource" parent="abstractDataSource">
 <property name="driverClassName" value="${mysql.driver}" />
 <property name="maxActive" value="${jdbc.maxActive}" />
 <property name="url" value="${jdbc.url}" />
 <property name="username" value="${jdbc.username}" />
 <property name="password" value="${jdbc.password}" />
</bean>
<bean id="abstractDataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close">
 <property name="filters" value="${datasource.filters}" />
 <property name="defaultAutoCommit" value="${datasource.defaultAutoCommit}" />
 <property name="initialSize" value="${datasource.initialSize}" />
 <property name="minIdle" value="${datasource.minIdle}" />
 <property name="maxWait" value="${datasource.maxWait}" />
 <property name="testWhileIdle" value="${datasource.testWhileIdle}" />
 <property name="testOnBorrow" value="${datasource.testOnBorrow}" />
 <property name="testOnReturn" value="${datasource.testOnReturn}" />
 <property name="validationQuery" value="${datasource.validationQuery}" />
 <property name="timeBetweenEvictionRunsMillis" value="${datasource.timeBetweenEvictionRunsMillis}" />
 <property name="minEvictableIdleTimeMillis" value="${datasource.minEvictableIdleTimeMillis}" />
 <property name="logAbandoned" value="${datasource.logAbandoned}" />
 <property name="removeAbandoned" value="${datasource.removeAbandoned}" />
 <property name="removeAbandonedTimeout" value="${datasource.removeAbandonedTimeout}" />
</bean>
<bean id="batchSqlSession" class="org.mybatis.spring.SqlSessionTemplate">
 <constructor-arg index="0" ref="sqlSessionFactory" />
 <constructor-arg index="1" value="BATCH" />
</bean>
  1. 运行示例

运行单测CachedUidGeneratorTest, 展示UID生成、解析等功能

@Resource
private UidGenerator uidGenerator;
@Test
public void testSerialGenerate() {
    // Generate UID
    long uid = uidGenerator.getUID();
   System.out.println(uidGenerator.parseUID(uid));
}

美团(Leaf)

美团出品,github:https://github.com/Meituan-Dianping/Leaf

Leaf的性能在4C8G的机器上QPS能压测到近5w/s,TP999 1ms。同时支持号段模式和snowflake算法模式,可以切换使用。

号段模式

即Leaf-segment方案,在使用数据库的方案上,做了如下改变:

  • 原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。
  • 各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。

数据库表设计如下:

+-------------+--------------+------+-----+-------------------+-----------------------------+| Field       | Type         | Null | Key | Default           | Extra                       |
+-------------+--------------+------+-----+-------------------+-----------------------------+| biz_tag     | varchar(128) | NO   | PRI |                   |                             |
| max_id      | bigint(20)   | NO   |     | 1                 |                             |
| step        | int(11)      | NO   |     | NULL              |                             |
| desc        | varchar(256) | YES  |     | NULL              |                             |
| update_time | timestamp    | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+-------------+--------------+------+-----+-------------------+-----------------------------+

重要字段说明:biz_tag用来区分业务,max_id表示该biz_tag目前所被分配的ID号段的最大值,step表示每次分配的号段长度。原来获取ID每次都需要写数据库,现在只需要把step设置得足够大,比如1000。那么只有当1000个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从1减小到了1/step,大致架构如下图所示:

image

test_tag在第一台Leaf机器上是1~1000的号段,当这个号段用完时,会去加载另一个长度为step=1000的号段,假设另外两台号段都没有更新,这个时候第一台机器新加载的号段就应该是3001~4000。同时数据库对应的biz_tag这条数据的max_id会从3000被更新成4000,更新号段的SQL语句如下:

BeginUPDATE table SET max_id=max_id+step WHERE biz_tag=xxxSELECT tag, max_id, step FROM table WHERE biz_tag=xxxCommit

这种模式有以下优缺点:

优点:

  • Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
  • ID号码是趋势递增的8byte的64位数字,满足上述数据库存储的主键要求。
  • 容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务。
  • 可以自定义max_id的大小,非常方便业务从原有的ID方式上迁移过来。

缺点:

  • ID号码不够随机,能够泄露发号数量的信息,不太安全。
  • TP999数据波动大,当号段使用完之后还是会hang在更新数据库的I/O上,tg999数据会出现偶尔的尖刺。
  • DB宕机会造成整个系统不可用。

双buffer优化

Leaf 取号段的时机是在号段消耗完的时候进行的,也就意味着号段临界点的ID下发时间取决于下一次从DB取回号段的时间,并且在这期间进来的请求也会因为DB号段没有取回来,导致线程阻塞。如果请求DB的网络和DB的性能稳定,这种情况对系统的影响是不大的,但是假如取DB的时候网络发生抖动,或者DB发生慢查询就会导致整个系统的响应时间变慢。

为此,为了DB取号段的过程能够做到无阻塞,不需要在DB取号段的时候阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。这样做就可以很大程度上的降低系统的TP999指标。详细实现如下图所示:

image

采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

  • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。

Leaf高可用容灾

对于第三点“DB可用性”问题,目前采用一主两从的方式,同时分机房部署,Master和Slave之间采用半同步方式同步数据。同时使用Atlas数据库中间件做主从切换。当然这种方案在一些情况会退化成异步模式,甚至在非常极端情况下仍然会造成数据不一致的情况,但是出现的概率非常小。如果系统要保证100%的数据强一致,可以选择使用“类Paxos算法”实现的强一致MySQL方案,但是运维成本和精力都会相应的增加,根据实际情况选型即可。

image

同时Leaf服务分IDC部署,内部的服务化框架是“MTthrift RPC”。服务调用的时候,根据负载均衡算法会优先调用同机房的Leaf服务。在该IDC内Leaf服务不可用的时候才会选择其他机房的Leaf服务。同时服务治理平台OCTO还提供了针对服务的过载保护、一键截流、动态流量分配等对服务的保护措施。

snowflake算法模式

Leaf-snowflake方案完全沿用snowflake方案的bit位设计,即是“1+41+10+12”的方式组装ID号。对于workerID的分配,当服务集群数量较小的情况下,完全可以手动配置。Leaf服务规模较大,动手配置成本太高。所以使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID。Leaf-snowflake是按照下面几个步骤启动的:

  1. 启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)。
  2. 如果有注册过直接取回自己的workerID(zk顺序节点生成的int类型ID号),启动服务。
  3. 如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerID号,启动服务。

image

弱依赖ZooKeeper

除了每次会去ZK拿数据以外,也会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。这样做到了对三方组件的弱依赖。一定程度上提高了SLA。

实现方式

  • 号段模式
  1. 需依赖表leaf_alloc
DROP TABLE IF EXISTS `leaf_alloc`;
CREATE TABLE `leaf_alloc` (
  `biz_tag` varchar(128)  NOT NULL DEFAULT '' COMMENT '业务key',
  `max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '当前已经分配了的最大id',
  `step` int(11) NOT NULL COMMENT '初始步长,也是动态调整的最小步长',
  `description` varchar(256)  DEFAULT NULL COMMENT '业务key的描述',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据库维护的更新时间',
  PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;
  1. 在项目中开启号段模式,配置对应的数据库信息,并关闭Snowflake模式
leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf_test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
leaf.jdbc.username=root
leaf.jdbc.password=root
leaf.snowflake.enable=false
#leaf.snowflake.zk.address=
#leaf.snowflake.port=
  1. 启动leaf-server模块的LeafServerApplication

号段模式获取分布式自增ID的测试url :http://localhost:8080/api/segment/get/leaf-segment-test

监控号段模式:http://localhost:8080/cache

  • snowflake模式

Leaf的snowflake模式依赖于ZooKeeper,不同于原始snowflake算法也主要是在workId的生成上,Leaf中workId是基于ZooKeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会都在Zookeeper中生成一个顺序Id,相当于一台机器对应一个顺序节点,也就是一个workId。

leaf.snowflake.enable=true
leaf.snowflake.zk.address=127.0.0.1
leaf.snowflake.port=2181

snowflake模式获取分布式自增ID的测试url:http://localhost:8080/api/snowflake/get/test

滴滴(Tinyid)

滴滴出品,github地址:https://github.com/didi/tinyid

架构图


image

  • nextId和getNextSegmentId是tinyid-server对外提供的两个http接口
  • nextId是获取下一个id,当调用nextId时,会传入bizType,每个bizType的id数据是隔离的,生成id会使用该bizType类型生成的IdGenerator。
  • getNextSegmentId是获取下一个可用号段,tinyid-client会通过此接口来获取可用号段
  • IdGenerator是id生成的接口
  • IdGeneratorFactory是生产具体IdGenerator的工厂,每个biz_type生成一个IdGenerator实例。通过工厂,我们可以随时在db中新增biz_type,而不用重启服务
  • IdGeneratorFactory实际上有两个子类IdGeneratorFactoryServer和IdGeneratorFactoryClient,区别在于,getNextSegmentId的不同,一个是DbGet,一个是HttpGet
  • CachedIdGenerator则是具体的id生成器对象,持有currentSegmentId和nextSegmentId对象,负责nextId的核心流程。nextId最终通过AtomicLong.andAndGet(delta)方法产生。

主要思想

Tinyid主要思想是对号段生成方案进行优化:

  • 双号段缓存

 对于号段用完需要访问db,我们很容易想到在号段用到一定程度的时候,就去异步加载下一个号段,保证内存中始终有可用号段,则可避免性能波动。

  • 增加多db支持

 db只有一个master时,如果db不可用(down掉或者主从延迟比较大),则获取号段不可用。实际上我们可以支持多个db,比如2个db,A和B,我们获取号段可以随机从其中一台上获取。那么如果A,B都获取到了同一号段,我们怎么保证生成的id不重呢?tinyid是这么做的,让A只生成偶数id,B只生产奇数id,对应的db设计增加了两个字段,如下所示

idbiz_typemax_idstepdeltaremainderversion
1100020001000200

 delta代表id每次的增量,remainder代表余数,例如可以将A,B都delta都设置2,remainder分别设置为0,1则,A的号段只生成偶数号段,B是奇数号段。 通过delta和remainder两个字段我们可以根据使用方的需求灵活设计db个数,同时也可以为使用方提供只生产类似奇数的id序列。

  • 增加tinyid-client

 使用http获取一个id,存在网络开销,是否可以本地生成id?为此我们提供了tinyid-client,我们可以向tinyid-server发送请求来获取可用号段,之后在本地构建双号段、id生成,如此id生成则变成纯本地操作,性能大大提升,因为本地有双号段缓存,则可以容忍tinyid-server一段时间的down掉,可用性也有了比较大的提升。

接入方式

提供http与tinyid-clinet两种方式接入

  • Http方式接入
  1. clone源码:

git clone https://github.com/didi/tinyid

  1. 创建表结构:
CREATE TABLE `tiny_id_info` (
  `id` bigint(20) unsigned 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 '2020-01-01 00:00:00' COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT '2020-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信息表';
CREATE TABLE `tiny_id_token` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id',
  `token` varchar(255) NOT NULL DEFAULT '' COMMENT 'token',
  `biz_type` varchar(63) NOT NULL DEFAULT '' COMMENT '此token可访问的业务类型标识',
  `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
  `create_time` timestamp NOT NULL DEFAULT '2020-01-01 00:00:00' COMMENT '创建时间',
  `update_time` timestamp NOT NULL DEFAULT '2020-01-01 00:00:00' COMMENT '更新时间',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT 'token信息表';
INSERT INTO `tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
VALUES
    (1, 'test', 1, 1, 100000, 1, 0, '2020-03-25 18:35:22', '2020-03-25 18:35:25', 1);
INSERT INTO `tiny_id_info` (`id`, `biz_type`, `begin_id`, `max_id`, `step`, `delta`, `remainder`, `create_time`, `update_time`, `version`)
VALUES
    (2, 'test_odd', 1, 1, 100000, 2, 1, '2020-03-25 18:38:20', '2020-03-25 18:38:22', 3);
  1. 配置数据库:
datasource.tinyid.names=primary
datasource.tinyid.primary.driver-class-name=com.mysql.jdbc.Driver
datasource.tinyid.primary.url=jdbc:mysql://ip:port/databaseName?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
datasource.tinyid.primary.username=root
datasource.tinyid.primary.password=root
  1. 启动tinyid-server后测试
获取分布式自增ID: http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=xxxxx'  //token从token表中获取
返回结果: 3
批量获取分布式自增ID:
http://localhost:9999/tinyid/id/nextIdSimple?bizType=test&token=xxxxxxx&batchSize=10'  //token从token表中获取
返回结果:  4,5,6,7,8,9,10,11,12,13
  • Java客户端方式接入
  1. 引入依赖
<dependency>
    <groupId>com.xiaoju.uemc.tinyid</groupId>
    <artifactId>tinyid-client</artifactId>
    <version>${tinyid.version}</version>
</dependency>
  1. 创建表结构:同上
  2. 配置数据库:同上
  3. 配置文件
tinyid.server =localhost:9999
tinyid.token =xxxxx //token从token表中获取

test 、tinyid.token是在数据库表中预先插入的数据,test是具体业务类型,tinyid.token表示可访问的业务类型

//获取单个分布式自增ID
Long id =  TinyId.nextId("test");
//按需批量分布式自增ID
List<Long> ids = TinyId.nextId("test", 10);

tinyid-client本质上还是依赖tinyid-server,它封装了对tinyid-server的HTTP请求,然后暴露API给用户使用。

总结

以上各种生成分布ID复杂度和优缺点不尽相同,在具体的业务场景如何使用需根据具体的业务需求来使用。

 

参考:

https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

https://tech.meituan.com/2019/03/07/open-source-project-leaf.html

https://github.com/didi/tinyid/wiki/tinyid%E5%8E%9F%E7%90%86%E4%BB%8B%E7%BB%8D

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值