在SQL数据库的实际使用中,慢慢增长的ID主键冲突问题常常出现在分布式部署、数据分片、多环境数据同步等场景,传统的自增ID或者步长递增的ID方案很难适配这些场景,而雪花算法虽然能解决分布式ID问题,但存在时钟回拨风险、依赖机器ID配置等不足,因此很多开发者需要寻找更合适的替代方案。

慢慢增长的ID产生主键冲突的原因
传统的慢慢增长ID通常基于数据库自增属性实现,比如MySQL的AUTO_INCREMENT,或者应用层按固定步长递增生成。这类方案在单库单表场景下基本不会出现冲突,但遇到以下情况就会出问题:
- 多数据库实例同时写入数据,各自的自增ID起始值和步长如果配置不当,就会出现重复ID
- 分库分表后,不同分片的自增ID规则没有做全局统一,导致不同分片的ID重复
- 数据迁移或者批量导入历史数据时,导入的ID和现有自增ID的范围重叠,触发冲突
- 应用层缓存了部分ID段,重启后缓存的ID段和数据库当前自增最大值冲突
雪花算法的不足与替代需求
雪花算法生成的ID是64位的长整型,包含时间戳、机器ID、序列号三部分,能在分布式场景下生成趋势递增且不重复的ID,但它存在两个明显的问题:一是如果服务器时钟发生回拨,可能会生成重复ID;二是需要提前规划机器ID的分配,小规模场景下配置成本较高。因此很多轻量级业务场景下,开发者需要更简单的替代方案。
可替代雪花算法的ID生成方案
1. UUID方案
UUID是全球唯一的字符串标识,不需要依赖中心化的服务,生成逻辑简单,几乎不会出现重复。在SQL中使用时,只需要把主键字段的类型设置为VARCHAR(36)或者CHAR(36)即可存储UUID值。
UUID的生成示例(Java语言):
import java.util.UUID;
public class UUIDGenerator {
public static String generateId() {
// 生成标准UUID,去掉横杠后长度为32位
return UUID.randomUUID().toString().replace("-", "");
}
}
在SQL中插入数据时直接使用生成的UUID作为主键:
INSERT INTO user_table (id, user_name, age)
VALUES ('550e8400e29b41d4a716446655440000', '张三', 25);
这种方案的优点是生成逻辑简单,无网络依赖,缺点是UUID是字符串类型,索引效率比整型低,而且不是趋势递增,可能会导致索引页分裂。
2. 数据库号段模式
号段模式是应用层从数据库批量获取一段连续的ID范围,比如每次获取1000个ID,应用层在本地依次分配这1000个ID,用完后再次从数据库获取下一段。这种模式既保留了ID是整型、趋势递增的优势,又避免了频繁访问数据库。
首先需要创建号段管理表:
CREATE TABLE id_segment (
biz_type VARCHAR(20) NOT NULL COMMENT '业务类型,比如user表示用户表',
max_id BIGINT NOT NULL COMMENT '当前已分配的最大ID',
step INT NOT NULL DEFAULT 1000 COMMENT '每次获取的号段长度',
PRIMARY KEY (biz_type)
);
获取号段的SQL逻辑:
-- 假设要获取用户表的ID号段,步长为1000 UPDATE id_segment SET max_id = max_id + 1000 WHERE biz_type = 'user'; -- 查询更新后的最大ID SELECT max_id FROM id_segment WHERE biz_type = 'user';
应用层拿到返回的max_id后,本次可使用的ID范围就是(max_id - 1000, max_id],依次分配即可。这种方案适合大部分业务场景,ID是整型趋势递增,索引效率高,缺点是需要依赖数据库表,号段用完前需要提前续期,避免ID耗尽。
3. Redis自增方案
Redis的INCR命令能保证原子性的自增操作,利用这个特性可以生成全局唯一的递增ID,性能比直接访问数据库更高。这种方案适合对ID生成性能要求较高的场景。
生成ID的示例(Python语言,使用redis-py库):
import redis
# 连接Redis服务
redis_client = redis.Redis(host='127.0.0.1', port=6379, db=0)
def generate_user_id():
# 对用户表对应的键做自增,初始值为0,第一次调用返回1
return redis_client.incr('user_id_counter')
生成的ID可以直接作为SQL表的主键插入:
INSERT INTO order_table (id, order_no, user_id) VALUES (123, 'ORD20240501001', 1);
这种方案的优点是性能高,ID是整型递增,缺点是需要依赖Redis服务,如果Redis宕机且没有持久化,可能会丢失部分ID段,需要做好Redis的高可用配置。
4. 自定义时间戳拼接方案
如果不想引入额外的组件,也可以自定义简单的ID生成规则,比如用当前时间戳(精确到毫秒)拼接上应用实例的固定标识和自增序列,生成唯一ID。这种方案适合小规模分布式场景。
生成逻辑示例(Go语言):
package main
import (
"fmt"
"sync"
"time"
)
var (
seq int64
mu sync.Mutex
// 假设当前实例的固定标识是01,多实例需要保证标识唯一
instanceFlag = "01"
)
func generateCustomId() string {
mu.Lock()
defer mu.Unlock()
// 获取当前毫秒时间戳
timestamp := time.Now().UnixMilli()
seq++
// 拼接时间戳、实例标识、序列号,保证唯一
return fmt.Sprintf("%d%s%03d", timestamp, instanceFlag, seq)
}
这种方案生成的ID是字符串类型,包含时间信息,趋势递增,不需要依赖外部服务,缺点是需要保证实例标识唯一,序列号在多实例或者高并发下需要做好同步,否则可能出现重复。
不同方案的适用场景对比
| 方案 | ID类型 | 是否趋势递增 | 依赖组件 | 适用场景 |
|---|---|---|---|---|
| UUID | 字符串 | 否 | 无 | 对ID无递增要求、业务规模小的场景 |
| 数据库号段模式 | 整型 | 是 | 数据库 | 大部分常规业务场景 |
| Redis自增 | 整型 | 是 | Redis | 高并发、对性能要求高的场景 |
| 自定义时间戳拼接 | 字符串 | 是 | 无 | 小规模分布式、不想引入额外组件的场景 |
方案选择的注意事项
在选择替代方案时,需要结合自身的业务需求判断:如果业务对ID的索引性能要求高,优先选择整型ID的方案,比如号段模式或者Redis自增;如果业务规模很小,不想引入额外组件,UUID或者自定义时间戳拼接方案更合适;如果业务是分库分表场景,需要确保ID生成规则是全局唯一的,避免不同分片的ID冲突。另外不管选择哪种方案,都建议在应用层做好ID重复的检测,进一步降低主键冲突的风险。