我们写SQL时候,会遇到一个头疼问题:怎么给数据配一个唯一ID?不管是存订单、用户还是产品,没有唯一ID后续查数据、改数据容易出乱。今天我们就一起来探究SQL常用的11种唯一ID生成方法。
一、自增ID(AUTO_INCREMENT/IDENTITY):最简单的“自动数数”
自增ID就像我们平时记东西按1、2、3排序一样,数据库会自动给每条新数据分配一个比上一条大1的数字,不用我们手动算。在不同数据库中名字不一样,用法也有差别。
1、生成逻辑:数据库帮我们“数序号”
如:我们建了个订单表,第一条订单ID是1,第二条自动变成2,第三条3……不管我们插数据时候有没有指定这个ID,数据库都能自己搞定。注意:不同数据库的关键词不一样:MySQL用AUTO_INCREMENT,SQL Server用IDENTITY,Oracle和PostgreSQL没有直接的自增关键词,我们用“序列”来模拟。
2、SQL实现:分数据库写,照着抄就行
我们先看MySQL的用法,建表的时候给order_id加上AUTO_INCREMENT,再设成主键,这样它就自动自增了:
-- MySQL建表,order_id自动从1开始涨CREATE TABLE Orders ( order_id INT AUTO_INCREMENT PRIMARY KEY, -- 关键是AUTO_INCREMENT customer_id CHAr(10) -- 如:存客户编号C001、C002);-- 插数据的时候不用管order_id,数据库自己填INSERT INTO Orders (customer_id) VALUES ('C001');-- 想知道刚插的那条数据ID是多少?用LAST_INSERT_ID()SELECt LAST_INSERT_ID(); -- 这时候会输出1,下次插就是2了再看SQL Server,它用的是IDENTITY(1,1),括号里第一个数是“起始值”,第二个是“每次涨多少”,这里就是从1开始,每次加1:
-- SQL Server建表CREATE TABLE Orders ( order_id INT IDENTITY(1,1) PRIMARY KEY, -- IDENTITY(起始值, 步长) customer_id CHAr(10));-- 插数据同样不用写order_idINSERT INTO Orders (customer_id) VALUES ('C002');-- 查刚生成的ID,用SCOPE_IDENTITY(),比@@IDENTITY更安全(不会受其他表影响)SELECT SCOPE_IDENTITY(); -- 输出23、适用场景:单体应用首选,简单又快
自增ID最适合那种“单机跑的小系统”,如:我们公司内部的管理系统、小型电商后台。因为它有两个大优点:
- 简单:不用写复杂逻辑,建表时候加个关键词就行,插数据也不用管ID。
- 高效:数据库内部处理自增很快,不会拖慢插数据速度,而且整数ID存起来占空间小,查的时候也快。
缺点也很明显,就是“没法跨数据库”。如:我们做个分布式系统,数据存在两个MySQL实例里,第一个实例的订单ID到了1000,第二个实例可能也从1开始,这时候就会出现两个相同的ID,数据就乱了。所以分布式系统绝对不能用自增ID。
还有个问题:如果删了中间的一条数据,如:删了ID=5的订单,下次插数据会是6,而不是补5,这会导致ID不连续。不过大部分场景下,ID连不连续不重要,只要唯一就行,所以这个问题影响不大。另外,事务回滚也会导致自增ID不连续。
二、UUID/GUID:“全局(球)唯一”随机串,分布式系统救星
如果我们系统是分布式,如:数据存在多个数据库实例里,自增ID肯定不行,这时候就该用UUID或者GUID了。它们是一个东西,UUID是通用叫法,GUID是SQL Server里的特定叫法,都是128位的随机字符串,全世界范围内重复的概率几乎为零。
1、生成逻辑:靠随机或时间戳保证唯一
UUID有好几个版本,我们常用v4和v1:
- v4:纯随机生成,完全靠概率保证唯一,不用任何硬件信息,兼容性最好。
- v1:基于当前时间戳+电脑的MAC地址生成,理论上也唯一,但会暴露MAC地址(有安全风险),所以一般不用v1,优先用v4。
不管是哪个版本,生成字符串都是类似“550e8400-e29b-41d4-a716-446655440000”这样的格式,一共36个字符(包括中间的横杠)。
2、SQL实现:不同数据库生成方式略有差别
我们先看SQL Server,它用NEWID()函数生成GUID,建表的时候可以设成默认值,插数据不用管:
-- SQL Server建表,user_id默认生成GUIDCREATE TABLE Users ( user_id UNIQUEIDENTIFIER DEFAULT NEWID() PRIMARY KEY, -- UNIQUEIDENTIFIER是GUID的类型 username VARCHAr(50) -- 存用户名,如:Alice、Bob);-- 插数据,不用写user_idINSERT INTO Users (username) VALUES ('Alice');-- 想单独生成一个GUID看看?直接调用NEWID()SELECT NEWID(); -- 会输出类似"5D8F7A23-981E-4F3B-8A7C-1234567890AB"的字符串再看MySQL,它用UUID()函数生成UUID,MySQL里没有专门的UUID类型,要用CHAr(36)来存(因为UUID字符串正好36个字符):
-- MySQL建表,user_id默认生成UUIDCREATE TABLE Users ( user_id CHAr(36) DEFAULT UUID() PRIMARY KEY, -- CHAr(36)存UUID刚好 username VARCHAr(50));-- 插数据INSERT INTO Users (username) VALUES ('Bob');-- 单独生成UUIDSELECT UUID(); -- 输出类似"6b9d4f3a-7c8d-11e9-9f0b-0242ac120003"注意小坑:MySQL的UUID()函数生成的UUID是小写的,而SQL Server的NEWID()生成的是大写的,但这不影响唯一性,我们要是想统一大小写,可以用UPPER()(转大写)或LOWER()(转小写)函数处理一下,如:SELECT UPPER(UUID())。
3、适用场景:分布式系统必选,但要接受它的“缺点”
UUID最大的优点就是“全局唯一”,不管我们有多少个数据库实例,生成的UUID都不会重复,所以分布式系统(如:大型电商、社交APP)几乎都用它。还有个优点:生成ID的时候不用访问数据库(有些编程语言在代码里就能生成UUID),能减少数据库压力。
它的缺点也很突出:
- 占空间大:UUID是36个字符的字符串,而自增ID是4个字节的整数(如:INT类型),存UUID比存自增ID多占不少空间,数据量大的时候会影响存储和查询速度。
- 无序:UUID是随机生成的,没有顺序,而数据库的主键索引(如:InnoDB的聚簇索引)是按主键顺序排列的,无序的UUID会导致索引频繁分裂,影响性能。
- 不好读:我们看“550e8400-e29b-41d4-a716-446655440000”这串字符,根本记不住,而自增ID“10086”好记,要是排查问题的时候看日志,UUID会比自增ID麻烦一点。
三、存储过程生成唯一ID:“强制唯一”,关键业务才用
有时候,我们对ID的唯一性要求特别高,如:金融场景的交易ID、支付单号,哪怕只有万分之一的重复概率也不能接受。这时候光靠UUID或者自增可能不够放心,就可以用“存储过程”来生成ID。简单说就是写一段SQL逻辑,在数据库里跑,确保生成的ID绝对唯一。
1、生成逻辑:用事务+判断,重复了就重新生成
存储过程的思路是:先尝试生成一个ID(如:用UUID),然后查一下这个ID在表里有没有存在;如果不存在,就把这个ID存到表里,返回给用户;如果存在了(虽然概率极低),就重新生成一次,直到拿到唯一ID。整个过程会用“事务”包裹,确保不会出现两个存储过程同时生成同一个ID的情况。
2、SQL实现:SQL Server存储过程,逻辑很直观
我们以SQL Server为例,先建一个存唯一ID的表,再写存储过程:
-- 先建个表,专门存生成的唯一ID,确保不会重复CREATE TABLE UniqueIDs ( ID UNIQUEIDENTIFIER PRIMARY KEY -- 用GUID当主键,本身就唯一);-- 写存储过程,名字叫GenerateUniqueIDCREATE PROCEDURE GenerateUniqueIDASBEGIN -- 第一步:关闭事务的自动提交,避免中间步骤出问题 SET NOCOUNT ON; BEGIN TRANSACTION; -- 开启事务 DECLARE @NewID UNIQUEIDENTIFIER; -- 定义变量存新生成的ID SET @NewID = NEWID(); -- 生成一个GUID -- 第二步:判断这个ID在表里有没有 IF NOT EXISTS (SELECT 1 FROM UniqueIDs WHERe ID = @NewID) BEGIN -- 不存在就插入表 INSERT INTO UniqueIDs (ID) VALUES (@NewID); COMMIT TRANSACTION; -- 提交事务 SELECt @NewID AS GeneratedID; -- 返回生成的ID END ELSE BEGIN -- 存在就重新调用自己,再生成一次 ROLLBACK TRANSACTION; -- 回滚事务,避免脏数据 EXEC GenerateUniqueID; -- 递归调用 ENDEND;-- 用的时候调用这个存储过程就行EXEC GenerateUniqueID; -- 会输出一个绝对唯一的GUIDMySQL存储过程逻辑差不多,就是函数名和语法有点区别,如:生成UUID用UUID(),判断存在用NOT EXISTS,等等。
3、适用场景:关键业务场景,对唯一性要求“零容忍”
这种方法的最大优点就是“强制唯一”,它会先查再插,还加了事务,哪怕极端情况下两个请求同时生成了同一个ID,事务也会保证只有一个能插入成功,另一个会重试,所以几乎不可能出现重复ID。适合金融交易、支付、订单确认这些“一旦ID重复就会出大问题”的场景。
它的缺点也很致命,一般场景不能用:
- 性能差:每次生成ID都要执行“生成→查询→插入”三步,还可能递归重试,比自增ID和UUID慢多了,高并发场景下会严重拖慢系统。
- 有死循环风险:虽然UUID重复的概率极低,但理论上还是有可能一直重复(如:系统出bug,每次都生成同一个ID),这时候存储过程会一直递归调用自己,最后形成死循环。所以写的时候一定要加“超时控制”,如:递归10次还失败就抛出错误,别让它一直跑。
- 维护麻烦:存储过程是在数据库里写的,后续要改逻辑(如:换ID生成方式),得去改数据库里的存储过程,不像代码里的逻辑改起来方便。
四、唯一约束(UNIQUE):不是“生成ID”,是“保证字段不重复”
前面的都是“主动生成唯一ID”的方法,“唯一约束”不一样,它不是用来生成ID的,而是用来“限制某个字段的值不能重复”。如:我们存用户信息,要求每个用户的邮箱、手机号必须唯一,不能有两个人用同一个邮箱,这时候我们就需要给邮箱、手机号加“唯一约束”。
1、生成逻辑:数据库帮我们“把关”,重复了就报错
唯一约束逻辑很简单:我们给某个字段(如:email)加了UNIQUE约束之后,每次往表里插数据或者改数据,数据库都会先查一下这个字段的值是否已经存在;如果存在,就直接报错,不让我们插/改;如果不存在,才允许操作。而且数据库会自动给加了唯一约束的字段建“唯一索引”,查的时候会更快。
2、SQL实现:建表时加约束,或后期改表加约束
先看“建表的时候直接加唯一约束”:用UNIQUE关键词。
-- MySQL建表,给email加唯一约束CREATE TABLE Employees ( id INT PRIMARY KEY AUTO_INCREMENT, -- 主键用自增ID email VARCHAr(255) UNIQUE, -- 给email加唯一约束,不能重复 phone CHAr(11) -- 如:存手机号);-- SQL Server建表,同样给email加唯一约束CREATE TABLE Employees ( id INT PRIMARY KEY IDENTITY(1,1), email VARCHAr(255) UNIQUE, -- 唯一约束 phone CHAr(11));有时候表已经建好了,想给existing字段加唯一约束,这时候要用ALTER TABLE:
-- MySQL给existing的email字段加唯一约束,还能起个名字(方便后续管理)ALTER TABLE EmployeesADD ConSTRAINT UQE_Employees_Email UNIQUE (email); -- UQE是Unique Constraint的缩写,起名字是好习惯-- SQL Server同样的操作ALTER TABLE EmployeesADD ConSTRAINT UQE_Employees_Email UNIQUE (email);有个细节:如果我们想给“多个字段的组合”加唯一约束,如:“用户名+部门”不能重复(同一个部门里不能有两个同名的员工),我们可以这么写:
-- MySQL:用户名+部门组合唯一ALTER TABLE EmployeesADD ConSTRAINT UQE_Employees_Name_Dept UNIQUE (username, dept_id);3、适用场景:保证业务字段唯一,如:邮箱、手机号
唯一约束的适用场景太广了,只要某个字段的值“不能重复”,就需要加。如:
- 用户表:email、phone、username必须唯一。
- 商品表:商品编号(sku)必须唯一。
- 订单表:订单编号必须唯一(不过订单编号一般是生成的,这里加唯一约束是双重保险)。
优点很明确:
- 简单:加约束的语法很简单,不用写复杂逻辑。
- 安全:数据库层面把关,不管是代码里插数据,还是手动改数据,只要重复就报错,不会出现脏数据。
- 高效:数据库会自动给唯一约束字段建唯一索引,查询这个字段的时候(如:用email查用户)会比没索引快很多。
有个坑必须注意:空值(NULL)的问题。SQL标准里规定“NULL不等于任何值,包括它自己”,所以如果给允许NULL的字段加唯一约束,可能会出现多个NULL值。如:SQL Server里,如果我们给email加了唯一约束,但email允许为NULL,那么可以插多条email为NULL的记录;而MySQL里,有些版本会把NULL当作“相同值”,只允许一条NULL记录。所以用唯一约束的时候,一定要明确字段能不能为NULL:如果字段是“必须填”的(如:email),就给字段加NOT NULL约束,避免出现多个NULL;如果字段允许为NULL,就要考虑要不要接受“多个NULL共存”的情况,或者用“默认值”代替NULL(如:用空字符串''代替NULL)。
还要注意:唯一约束和主键的区别。主键是“表的唯一标识”,一个表只能有一个主键,而且主键字段不能为NULL;而唯一约束可以加多个(如:同时给email和phone加),字段可以允许NULL(看数据库)。所以主键是“标识行唯一”,唯一约束是“标识字段唯一”,我们别搞混了。对需要唯一约束的业务关键字段(如:email, phone),我们可同时设置NOT NULL约束,以彻底避免因NULL值导致的歧义和潜在的数据不一致问题。
五、组合ID(业务逻辑生成):“看得懂的ID”,业务追踪方便
自增ID太简单(只有数字),UUID太乱(全是随机串),“组合ID”不一样,它把“业务信息+时间+序列号”拼起来做ID,如:“ORDER_20240520_0001”,我们一看就知道这是2024年5月20日的第1个订单。
1、生成逻辑:自己拼字符串,按业务规则来
组合ID的生成逻辑完全由业务决定,简单讲,就是“拼接”,一般会包含这几部分:
- 业务前缀:如:订单用“ORDER”,发票用“INVOICE”,用户用“USER”,一眼就能看出这是哪个业务的ID。
- 时间戳:通常是年月日,如:“20240520”,表示2024年5月20日,方便按时间筛选数据。
- 序列号:每天从1开始计数,如:“0001”、“0002”,确保同一天内的ID不重复。
我们看个例子:订单表的组合ID可能是“ORDER_20240520_0001”,拆开来就是:业务前缀(ORDER)+ 日期(20240520)+ 当日序列号(0001)。
2、SQL实现:分数据库写,“拼接字符串”
我们先看SQL Server写法,步骤是:定义前缀→获取当前日期→查当天最大序列号→拼接成新ID→插入表:
-- SQL Server生成订单组合IDDECLARE @Prefix VARCHAr(10) = 'ORDER'; -- 业务前缀DECLARE @DatePart VARCHAr(8) = FORMAT(GETDATE(), 'yyyyMMdd'); -- 日期部分,如:20240520-- 查当天最大的序列号:从order_id里截取最后4位,转成数字,取最大的+1,没有就从1开始DECLARE @Sequence INT = ISNULL((SELECT MAX(CAST(RIGHt(order_id, 4) AS INT)) FROM Orders WHERe order_id LIKE @Prefix + '_' + @DatePart + '_%'), 0) + 1;-- 拼接ID:前缀+日期+序列号(补0凑4位,如:1变成0001)DECLARE @NewID VARCHAr(20) = @Prefix + '_' + @DatePart + '_' + RIGHt('0000' + CAST(@Sequence AS VARCHAR), 4);-- 插入订单表INSERT INTO Orders (order_id, customer_id) VALUES (@NewID, 'C003');再看MySQL写法,逻辑一样,函数名有点区别,如:取日期用DATE_FORMAT,补0用LPAD:
-- MySQL生成发票组合IDSET @Prefix = 'INVOICE'; -- 发票前缀SET @DatePart = DATE_FORMAT(NOW(), '%Y%m%d'); -- 日期部分-- 查当天最大序列号:截取invoice_id的第13到16位-- 简单说就是从现有ID里提取出序列号部分,转成数字,取最大+1SET @Sequence = IFNULL((SELECt MAX(CAST(SUBSTRINg(invoice_id, 13, 4) AS UNSIGNED)) + 1 FROM Invoices WHERe invoice_id LIKE CONCAt(@Prefix, '_', @DatePart, '_%')), 1);-- 拼接ID:用CONCAT函数,序列号补0成4位SET @NewID = CONCAt(@Prefix, '_', @DatePart, '_', LPAd(@Sequence, 4, '0'));-- 插入发票表INSERT INTO Invoices (invoice_id, customer_id) VALUES (@NewID, 'C004');注意:截取序列号的时候,一定要算对字符串的位置。如:“INVOICE_20240520_0001”这个ID,总长度是7(INVOICE)+1(下划线)+8(日期)+1(下划线)+4(序列号)=21位,所以序列号是从第18位开始(7+1+8+1=17),取4位,所以SUBSTRINg(invoice_id, 18, 4)才对。实际用的时候我们一定要算清楚,不然会截取错。
3、适用场景:需要“看懂ID含义”的业务,方便追踪
组合ID最大的优点就是“可读性强”,光看ID就能知道大概信息,特别适合这些场景:
- 订单管理:客服看到“ORDER_20240520_0001”,马上知道是2024年5月20日的第一个订单,查数据的时候不用先查数据库。
- 物流单号:如:“EXP_20240520_SH001”,能看出是2024年5月20日从上海发出的快递,方便分拣和追踪。
- 内部审批单:“APPROVE_20240520_FIN001”,一看就知道是2024年5月20日财务部门的第一个审批单。
缺点也很明显,“需要自己处理唯一性”:
- 并发问题:如果同一时间有多个请求生成当天的ID,可能会查到同一个最大序列号,导致生成重复的ID。如:两个订单同时查,都发现当前最大序列号是10,然后都生成11,这时候就重复了。解决办法是加“锁”或者用“事务”,如:查询序列号的时候加FOR UPDATe(MySQL)或WITH (UPDLOCK)、WITH (UPDLOCK, HOLDLOCK)(SQL Server),确保同一时间只有一个请求能查序列号。
- 逻辑复杂:要处理日期切换(如:零点的时候,序列号要从1重新开始)、序列号位数不够(如:一天超过9999个订单,4位序列号就不够了,得预留足够的位数,如:6位)、前缀变更(业务调整时可能要改前缀,得确保历史数据兼容)等问题。
- 存储和查询:组合ID是字符串,比自增ID占空间,查询的时候如果用ID排序,虽然带日期,但性能不如整数ID快。
六、序列(Sequence):比自增ID更灵活,支持跨表复用
“序列”是数据库提供的一种“生成连续整数”的工具,和自增ID有点像,但更灵活。自增ID只能绑定到一个表的某个字段,而序列可以被多个表共用,我们还能手动控制其起始值、步长(每次涨多少),甚至可以循环使用(不过一般不循环,避免重复)。
1、生成逻辑:数据库维护一个“计数器”,谁用谁取
序列就像一个独立的“计数器”,我们可以定义它从1000开始,每次加1,然后不管是订单表、用户表还是产品表,都可以调用“取下一个值”的函数来获取ID,用完之后计数器自动加1。如:序列当前是1000,订单表取一次变成1001,用户表再取一次变成1002,这样多个表的ID也不会重复。
不同数据库对序列的支持不一样:PostgreSQL和Oracle原生就支持序列;MySQL 8.0之前不支持,要用自增表模拟;MySQL 8.0之后也支持序列了。
2、SQL实现:不同数据库的序列用法
我们先看PostgreSQL,它的序列功能很完善,创建序列的时候可以指定起始值、步长、是否循环等等:
-- PostgreSQL创建序列:从1000开始,每次加1,不循环(NO CYCLE)CREATE SEQUENCE order_seq START 1000 -- 起始值 INCREMENT 1 -- 步长 NO CYCLE; -- 不循环,到最大值就报错-- 订单表用这个序列当IDINSERT INTO Orders (order_id, customer_id) VALUES (NEXTVAL('order_seq'), 'C001'); -- NEXTVAL取 next值-- 想知道当前序列的值是多少?用currvalSELECT currval('order_seq'); -- 如:刚插完,会输出1000,下次NEXTVAL就是1001再看Oracle,它的序列语法和PostgreSQL类似,就是函数名用.连接:
-- Oracle创建序列:从1000开始,每次加1,不循环CREATE SEQUENCE order_seq START WITH 1000 -- 起始值用START WITH INCREMENT BY 1 -- 步长用INCREMENT BY NOCYCLE; -- 不循环-- 订单表用序列的NEXTVALINSERT INTO Orders (order_id, customer_id) VALUES (order_seq.NEXTVAL, 'C002');-- 查当前值用CURRVAL,注意要先调用过NEXTVAL才能用CURRVALSELECT order_seq.CURRVAL FROM dual; -- 输出1000MySQL 8.0之前没有序列,要用“自增表”模拟,我们建一个只有自增ID的表,每次取ID就往表里插一条空数据,然后用LAST_INSERT_ID()获取:
-- MySQL 5.7及以下:用表模拟序列CREATE TABLE order_seq ( id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY);-- 取序列值:插一条空数据,然后取IDINSERT INTO order_seq VALUES (NULL);SELECt LAST_INSERT_ID(); -- 第一次是1,第二次是2,以此类推-- 用完之后可以删除这条数据(可选,不删的话表会越来越大)DELETE FROM order_seq WHERe id = LAST_INSERT_ID();MySQL 8.0之后支持序列了,我们看语法和PostgreSQL类似:
-- MySQL 8.0+创建序列CREATE SEQUENCE order_seq START WITH 1000 INCREMENT BY 1 NOCYCLE;-- 取next值用NEXTVAL()INSERT INTO Orders (order_id, customer_id) VALUES (NEXTVAL(order_seq), 'C003');3、适用场景:多表共用ID、高并发场景,比自增ID灵活
序列的优点突出:
- 跨表复用:一个序列可以给多个表用,如:订单表、支付表用同一个序列,确保所有ID在整个系统里唯一,而自增ID只能绑定一个表。
- 灵活控制:可以自定义起始值(如:从10000开始,显得数据量大)、步长(如:每次加2,生成奇数ID,偶数ID留作他用),还能手动修改序列的值(如:ALTER SEQUENCE order_seq RESTART WITH 2000)。
- 减少锁竞争:自增ID在高并发插入时,数据库会对自增计数器加锁,可能导致性能瓶颈;而序列的锁机制更轻量,高并发下性能更好。
它的局限性:
- 依赖数据库:不是所有数据库都原生支持序列(如:MySQL 8.0之前),用起来不方便。
- 分布式问题:和自增ID一样,序列也是数据库实例级别的,多个数据库实例之间的序列不互通,分布式系统里还是会出现重复ID。
- 可能有“空洞”:如果取了序列值(如:1001)但没插入数据(如:事务回滚了),这个1001就浪费了,下次会取1002,导致ID不连续。不过ID一般不需要连续,所以这个问题不大。
七、雪花算法(Snowflake)SQL适配方案
1、生成逻辑
雪花算法是Twitter开源的分布式ID生成算法,就是生成64位二进制整数(最终可转为十进制或字符串存储),结构为:
- 1位符号位:固定为0(确保ID为正数);
- 41位时间戳:记录毫秒级时间(可覆盖约69年,需自定义起始时间戳);
- 10位机器码:拆分“5位数据中心ID+5位机器ID”(支持1024个节点部署);
- 12位序列号:同一毫秒内的自增序列(单节点每毫秒最多生成4096个ID)。
由于原生雪花算法依赖代码层实现(如:Java、Python),SQL适配方案需我们通过自定义函数(UDF)将算法逻辑嵌入数据库,直接在SQL中调用生成ID。虽然技术上可行,但出于性能和维护性考虑,雪花算法我们强烈推荐在应用层实现,而非数据库自定义函数。
2、SQL实现(以MySQL 8.0+为例)
首先创建存储雪花算法的自定义函数,我们需提前确定“起始时间戳”(如:2024-01-01 00:00:00的毫秒数1704067200000)、“数据中心ID”、“机器ID”(需根据实际部署节点配置,避免重复):
-- 1、创建雪花算法自定义函数:生成64位唯一ID(返回DECIMAL类型,避免整数溢出)DELIMITER //CREATE FUNCTION GenerateSnowflakeID( datacenter_id INT, -- 数据中心ID(0-31) worker_id INT -- 机器ID(0-31)) RETURNS DECIMAL(20,0)DETERMINISTICBEGIN -- 定义常量:起始时间戳(2024-01-01 00:00:00)、各字段偏移量 DECLARE start_timestamp DECIMAL(13,0) DEFAULT 1704067200000; DECLARE timestamp_bits INT DEFAULT 41; DECLARE datacenter_bits INT DEFAULT 5; DECLARE worker_bits INT DEFAULT 5; DECLARE sequence_bits INT DEFAULT 12; -- 定义变量:当前时间戳、序列号(需用临时表存序列号,避免并发重复) DECLARE current_timestamp DECIMAL(13,0); DECLARE sequence INT DEFAULT 0; DECLARE snowflake_id DECIMAL(20,0); -- 步骤1:获取当前毫秒级时间戳 SELECt UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 INTO current_timestamp; -- 计算相对起始时间的差值(避免时间戳过长) SET current_timestamp = current_timestamp - start_timestamp; -- 步骤2:处理序列号(用临时表维护“数据中心+机器”对应的最新序列号,加锁防并发) -- 先创建临时表(若不存在):存储(datacenter_id, worker_id, last_sequence, last_timestamp) CREATE TEMPORARY TABLE IF NOT EXISTS snowflake_seq ( datacenter_id INT, worker_id INT, last_sequence INT DEFAULT 0, last_timestamp DECIMAL(13,0) DEFAULT 0, PRIMARY KEY (datacenter_id, worker_id) ); -- 加行锁:确保同一“数据中心+机器”下只有一个会话修改序列号 INSERT INTO snowflake_seq (datacenter_id, worker_id) VALUES (datacenter_id, worker_id) ON DUPLICATE KEY UPDATE datacenter_id = datacenter_id; -- 判断当前时间戳是否与上次一致:一致则序列号+1,否则重置为0 SELECT last_sequence, last_timestamp INTO sequence, @last_ts FROM snowflake_seq WHERe datacenter_id = datacenter_id AND worker_id = worker_id; IF current_timestamp = @last_ts THEN -- 同一毫秒:序列号自增(需判断是否超过4095) SET sequence = IF(sequence >= (POWER(2, sequence_bits) - 1), 0, sequence + 1); ELSE -- 不同毫秒:序列号重置为0 SET sequence = 0; END IF; -- 更新临时表的最新序列号和时间戳 UPDATe snowflake_seq SET last_sequence = sequence, last_timestamp = current_timestamp WHERe datacenter_id = datacenter_id AND worker_id = worker_id; -- 步骤3:拼接雪花ID(按位运算:时间戳 << 22 | 数据中心ID << 17 | 机器ID << 12 | 序列号) SET snowflake_id = (current_timestamp << (datacenter_bits + worker_bits + sequence_bits)) | (datacenter_id << (worker_bits + sequence_bits)) | (worker_id << sequence_bits) | sequence; RETURN snowflake_id;END //DELIMITER ;-- 2、调用函数生成ID(假设数据中心ID=1,机器ID=1)SELECT GenerateSnowflakeID(1, 1) AS snowflake_unique_id;-- 3、表中使用(如订单表)CREATE TABLE Orders ( order_id DECIMAL(20,0) DEFAULT GenerateSnowflakeID(1,1) PRIMARY KEY, customer_id VARCHAr(20), order_time DATETIME DEFAULT CURRENT_TIMESTAMP);INSERT INTO Orders (customer_id) VALUES ('C005');3、适用场景
- 高并发分布式系统(如:电商订单、支付交易):需兼顾“全局唯一”、“有序性”、“高吞吐”;
- 需ID包含时间信息的场景:通过ID可反向推算生成时间(如:排查订单创建时间)。
4、优点
(1)全局唯一:1024个节点下,每毫秒可生成4096个ID,冲突概率几乎为0;
(2)有序递增:同一节点下ID按时间递增,利于数据库索引排序(比UUID性能好);
(3)紧凑存储:64位整数仅占8字节(比36位UUID节省存储);
(4)可解析:能从ID中提取生成时间、节点信息,便于问题排查。
5、缺点
(1)依赖时间同步:若节点时间回拨,可能生成重复ID(需在函数中加时间校验逻辑);
(2)节点配置复杂:我们需提前规划“数据中心ID+机器ID”,避免节点重复;
(3)SQL实现受限:低版本MySQL(如5.x)不支持复杂自定义函数,需代码层配合。
八、基于Redis的分布式ID(SQL+Redis联动)
1、生成逻辑
利用Redis的原子操作(INCR/INCRBY)生成自增序列,再通过SQL将“Redis生成的序列+业务信息”拼接为唯一ID。优势是Redis的单线程特性可确保序列唯一,且支持跨数据库实例(解决传统自增ID无法分布式的问题),流程如下:
(1)向Redis发送INCR key命令,获取全局自增序列(如:order_seq的当前值);
(2)在SQL中拼接“业务前缀+日期+Redis序列”(如:ORDER_20240520_10001);
(3)将拼接后的ID插入数据库表,或作为业务主键使用。
2、SQL实现(以MySQL+Redis联动为例)
需先确保MySQL能通过UDF或存储过程调用Redis(我们可借助redis-cli命令行工具,或使用MySQL的sys_exec()函数执行外部命令):
-- 前提:MySQL已启用sys_exec()函数(需安装lib_mysqludf_sys插件),且服务器已安装redis-cliDELIMITER //-- 1、创建存储过程:调用Redis生成自增序列,再拼接为业务IDCREATE PROCEDURE GenerateRedisbasedID( IN redis_key VARCHAr(50), -- Redis中存储序列的key(如order_seq) IN business_prefix VARCHAr(20), -- 业务前缀(如ORDER) OUT final_unique_id VARCHAr(50) -- 输出最终唯一ID)BEGIN -- 定义变量:Redis返回的自增序列、日期部分 DECLARE redis_seq INT; DECLARE date_part VARCHAr(8); DECLARE redis_cmd VARCHAr(200); -- 调用redis-cli的命令 -- 步骤1:获取当前日期(格式:yyyyMMdd) SELECT DATE_FORMAT(CURRENT_DATE(), '%Y%m%d') INTO date_part; -- 步骤2:调用Redis的INCR命令生成自增序列(通过sys_exec()执行shell命令) -- 命令格式:redis-cli -h 127.0.0.1 -p 6379 INCR redis_key → 输出序列值 SET redis_cmd = CONCAt( 'redis-cli -h 127.0.0.1 -p 6379 INCR ', redis_key, ' > /tmp/redis_seq.txt' -- 将Redis返回的序列写入临时文件 ); -- 执行Redis命令 SET @exec_result = sys_exec(redis_cmd); -- 步骤3:读取临时文件中的序列值(Redis返回的结果) SET @read_cmd = 'cat /tmp/redis_seq.txt'; SET @redis_seq_str = sys_eval(@read_cmd); -- sys_eval()返回命令输出的字符串 SET redis_seq = CAST(@redis_seq_str AS UNSIGNED); -- 转为整数 -- 步骤4:拼接最终ID(业务前缀+日期+6位补零序列,如ORDER_20240520_000001) SET final_unique_id = CONCAt( business_prefix, '_', date_part, '_', LPAd(redis_seq, 6, '0') -- 序列补零,确保固定长度 ); -- 步骤5:可选:将ID插入数据库表(如订单表) INSERT INTO Orders (order_id, customer_id) VALUES (final_unique_id, 'C006');END //DELIMITER ;-- 2、调用存储过程生成ID(Redis key为order_seq,业务前缀为ORDER)CALL GenerateRedisbasedID('order_seq', 'ORDER', @order_id);-- 查看生成的IDSELECT @order_id AS final_unique_id;示例展示了SQL与Redis联动的理论可能性,但在生产环境中,我们强烈建议在应用层通过Redis客户端库获取序列号并生成ID,以保证性能、安全性和稳定性。直接在存储过程中调用系统命令的方式风险高,不推荐使用。
3、适用场景
- 跨数据库实例的分布式系统;
- 高并发场景下需“全局自增+业务可读”的ID(如:物流单号、订单号);
- 已有Redis缓存集群的项目:可复用Redis资源,无需额外引入其他组件。
4、优点
(1)分布式友好:Redis作为中心节点,可给多数据库实例分配唯一序列;
(2)性能高:Redis INCR操作是内存级原子操作,TPS可达10万+;
(3)可扩展性强:支持通过INCRBY设置步长(如:分库分表时,不同表用不同步长);
(4)业务可读性:拼接前缀和日期,便于人工识别ID含义。
5、缺点
(1)依赖Redis可用性:若Redis宕机,ID生成会中断(需做Redis主从备份);
(2)网络开销:SQL调用Redis需跨进程/网络,比纯数据库方案多一次网络请求;
(3)临时文件风险:通过临时文件传递序列值,若文件读写失败会导致ID生成异常(可优化为直接解析sys_exec()输出)。
九、基于时间戳+随机数的复合ID
1、生成逻辑
结合“毫秒级时间戳”和“随机数”生成唯一ID,就是利用时间戳的“唯一性(同一毫秒重复概率低)”+随机数的“补充唯一性(避免同一毫秒内重复)”。无需依赖外部组件,纯SQL即可实现,适合对ID有序性要求不高,但需避免简单自增ID泄露业务量的场景(如:用户邀请码、临时凭证ID)。
2、SQL实现(跨MySQL/SQL Server通用思路)
(1)MySQL实现
-- 生成“时间戳(13位)+ 6位随机数”的唯一ID(共19位,字符串类型)DELIMITER //CREATE FUNCTION GenerateTimeRandID() RETURNS VARCHAr(20)DETERMINISTICBEGIN DECLARE timestamp_str VARCHAr(13); -- 毫秒级时间戳(如1716200000000) DECLARE rand_str VARCHAr(6); -- 6位随机数(000000-999999) DECLARE final_id VARCHAr(20); -- 1、获取当前毫秒级时间戳(转为字符串,避免整数溢出) SELECT CAST(UNIX_TIMESTAMP(CURRENT_TIMESTAMP(3)) * 1000 AS VARCHAR) INTO timestamp_str; -- 2、生成6位随机数(用RAND()生成0-1的小数,乘以1000000后取整,再补零) SELECT LPAd(FLOOR(RAND() * 1000000), 6, '0') INTO rand_str; -- 3、拼接ID(时间戳+随机数) SET final_id = CONCAt(timestamp_str, rand_str); RETURN final_id;END //DELIMITER ;-- 表中使用(如:用户邀请码表)CREATE TABLE UserInvite ( invite_id VARCHAr(20) DEFAULT GenerateTimeRandID() PRIMARY KEY, user_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP);INSERT INTO UserInvite (user_id) VALUES (1001);SELECT invite_id FROM UserInvite WHERe user_id = 1001;(2)SQL Server实现
-- 生成“时间戳(13位)+ 6位随机数”的唯一IDCREATE FUNCTION GenerateTimeRandID()RETURNS VARCHAr(20)ASBEGIN DECLARE @timestamp_str VARCHAr(13); DECLARE @rand_str VARCHAr(6); DECLARE @final_id VARCHAr(20); -- 1、获取当前毫秒级时间戳(DATEADD转换为1970-01-01以来的毫秒数) SELECt @timestamp_str = CAST(DATEDIFF(MILLISECOND, '1970-01-01 00:00:00', GETDATE()) AS VARCHAR); -- 2、生成6位随机数(NEWID()生成GUID,取前6位转为数字后补零) SELECT @rand_str = LPAd(CAST(SUBSTRINg(CAST(NEWID() AS VARCHAr(36)), 1, 6) AS BIGINT) % 1000000, 6, '0'); -- 3、拼接ID SET @final_id = CONCAt(@timestamp_str, @rand_str); RETURN @final_id;END;-- 表中使用(如:临时凭证表)CREATE TABLE TempCert ( cert_id VARCHAr(20) DEFAULT dbo.GenerateTimeRandID() PRIMARY KEY, user_id INT, expire_time DATETIME);INSERT INTO TempCert (user_id, expire_time) VALUES (1002, DATEADD(HOUR, 2, GETDATE()));SELECT cert_id FROM TempCert WHERe user_id = 1002;3、适用场景
- 非核心业务的临时ID(如:用户邀请码、短信验证码凭证、临时会话ID);
- 需隐藏业务量的场景(避免自增ID泄露“已生成多少条数据”,如:1716200000000123456无法直接推断总量);
- 低并发场景(同一毫秒内生成超过100万条数据时,6位随机数可能重复,需增加随机数位数)。
4、优点
(1)实现简单:纯SQL编写,无需依赖外部组件(如:Redis、代码层);
(2)无顺序泄露:ID不体现连续自增,避免竞争对手通过ID推断业务规模;
(3)低存储成本:19位字符串(或可转为DECIMAL),比UUID更紧凑。
5、缺点
(1)存在极小重复概率:同一毫秒内生成大量ID时,随机数可能碰撞(需根据并发量调整随机数位数,如:高并发用8位随机数);
(2)无序性:ID虽含时间戳,但随机数会破坏整体有序性,不利于索引排序(比雪花算法、自增ID的查询性能差);
(3)不可解析:无法从ID中提取除时间外的其他信息(如:节点、业务类型,需额外字段存储)。
十、数据库内置UUID变种(如PostgreSQL的UUID v7、MySQL 8.0.31+的UUID v7)
1、生成逻辑
传统UUID v4(纯随机)存在“无序、索引性能差”的问题,UUID v7是IETF提议的新UUID版本,主要改进是“基于时间戳+随机数”,结构为:
- 48位时间戳:记录毫秒级UNIX时间(可覆盖约8925年);
- 12位版本号+变体位:固定标识为UUID v7;
- 62位随机数:确保同一毫秒内的唯一性。
由于UUID v7包含时间戳且前半段有序,能大幅提升数据库索引性能(解决UUID v4的“索引碎片化”问题),各数据库已逐步支持原生生成:
- PostgreSQL 13+:通过uuid_generate_v7()函数(需安装uuid-ossp扩展);
- MySQL 8.0.31+:通过UUID_TO_BIn(UUID(), TRUE)生成有序二进制UUID(本质是UUID v7的优化实现);
- SQL Server 2022+:通过NEWUUIDV7()函数(预览功能,需开启相关配置)。
2、SQL实现(以PostgreSQL和MySQL为例)
(1)PostgreSQL 13+实现
-- 1、安装uuid-ossp扩展(支持UUID生成函数)CREATE EXTENSION IF NOT EXISTS "uuid-ossp";-- 2、生成UUID v7(PostgreSQL 15+原生支持uuid_generate_v7(),低版本需自定义函数)-- 若为PostgreSQL 15+:SELECt uuid_generate_v7() AS uuid_v7_unique_id;-- 若为PostgreSQL 13-14:自定义UUID v7函数(模拟实现)DELIMITER //CREATE OR REPLACE FUNCTION generate_uuid_v7()RETURNS UUIDAS $$DECLARE unix_ts_ms BIGINT; rand_a BIGINT; rand_b BIGINT; uuid_bytes BYTEA;BEGIN -- 1、获取当前毫秒级时间戳 SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP(3)) * 1000 INTO unix_ts_ms; -- 2、生成随机数(62位,拆分为32位+30位) SELECt FLOOR(RANDOM() * 4294967296)::BIGINT INTO rand_a; -- 32位随机数 SELECT FLOOR(RANDOM() * 1073741824)::BIGINT INTO rand_b; -- 30位随机数 -- 3、拼接字节:时间戳(6字节)+ 版本号(0111,即UUID v7)+ 随机数(7字节) SELECT CONCAt( ENCODE(INT8TOBYTES(unix_ts_ms), 'hex'), -- 6字节时间戳(48位) '7', -- 版本号:第7字节的高4位为0111(v7) SUBSTRINg(ENCODE(INT8TOBYTES(rand_a), 'hex'), 3, 6), -- 随机数前3字节(24位) 'b', -- 变体位:第10字节的高2位为10(RFC 4122标准) SUBSTRINg(ENCODE(INT8TOBYTES(rand_b), 'hex'), 3, 6) -- 随机数后3字节(24位) ) INTO uuid_bytes; -- 4、转为UUID格式(8-4-4-4-12) RETURN ( SUBSTRINg(uuid_bytes, 1, 8) || '-' || SUBSTRINg(uuid_bytes, 9, 4) || '-' || SUBSTRINg(uuid_bytes, 13, 4) || '-' || SUBSTRINg(uuid_bytes, 17, 4) || '-' || SUBSTRINg(uuid_bytes, 21, 12) )::UUID;END;$$ LANGUAGE plpgsql;-- 3、表中使用(如用户表)CREATE TABLE Users ( user_id UUID DEFAULT generate_uuid_v7() PRIMARY KEY, username VARCHAr(50), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP);INSERT INTO Users (username) VALUES ('Charlie');SELECT user_id FROM Users WHERe username = 'Charlie';(2)MySQL 8.0.31+实现
MySQL 8.0.31+虽未直接提供生成UUID v7的函数,但通过UUID_TO_BIn(UUID(), TRUE)函数,可以将标准的UUID v4转换为一种时间有序的二进制格式进行存储。这种方式借鉴了UUID v7按时间排序的思想,能显著提升基于UUID主键的索引性能,是优化现有UUID v4方案的有效手段。但它生成的源头仍是UUID v4,并非标准的UUID v7格式:
-- 1、生成有序UUID(二进制格式,节省存储)SELECt UUID_TO_BIn(UUID(), TRUE) AS ordered_uuid_bin; -- 输出16字节二进制SELECT BIN_TO_UUID(UUID_TO_BIn(UUID(), TRUE)) AS ordered_uuid_str; -- 转为36位字符串-- 2、表中使用(推荐用BINARY类型存储,比CHAr(36)节省50%存储)CREATE TABLE Products ( product_id BINARY(16) DEFAULT UUID_TO_BIn(UUID(), TRUE) PRIMARY KEY, product_name VARCHAr(100), price DECIMAL(10,2));INSERT INTO Products (product_name, price) VALUES ('Laptop', 5999.99);-- 3、查询时转为可读字符串SELECT BIN_TO_UUID(product_id) AS product_id_str, product_name FROM Products WHERe product_id = UUID_TO_BIn('018e8d7c-7f4a-7e0b-8c12-3456789abcde');3、适用场景
- 需“全局唯一+有序索引”的分布式场景(如:多PostgreSQL实例的用户表、跨地域产品表);
- 已有UUID v4代码逻辑,需低成本优化索引性能的场景(直接替换为UUID v7,无需大幅修改业务代码);
- 遵循国际标准的场景(UUID v7是IETF提议的标准,比自定义雪花算法更易兼容第三方系统)。
4、优点
(1)全局唯一:符合UUID标准,跨系统、跨地域无冲突;
(2)有序性:基于时间戳生成,前半段有序,大幅提升B+树索引的插入和查询性能(解决UUID v4的痛点);
(3)数据库原生支持:各数据库逐步内置函数,无需自定义复杂逻辑;
(4)向后兼容:可转为标准UUID字符串格式,兼容现有接收UUID的系统。
5、缺点
(1)存储成本高于整数ID:16字节二进制(或36位字符串),比8字节的雪花算法、4字节的自增ID存储成本高;
(2)版本兼容性:低版本数据库(如:MySQL 8.0.30及以下、PostgreSQL 12及以下)不支持原生UUID v7,需自定义函数;
(3)时间精度有限:基于毫秒级时间戳,同一毫秒内仍需依赖随机数(比雪花算法的12位序列号(4096个/毫秒)的并发支持弱,UUID v7的随机数位数更多,但本质仍是概率去重)。
十一、其他数据库特性:快速获取刚生成的ID,方便后续操作
有时候我们插完数据后,需要马上用到刚生成的ID(如:插完订单,马上用订单ID插订单项),这时候就需要数据库提供的“获取最新ID”的函数,不同数据库的函数不一样,用的时候得注意区分。
1、MySQL的LAST_INSERT_ID()
LAST_INSERT_ID()用来获取“当前会话”中,最后一次用自增ID插入数据的ID值,而且只针对自增列(AUTO_INCREMENT)有效。
-- 插一条用户数据,user_id是自增IDINSERT INTO Users (username) VALUES ('Alice');-- 马上获取刚生成的user_idSELECT LAST_INSERT_ID(); -- 如:输出5,就是Alice的ID-- 实际用的时候,可能接着用这个ID插其他表,如:用户详情表INSERT INTO UserDetails (user_id, address) VALUES (LAST_INSERT_ID(), '北京市朝阳区');注意:LAST_INSERT_ID()是“会话级”的,我们在自己的连接里调用,只能拿到自己插的数据的ID,不会受其他用户的操作影响。
2、SQL Server的SCOPE_IDENTITY()和@@IDENTITY
SQL Server里有两个类似的函数,SCOPE_IDENTITY()和@@IDENTITY,用法有区别,用错了可能会出问题。
- SCOPE_IDENTITY():获取“当前会话、当前作用域”内最后生成的自增ID,推荐用这个,安全。
- @@IDENTITY:获取“当前会话”内最后生成的自增ID,不管是不是当前作用域(如:触发器里生成的ID也会被它捕获),容易出问题。
我们看例子:
-- 插一条用户数据,user_id是IDENTITY自增INSERT INTO Users (username) VALUES ('Bob');-- 用SCOPE_IDENTITY()获取Bob的ID,推荐SELECT SCOPE_IDENTITY(); -- 输出10-- 如果这时候有个触发器,在插Users表后自动往Logs表插数据,Logs表也有自增ID-- 这时候@@IDENTITY会返回Logs表的ID,而不是Users表的,所以不推荐用SELECT @@IDENTITY; -- 可能输出20(Logs表的自增ID),而不是10所以在SQL Server里,优先用SCOPE_IDENTITY(),除非我们明确知道自己要获取其他作用域ID。
3、Oracle的RETURNING子句
Oracle里没有LAST_INSERT_ID()这种函数,但我们可以用RETURNING子句在插入数据的时候直接返回生成的ID:
-- 声明变量存IDDECLARE new_user_id NUMBER;BEGIN -- 插入用户数据,用序列生成ID,同时把生成的ID返回到new_user_id INSERT INTO Users (user_id, username) VALUES (user_seq.NEXTVAL, 'Charlie') RETURNING user_id INTO new_user_id; -- 关键是RETURNING ... INTO -- 输出ID DBMS_OUTPUT.PUT_LINE('新用户ID:' || new_user_id);END;/4、Hibernate的Hi/Lo算法(半数据库特性)
这个不算纯SQL方法,但和数据库交互密切。Hi/Lo是一种“减少数据库访问”的ID生成算法,原理是:先从数据库的序列里取一个“大块”的ID范围(如:1-100),然后在内存里从1开始用,用完了再去数据库取下一个范围(101-200)。这样不用每次生成ID都访问数据库,适合中低并发的分布式系统。
在Hibernate(Java的ORM框架)里配置Hi/Lo算法的XML示例:
<id name="id" type="long"> <generator class="org.hibernate.id.enhanced.SequenceStyleGenerator"> <param name="sequence_name">order_seq</param> <!-- 数据库里的序列名 --> <param name="initial_value">1000</param> <!-- 起始值 --> <param name="increment_size">100</param> <!-- 每次取100个ID,内存里用 --> </generator></id>优点是减少数据库访问,缺点是需要维护序列表,而且如果某个节点取了1-100但只用到50就宕机了,剩下的51-100就浪费了,会产生ID空洞。
十二、怎样选择唯一ID生成方法?
1、方法特性对比
方法 | 优点 | 缺点 | 适用场景 |
自增ID | 简单高效、存储成本低、索引性能优 | 无法跨数据库实例、不支持分布式 | 单体应用、低并发场景(内部管理系统、小型业务系统) |
UUID/GUID | 全局唯一、分布式友好、生成不依赖数据库 | 存储成本高(36字符)、无序影响索引、可读性差 | 分布式系统、数据量中等场景(多节点电商订单、跨系统用户标识) |
存储过程生成 | 强制唯一性、可自定义校验逻辑 | 性能差、有死循环风险、维护复杂 | 金融交易、支付等对唯一性零容忍的核心场景 |
唯一约束 | 数据库原生保障、自动创建索引、查询高效 | 不同数据库对NULL处理不同 | 需保证字段唯一性的场景(用户邮箱、手机号、商品编码) |
组合ID | 可读性强、含业务信息、便于追踪 | 需处理并发冲突、逻辑复杂 | 需人工识别含义的场景(物流单号、审批单号、订单编号) |
序列 | 跨表复用、灵活控制、高并发性能好 | 依赖数据库特性、不支持分布式 | 单体应用多表共用ID、高并发场景(多业务线共享ID池) |
雪花算法 | 全局唯一、有序递增、高吞吐、可解析 | 依赖时间同步、节点配置复杂 | 高并发分布式系统(大规模电商、支付平台) |
Redis分布式ID | 分布式友好、性能高、可拼接业务信息 | 依赖Redis可用性、有网络开销 | 跨数据库实例场景、需业务可读的全局ID(多区域订单系统) |
时间戳+随机数 | 实现简单、无顺序泄露、不依赖外部组件 | 存在极小重复概率、无序性影响索引 | 非核心临时ID(邀请码、临时凭证、会话ID) |
UUID v7 | 全局唯一、有序性好、兼容标准、索引友好 | 存储成本高于整数ID、低版本数据库不支持 | 需兼顾标准性和性能的分布式场景(跨系统数据同步) |
其他特性(如:LAST_INSERT_ID) | 快速获取最新生成ID、方便后续操作 | 数据库函数不统一、易混淆 | 插入后需立即使用ID的场景(主从表关联插入) |
2、业务场景对比
业务场景 | 推荐方法 | 不推荐方法 | 选择依据 |
单体应用、低并发 (如:内部OA、小型CRM) | 自增ID、序列 | UUID、Redis分布式ID | 追求简单高效,无需处理分布式复杂性,自增ID索引性能最优 |
分布式系统、高并发 (如:电商订单、支付系统) | 雪花算法、Redis分布式ID、UUID v7 | 自增ID、存储过程生成 | 需全局唯一且高吞吐,雪花算法兼顾有序性和性能,Redis适合需业务可读场景 |
跨系统数据同步 (如:多系统共享用户数据) | UUID v7、传统UUID | 自增ID、组合ID | 需无系统依赖的全局唯一标识,UUID可避免跨库ID冲突 |
业务可读ID需求 (如:物流单号、客服工单) | 组合ID、Redis分布式ID(带业务前缀) | 自增ID、UUID | 需人工快速识别ID含义,便于业务追踪和客服查询 |
非核心临时ID (如:邀请码、短信验证码) | 时间戳+随机数、UUID v4 | 雪花算法、存储过程 | 对性能和有序性要求低,优先选择实现简单的方案 |
字段唯一性保障 (如:用户邮箱、手机号) | 唯一约束+索引 | 存储过程生成 | 数据库原生机制比自定义逻辑更可靠,且自动优化查询性能 |
主从表关联场景 (如:订单表与订单项表) | 自增ID/序列 + LAST_INSERT_ID() | 无明确不推荐 | 需要高效获取刚生成的主表ID,用于从表插入 |
3、实用选择建议
1、优先考虑场景适配性:没有万能方案,单体应用不必过度设计(如:用自增ID即可),分布式系统则必须保证全局唯一性(如:雪花算法、UUID)。
2、平衡性能与复杂度:存储过程虽能保证强一致性,但性能开销大,仅在金融级场景使用;Redis分布式ID性能好,但需额外维护Redis集群,适合已有Redis资源的项目。
3、双重保障机制:任何场景下,关键ID字段都应加主键约束,业务唯一字段(如:邮箱)应加唯一约束,即使使用自增ID或UUID,也需通过数据库约束防止异常重复。
4、灵活组合方案:复杂场景可采用"主ID+副ID"模式,例如:
- 主ID用雪花算法(保证唯一和性能)
- 副ID用组合ID(如:ORDER_20240520_0001,方便业务使用)
5、预留扩展空间:设计ID时需考虑业务增长,如组合ID的序列号位数应预留冗余(每天最大订单量10万则至少用6位序列号),雪花算法节点ID应预留未来扩容空间。

