当前位置: 首页 > 工具软件 > UidGenerator > 使用案例 >

分布式唯一性id生成方案 ------- 百度的UidGenerator

伯丁雷
2023-12-01

在分库分表中必定会面临着一个问题, 就是如何快速高效的生成唯一性ID。 而网络上也有一些通用的解决方案:

使用uuid作为主键

优点:

  • 不用依赖任何第三方, 每台应用都能独立生成;
  • 生成的id重复率极低, 并且无法被人猜测到

缺点:

  • 生成的id是字符串,不是数字,难以比较大小
  • 生成的不是有序增长的, 在很多的查询中不方便
  • 生成的长度过长

使用数据库自增序列, 只是步进长度不同

这种方式也只用依赖数据库就可以了, 不用在引入过多的依赖。
优点:

  • 不用引入过多依赖
  • 绝对不会重复, 依赖各自的数据库,效率还可以
  • id 保持自动增长

缺点:

  • 扩展不方便, 无法提前预估业务增长量,设置了不合理的步进长度就会难以扩展

这种方案的缺点还是很大的, 都用到分库分表了, 说明业务的增长很快, 后期很有可能会继续扩展数据库,这时候就很不方便了

使用Snowflake算法

Snowflake是和uuid一样的不用依赖任何第三方的id生成算法。 并且这个算法生成的id还是自动增长的数字类型的。

由于这个算法的生成依赖时间,如果不同的机器时间出现跳跃还是会可能生成一样的id的。 但是这个算法也有应对的办法,就是不同的机器使用不同的wordId。 这样两台机器无论时间怎样跳跃,生成的id都不会重复。

但是分布式应用中,各个应用很可能是随时变化的, 比如业务量激增的时候多加几个应用, 业务量减少的时候下线几个应用是常有的事情, 如何保证应用变化的时候它的wordId都不一样呢?

常用的解决方案有3中:

  1. 借助zookeeper的有序id来实现。 zookeeper 天生适合用于分布式的场景中
  2. 接入redis来实现, redis内部的单线程机制能保证分布式中数据的一致性
  3. 借助数据库来实现, 主流的数据库都有事务机制,可以保证数据的一致性, 不会出现重复的id

而百度的UidGenerator就是Snowflake结合数据库实现的id生成方案, 这个方案的实现方式是:
1. 新建一张表, 主键使用自增的’
2. 每次有应用启动的时候,在这个表中新增一条记录, 并获取返回的自增主键id
3. 使用返回的自增主键id作为Snowflake的wordId
4. 构建好了Snowflake后使用批量获取的方式获取一批id保存在内部维护的环结构中。

从实现方式发现了有几个问题。

  • 原版的Snowflake中WorkerBits只有10个bit位, 如果使用原版的方式最多只能支持应用重启1024次,这有点不够用。 所以百度的扩展了这个位数, 使用22位bit来表示, 可以重启4194304次。
  • 原版的时间位TimeBits占用41位。 可以使用69年, 但是现实中根本没有必要, 所以百度的减少了时间位的bit数, 只是使用28位来标识时间,只能使用8.7年, 但是可以定义时间的起点(ps: 我觉得这个时间有点少, 反而重启次数有点过多)
  • 原版使用12位作为并发的序列SeqBits, 支持每个节点每个毫秒生成4096个ID。 而百度增加了一位, 使用13位来, 支持每个节点每个毫秒生成8192个ID。

上面只是百度的默认使用的bit位的分配比例, 但是UidGenerator支持自己定义这三个的信息的占位的大小, 这就很有自由度了, 比如你认为你的应用不需要这么多的重启次数,你可以减少WorkerBits的设置, 如果你任务你的并发量超级高,8192个id已经不能满足你的业务需要, 你可以增加SeqBits位数。 如果你任务你的应用只要有超过8.7年的生命时间, 可以增加TimeBits的位数。 具体可以根据需要来分配他们。

Springboot 接入UidGenerator方式

限制: 内部依赖了mybatis和mybatis-spring。 如果你是使用的jpa的框架, 没有使用mybatis那么你无法直接使用下面方式, 建议你下载下来源码后改造下使用, 源码地址:
https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

导入maven包:

 <dependency>
    <groupId>com.xfvape.uid</groupId>
      <artifactId>uid-generator</artifactId>
      <version>0.0.4-RELEASE</version>
      <exclusions>
          <exclusion>
              <artifactId>mybatis</artifactId>
              <groupId>org.mybatis</groupId>
          </exclusion>
          <exclusion>
              <artifactId>mybatis-spring</artifactId>
              <groupId>org.mybatis</groupId>
          </exclusion>
      </exclusions>
  </dependency>

它的内部依赖有mybatis和mybatis-spring , 一般这两个依赖项目中都会有自己导入的, 防止依赖冲突需要排除掉。

新建WORKER_NODE表

可以运行下面的建表语句:

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;

引入mybatis的mapper文件

需要在配置文件中增加配置:

mybatis.mapper-locations= classpath:mapper/*.xml

同时还要在mapper目录下增加如下内容的xml文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xfvape.uid.worker.dao.WorkerNodeDAO">
    <resultMap id="workerNodeRes"
               type="com.xfvape.uid.worker.entity.WorkerNodeEntity">
        <id column="ID" jdbcType="BIGINT" property="id" />
        <result column="HOST_NAME" jdbcType="VARCHAR" property="hostName" />
        <result column="PORT" jdbcType="VARCHAR" property="port" />
        <result column="TYPE" jdbcType="INTEGER" property="type" />
        <result column="LAUNCH_DATE" jdbcType="DATE" property="launchDate" />
        <result column="MODIFIED" jdbcType="TIMESTAMP" property="modified" />
        <result column="CREATED" jdbcType="TIMESTAMP" property="created" />
    </resultMap>

    <insert id="addWorkerNode" useGeneratedKeys="true" keyProperty="id"
            parameterType="com.xfvape.uid.worker.entity.WorkerNodeEntity">
        INSERT INTO WORKER_NODE
        (HOST_NAME,
         PORT,
         TYPE,
         LAUNCH_DATE,
         MODIFIED,
         CREATED)
        VALUES (
                   #{hostName},
                   #{port},
                   #{type},
                   #{launchDate},
                   NOW(),
                   NOW())
    </insert>

    <select id="getWorkerNodeByHostPort" resultMap="workerNodeRes">
        SELECT
            ID,
            HOST_NAME,
            PORT,
            TYPE,
            LAUNCH_DATE,
            MODIFIED,
            CREATED
        FROM
            WORKER_NODE
        WHERE
            HOST_NAME = #{host} AND PORT = #{port}
    </select>
</mapper>

扫描加载相关类

在启动类Bootstrap中加上如下注解:
@MapperScan(basePackages = {"com.xfvape.uid.worker.dao"})

配置如下的两个bean到容器中:

    @Bean
    public CachedUidGenerator cachedUidGenerator(WorkerIdAssigner disposableWorkerIdAssigner) {
        CachedUidGenerator cachedUidGenerator = new CachedUidGenerator();
        cachedUidGenerator.setWorkerIdAssigner(disposableWorkerIdAssigner);
        cachedUidGenerator.setTimeBits(29);
        cachedUidGenerator.setWorkerBits(21);
        cachedUidGenerator.setSeqBits(13);
        //从2020-08-06起, 可以使用8.7年
        cachedUidGenerator.setEpochStr("2020-08-06");
        //扩容比如设置成2,  当RingBuffer不够用默认是扩大三倍,
        cachedUidGenerator.setBoostPower(2);
        //当环上的缓存的uid少于25的时候, 进行新生成填充
        cachedUidGenerator.setPaddingFactor(25);
        //开启一个线程, 定时检查填充uid。 不采用这个方式。
        //cachedUidGenerator.setScheduleInterval(60);

        //两个拒绝策略使用默认的足够
        //拒绝策略: 当环已满, 无法继续填充时, 默认无需指定, 将丢弃Put操作, 仅日志记录.
        //cachedUidGenerator.setRejectedPutBufferHandler();
        //拒绝策略: 当环已空, 无法继续获取时,默认无需指定, 将记录日志, 并抛出UidGenerateException异常.
        //cachedUidGenerator.setRejectedTakeBufferHandler();

        return cachedUidGenerator;
    }


    @Bean
    public DisposableWorkerIdAssigner disposableWorkerIdAssigner() {
        DisposableWorkerIdAssigner disposableWorkerIdAssigner = new DisposableWorkerIdAssigner();
        return disposableWorkerIdAssigner;
    }

完成上面的操作后, 配置就完成了,剩下的就是使用的问题。

使用

使用只要注入CachedUidGenerator对象, 使用它的getUID方就可以直接获取id了。

    @Resource
    CachedUidGenerator cachedUidGenerator;
    @Override
    public Long nextId() {
        return cachedUidGenerator.getUID();
    }

总结:

UidGenerator对Snowflake算法进行了很多的优化, 并且灵活性很大,你完全可以根据自己的需要另外的选择算法的bit分配, 并且还可以允许你自定义拒绝策略. 使用也很简单。 还是很推荐使用的。

 类似资料: