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