Mysql主键设计

Posted by Wh0ami-hy on July 22, 2024

1. 主键的选择

可以使用 BIGINT 的自增类型作为主键,同时由于整型的自增性,数据库插入也是顺序的,性能较好

但是要注意,使用 BIGINT 的自增类型作为主键的设计仅仅适合非核心业务表,比如告警表、日志表等。真正的核心业务表,一定不要用自增键做主键,主要有 6 个原因:

  • 自增存在回溯问题;
  • 自增值在服务器端产生,存在并发性能问题;
  • 自增值做主键,只能在当前实例中保证唯一,不能保证全局唯一
  • 公开数据值,容易引发安全问题,例如知道地址http://www.example.com/User/10/,很容猜出 User 有 11、12 依次类推的值,容易引发数据泄露;
  • MGR(MySQL Group Replication) 可能引起的性能问题;
  • 分布式架构设计问题。

更推荐 UUID 做主键或业务自定义生成主键

2. UUID主键设计

UUID(Universally Unique Identifier)代表全局唯一标识 ID。

MySQL 数据库遵循 DRFC 4122 命名空间版本定义的 Version 1规范,可以通过函数 UUID自动生成36字节字符

SELECT UUID();

根据 Version 1的规范,MySQL中的 UUID 由以下几个部分组成:

UUID = 时间低(4字节)- 时间中高+版本(4字节)- 时钟序列 - MAC地址

需要特别注意的是,在存储时间时,UUID 是根据时间位逆序存储, 也就是低时间低位存放在最前面,高时间位在最后,即 UUID 的前 4 个字节会随着时间的变化而不断“随机”变化,并非单调递增。而非随机值在插入时会产生离散 IO,从而产生性能瓶颈。这也是 UUID 对比自增值最大的弊端。

为了解决这个问题,MySQL 8.0 推出了函数 UUID_TO_BIN,它可以把 UUID 字符串:

  • 通过参数将时间高位放在最前,解决了 UUID 插入时乱序问题;
  • 去掉了无用的字符串”-“,精简存储空间;
  • 将字符串其转换为二进制值存储,空间最终从之前的 36 个字节缩短为了 16 字节。

除此之外,MySQL 8.0 也提供了函数 BIN_TO_UUID,支持将二进制值反转为 UUID 字符串。

因此,创建表时可以将其主键修改为 BINARY(16),用于存储排序后的 16 字节的 UUID 值。其表结构修如下:

CREATE TABLE User (

    id  BINARY(16) NOT NULL,

    name VARCHAR(255) NOT NULL,

    sex CHAR(1) NOT NULL,

    password VARCHAR(1024) NOT NULL,

    money INT NOT NULL DEFAULT 0,

    register_date DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),

    last_modify_date DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),

    uuid CHAR(36) AS (BIN_TO_UUID(id)),

    CONSTRAINT chk_sex CHECK (sex = 'M' OR sex = 'F'),

    PRIMARY KEY(id)

);

现在,你可以在客户端通过以下 SQL 命令插入数据,如:

INSERT INTO User VALUES (UUID_TO_BIN(UUID(),TRUE),......);

MySQL 8.0 提供的排序 UUID 性能最好,甚至比自增ID还要好

MySQL 8.0版本之前没有函数 UUID_TO_BIN/BIN_TO_UUID,但是可以通过用户定义函数(UDF)的方式解决,如创建下面的函数:

DELIMITER //

CREATE FUNCTION my_uuid() RETURNS CHAR(32)
NO SQL
BEGIN
    DECLARE original_uuid CHAR(36);
    DECLARE rearranged_uuid CHAR(32);

    -- 生成原始 UUID
    SET original_uuid = UUID();

    -- 将时间高位放在最前,并去掉无用的字符串 -
    SET rearranged_uuid = CONCAT(
        SUBSTRING(original_uuid, 15, 4), 
        SUBSTRING(original_uuid, 10, 4), 
        SUBSTRING(original_uuid, 1, 8), 
        SUBSTRING(original_uuid, 20, 4), 
        SUBSTRING(original_uuid, 25, 12)
    );

    RETURN rearranged_uuid;
END //

DELIMITER ;

3. 业务自定义生成主键

UUID 虽好,但是在分布式数据库场景下,主键还需要加入一些额外的信息,这样才能保证后续二级索引的查询效率。现在你只需要牢记:分布式数据库架构,仅用 UUID 做主键依然是不够的。 所以,对于分布式架构的核心业务表,我推荐类似如下的设计,比如:

PK = 时间字段 + 随机码(可选) + 业务信息1 + 业务信息2 ......

一个最基本的分布式 ID 需要满足下面这些要求:

  • 全局唯一:ID 的全局唯一性肯定是首先要满足的!

  • 高性能:分布式 ID 的生成速度要快,对本地资源消耗要小。

  • 高可用:生成分布式 ID 的服务要保证可用性无限接近于 100%。

  • 方便易用:拿来即用,使用方便,快速接入!

除了这些之外,一个比较好的分布式 ID 还应保证:

  • 安全:ID 中不包含敏感信息。

  • 有序递增:如果要把 ID 存放在数据库的话,ID 的有序性可以提升数据库写入速度。并且,很多时候 ,我们还很有可能会直接通过 ID 来进行排序。

  • 有具体的业务含义:生成的 ID 如果能有具体的业务含义,可以让定位问题以及开发更透明化(通过 ID 就能确定是哪个业务)。

  • 独立部署:也就是分布式系统单独有一个发号器服务,专门用来生成分布式 ID。这样就生成 ID 的服务可以和业务相关的服务解耦。不过,这样同样带来了网络调用消耗增加的问题。总的来说,如果需要用到分布式 ID 的场景比较多的话,独立部署的发号器服务还是很有必要的。

4. IdGenerator

https://github.com/yitter/IdGenerator

参考


本站总访问量