本文由 简悦 SimpRead 转码, 原文地址 huaweicloud.csdn.net

数据库 ID 生成策略在数据库表设计时,主键 ID 是必不可少的字段,如何优雅的设计数据库 ID,适应当前业务场景,需要根据需求选取合适高效的策略,在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识,下面介绍下常用的几种 ID 生成策略。

在数据库表设计时,主键 ID 是必不可少的字段,如何优雅的设计数据库 ID,适应当前业务场景,需要根据需求选取合适高效的策略,在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识,下面介绍下常用的几种 ID 生成策略。

数据库自增长序列或字段,最常见的方式。由数据库维护,数据库表唯一。

优点

  • 简单,代码方便,性能可以接受。
  • 数字 ID 天然排序,对分页或者需要排序的结果很有帮助。

缺点

  • 不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。
  • 在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。
  • 在性能达不到要求的情况下,比较难于扩展。
  • 如果遇见多个系统需要合并或者涉及到数据迁移会相当痛苦。
  • 分表分库的时候会有麻烦。

UUID(Universally Unique Identifier) 的标准型式包含 32 个 16 进制数字,以连字号分为五段,形式为 8-4-4-4-12 的 36 个字符。

它通过 MAC 地址、时间戳、命名空间、随机数、伪随机数来保证生成 ID 的唯一性。

使用 JAVA 代码生成

String uuid = UUID.randomUUID().toString();
        System.err.println(uuid);  // a299d6ef-9f82-475e-b3d5-f16df808fea1
        System.err.println(uuid.length());

使用 Mysql 函数生成:

UUID()

UUID 主要有五个算法:

  1. uuid1()
    基于时间戳。由 MAC 地址、当前时间戳、随机数生成。可以保证全球范围内的唯一性,但 MAC 的使用同时带来安全性问题,局域网中可以使用 IP 来代替 MAC。
  2. uuid2()
    基于分布式计算环境 DCE(Python 中没有这个函数)。算法与 uuid1 相同,不同的是把时间戳的前 4 位置换为 POSIX 的 UID。实际中很少用到该方法。
  3. uuid3()
    基于名字的 MD5 散列值。通过计算名字和命名空间的 MD5 散列值得到,保证了同一命名空间中不同名字的唯一性,和不同命名空间的唯一性,但同一命名空间的同一名字生成相同的 uuid。
  4. uuid4()
    基于随机数。由伪随机数得到,有一定的重复概率,该概率可以计算出来。
  5. uuid5()
    基于名字的 SHA-1 散列值。算法与 uuid3 相同,不同的是使用 Secure Hash Algorithm 1 算法。

优点

  • 简单,代码方便。

  • 全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。

  • 本地生成,没有网络消耗。

       缺点

  • 不易于存储:UUID 太长,16 字节 128 位,通常以 36 长度的字符串表示,很多场景不适用。

  • 信息不安全:基于 MAC 地址生成 UUID 的算法可能会造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

  • MySQL 官方有明确的建议主键要尽量越短越好 [4],36 个字符长度的 UUID 不符合要求。

  • 对 MySQL 索引不利:如果作为数据库主键,在 InnoDB 引擎下,UUID 的无序性可能会引起数据位置频繁变动,严重影响性能。

snowflake 是 Twitter 开源的分布式 ID 生成算法,结果是一个 long 型的 ID。其核心思想是:使用 41bit 作为毫秒数,10bit 作为机器的 ID(5 个 bit 是数据中心,5 个 bit 的机器 ID),12bit 作为毫秒内的流水号(意味着每个节点在每毫秒可以产生 4096 个 ID),最后还有一个符号位,永远是 0。snowflake 算法可以根据自身项目的需要进行一定的修改。比如估算未来的数据中心个数,每个数据中心的机器数以及统一毫秒可以能的并发数来调整在算法中所需要的 bit 数。

Snowflake snowflake = IdUtil.getSnowflake(1, 1);
        long id = snowflake.nextId();
        System.err.println(id);

优点

  • 毫秒数在高位,自增序列在低位,整个 ID 都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成 ID 的性能也是非常高的。
  • 可以根据自身业务特性分配 bit 位,非常灵活。
    缺点
  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

地址
UidGenerator 是 Java 实现的,基于 Snowflake 的唯一 ID 生成器。它用作组件,并允许用户覆盖 workId 位和初始化策略。因此,它更适合于虚拟化环境,例如 docker。除此之外,它通过消耗将来的时间克服了 Snowflake 算法的并发限制。通过使用 RingBuffer 缓存 UID 来并行化 UID 产生和使用;通过填充消除了来自 RingBuffer 的 CacheLine 伪共享。最后,每个实例可以提供超过 600 万个 QPS。

地址
Tinyid 是 ID 生成器服务。它提供了一个 REST API 和一个用于获取 ID 的 Java 客户端。使用 Java 客户端时,每个单个实例超过 1000 万个 QPS。

地址
Leaf 指的是行业中一些常见的 ID 生成方案,包括 redis,UUID,snowflare 等。以上每种方法都有其自身的问题,因此我们决定实施一套分布式 ID 生成服务以满足要求。目前,Leaf 负责美团点评公司的内部财务,餐饮,外卖,酒店旅行,猫眼电影和许多其他业务。在 4C8G VM 的基础上,通过公司的 RPC 方法,QPS 压力测试结果接近 5w / s,TP999 为 1ms。

MybatisPlus 提供了多种 ID 生成策略。

生成 ID 类型枚举类:

public enum IdType {
    /**
     * 数据库ID自增
     * <p>该类型请确保数据库设置了 ID自增 否则无效</p>
     */
    AUTO(0),
    /**
     * 该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
     */
    NONE(1),
    /**
     * 用户输入ID
     * <p>该类型可以通过自己注册自动填充插件进行填充</p>
     */
    INPUT(2),
 
    /* 以下3种类型、只有当插入对象ID 为空,才自动填充。 */
    /**
     * 分配ID (主键类型为number或string),
     * 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(雪花算法)
     *
     * @since 3.3.0
     */
    ASSIGN_ID(3),
    /**
     * 分配UUID (主键类型为 string)
     * 默认实现类 {@link com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator}(UUID.replace("-",""))
     */
    ASSIGN_UUID(4),
    /**
     * @deprecated 3.3.0 please use {@link 
     */
    @Deprecated
    ID_WORKER(3),
    /**
     * @deprecated 3.3.0 please use {@link 
     */
    @Deprecated
    ID_WORKER_STR(3),
    /**
     * @deprecated 3.3.0 please use {@link 
     */
    @Deprecated
    UUID(4);
 
    private final int key;
 
    IdType(int key) {
        this.key = key;
    }
}

数据库 ID 自增,该类型请确保数据库设置了 ID 自增, 否则无效。

  1. 设置数据库表 ID 自增

  2. 设置实体类 IdType 为 AUTO

@TableId(value = "id", type = IdType.AUTO)
    private Integer id;
  1. 插入数据测试
OrderTbl orderTbl = new OrderTbl().setMoney(100).setUserId("123").setCommodityCode("PHONE");
        orderTblMapper.insert(orderTbl);

该类型为未设置主键类型 (注解里等于跟随全局, 全局里约等于 INPUT)。

  1. 取消数据库表 ID 自增
  2. 设置实体类 IdType 为 NONE
@TableId(value = "id", type = IdType.NONE)
    private Integer id;
  1. 插入数据测试
public void insertTest() {
        OrderTbl orderTbl = new OrderTbl().setId(11111).setMoney(100).setUserId("123").setCommodityCode("PHONE");
        orderTblMapper.insert(orderTbl);
    }

用户输入 ID,该类型可以通过自己注册自动填充插件进行填充(经测试 MetaObjectHandler 并不能实现主键 ID 自动填充,因为自动填充时是获取 TableInfo 字段信息循环字段并填充,但是 TableInfo 字段中不包含主键,所以无法填充)。应该可以使用 mybatis 插件实现(未测试)。

方式一

  1. 取消数据库表 ID 自增
  2. 设置实体类 IdType 为 INPUT
@TableId(value = "id", type = IdType.INPUT)
    private Integer id;
  1. 插入数据测试同 NONE 步骤

分配 ID (主键类型为 number 或 string), 默认实现类 DefaultIdentifierGenerator(雪花算法)。

方式一使用默认生成器

  1. 取消数据库表 ID 自增
  2. 设置实体类 IdType 为 ASSIGN_ID,字段类型为 Long
@TableId(value = "id", type = IdType.ASSIGN_ID)
    private Long id;
  1. 插入数据测试同 NONE 步骤

    方式二使用自定义 ID 生成器

  2. 取消数据库表 ID 自增

  3. 设置实体类 IdType 为 ASSIGN_ID,字段类型为 Long

@TableId(value = "id", type = IdType.ASSIGN_ID)
    private Long id;
  1. 添加自定义 ID 生成器
@Component
public class CustomIdGenerator implements IdentifierGenerator {
    @Override
    public Long nextId(Object entity) {
        
        Snowflake snowflake = IdUtil.getSnowflake(1, 1);
        return snowflake.nextId();
    }
}
  1. 插入数据测试同 NONE 步骤