前言
在分布式系统中,分库分表是解决"数据量过大、单库单表性能瓶颈"的核心方案。但分库分表后,原本单表依赖的"自增主键(AUTO_INCREMENT)"彻底失效------多个分表若各自自增,会出现主键重复;跨库跨表查询时,重复主键会导致数据混乱。因此,"生成全局唯一、有序且高性能的主键 ID",成为分库分表落地的关键问题。
本文将拆解 5 种主流的主键生成方案,结合代码示例分析其优缺点,帮你找到适合业务的"优雅方案"。
一、为什么不能用"自增主键"?
先明确痛点:单库单表时,AUTO_INCREMENT
能保证主键唯一且有序,但分库分表后完全不适用:
• 主键重复风险:比如分表 2 张(order_1
、order_2
),若两张表都从 1 开始自增,会同时生成 ID=1 的订单记录,跨表查询时无法区分;
• 无法全局排序:自增主键仅在单表内有序,跨表时 ID 顺序混乱(比如 order_1
最大 ID=100,order_2
最大 ID=90),无法通过 ID 判断数据插入时间。
因此,分库分表需要的主键 ID,必须满足 3 个核心要求:全局唯一、趋势有序(可选,便于索引优化)、高性能(生成速度快,无单点瓶颈)。
二、5 种主键生成方案:代码示例 + 优缺点分析
方案 1:UUID/GUID(最简单但不推荐)
原理:基于 UUID(Universally Unique Identifier)标准,通过 MAC 地址、时间戳、随机数等生成 128 位的全局唯一字符串(如 550e8400-e29b-41d4-a716-446655440000
)。 Java 中可直接通过 java.util.UUID
类生成,无需额外依赖。
代码示例
java
import java.util.UUID;
public class UuidGenerator {
public static String generateId() {
// 生成标准 UUID(带横杠),可通过 replace("-", "") 去除横杠
UUID uuid = UUID.randomUUID();
return uuid.toString().replace("-", ""); // 输出示例:550e8400e29b41d4a716446655440000
}
public static void main(String[] args) {
System.out.println("生成的UUID主键:" + generateId());
}
}
优缺点
• 优点:实现简单(一行代码)、无网络开销、无单点风险;
• 缺点:
1.无序:UUID 是随机字符串,无法通过 ID 判断插入时间,会导致数据库索引(如 MySQL 聚簇索引)频繁分裂,性能下降;
2.占用空间大:128 位字符串比 64 位整数(Long 类型)占用更多存储,且查询效率低。
适用场景:对 ID 有序性无要求、数据量小的非核心业务(如日志表、临时表)。
方案 2:数据库自增表(简单但有瓶颈)
原理 :单独创建一张"主键生成表",通过单库单表的自增主键,为所有分库分表提供全局唯一 ID。例如创建 id_generator
表,每次需要 ID 时,插入一条空记录并返回自增 ID。
代码示例(MySQL 表 + Java 实现)
1.先创建主键生成表:
sql
CREATE TABLE `id_generator` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '全局唯一ID',
`biz_type` varchar(50) NOT NULL COMMENT '业务类型(如order、user)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_biz_type` (`biz_type`) COMMENT '避免同一业务重复插入'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT '全局主键生成表';
2.Java 代码调用(通过 MyBatis 操作):
java
// Mapper 接口
public interface IdGeneratorMapper {
// 插入一条业务记录,获取自增ID
@Insert("INSERT INTO id_generator (biz_type) VALUES (#{bizType}) ON DUPLICATE KEY UPDATE id = id + 1")
@SelectKey(statement = "SELECT 1608051", keyProperty = "id", resultType = Long.class, before = false)
void generateId(@Param("bizType") String bizType, @Param("id") Long id);
}
// 服务层实现
@Service
public class IdGeneratorService {
@Autowired
private IdGeneratorMapper idGeneratorMapper;
// 生成指定业务的全局ID
public Long getGlobalId(String bizType) {
Map<String, Object> param = new HashMap<>();
param.put("bizType", bizType);
param.put("id", 0L); // 初始值,会被覆盖
idGeneratorMapper.generateId(param);
return (Long) param.get("id");
}
// 测试:生成订单业务的全局ID
public static void main(String[] args) {
IdGeneratorService service = new IdGeneratorService();
Long orderId = service.getGlobalId("order");
System.out.println("订单全局ID:" + orderId); // 输出示例:100001
}
}
优缺点
•优点:实现简单、ID 有序(便于索引优化)、全局唯一;
•缺点:
1.单点瓶颈:所有业务的 ID 生成都依赖这张表,高并发场景下(如秒杀订单)会导致数据库压力过大,甚至宕机;
2.扩展性差:若主键生成表所在数据库挂了,整个系统无法生成 ID,可用性低。
适用场景:中小规模业务、并发量不高(如日均订单 10 万以下)的场景。
方案 3:号段模式(缓解数据库压力)
原理:在"数据库自增表"基础上优化,一次性从数据库获取一段 ID(如 1000 个),缓存在本地服务中,后续生成 ID 直接从缓存中取,用完再向数据库申请新号段。例如:第一次申请 1-1000,用完后申请 1001-2000。
代码示例(基于 Redis 缓存号段)
1.数据库表结构不变(复用方案 2 的 id_generator
表);
2.Java 代码实现(结合 Redis 缓存号段):
java
@Service
public class SegmentIdGeneratorService {
@Autowired
private IdGeneratorMapper idGeneratorMapper;
@Autowired
private RedisTemplate<String, String> redisTemplate;
// 号段长度(每次从数据库申请1000个ID)
private static final int SEGMENT_SIZE = 1000;
// Redis键前缀(存储当前业务的号段范围,如 order:current=1000,order:max=2000)
private static final String REDIS_KEY_CURRENT = "%s:segment:current";
private static final String REDIS_KEY_MAX = "%s:segment:max";
public Long getGlobalId(String bizType) {
// 1. 先从Redis获取当前号段
String currentKey = String.format(REDIS_KEY_CURRENT, bizType);
String maxKey = String.format(REDIS_KEY_MAX, bizType);
Long current = redisTemplate.opsForValue().increment(currentKey, 1);
Long max = Long.valueOf(redisTemplate.opsForValue().get(maxKey));
// 2. 若当前号段已用完(current > max),向数据库申请新号段
if (current == null || max == null || current > max) {
synchronized (bizType.intern()) { // 防止并发重复申请号段
// 二次检查(避免锁等待期间已被其他线程申请)
current = redisTemplate.opsForValue().increment(currentKey, 1);
max = Long.valueOf(redisTemplate.opsForValue().get(maxKey));
if (current == null || max == null || current > max) {
// 从数据库申请新号段(每次加SEGMENT_SIZE)
Long dbMaxId = getDbMaxId(bizType);
Long newMax = dbMaxId + SEGMENT_SIZE;
// 更新Redis中的号段
redisTemplate.opsForValue().set(currentKey, "1"); // 重置current为1
redisTemplate.opsForValue().set(maxKey, newMax.toString());
// 重新获取当前ID
current = redisTemplate.opsForValue().increment(currentKey, 1);
max = newMax;
}
}
}
// 3. 生成最终ID(数据库当前最大ID + 缓存中的current)
Long dbMaxId = getDbMaxId(bizType);
return dbMaxId + current - 1;
}
// 从数据库获取当前业务的最大ID
private Long getDbMaxId(String bizType) {
Map<String, Object> param = new HashMap<>();
param.put("bizType", bizType);
param.put("id", 0L);
idGeneratorMapper.generateId(param);
return (Long) param.get("id");
}
// 测试
public static void main(String[] args) {
SegmentIdGeneratorService service = new SegmentIdGeneratorService();
Long orderId = service.getGlobalId("order");
System.out.println("订单全局ID:" + orderId); // 输出示例:100001(若数据库当前最大ID=100000,current=1)
}
}
优缺点
•优点:大幅降低数据库压力(1 次数据库请求对应 1000 次 ID 生成)、ID 有序、无重复;
•缺点:
1.服务重启丢失号段:若服务宕机,缓存中的未使用号段会丢失(如申请了 1001-2000,只用了 1001-1500,重启后会从 2001 开始,1501-2000 永久丢失),导致 ID 不连续;
2.仍依赖数据库:数据库挂了,无法申请新号段,仍有可用性风险。
适用场景:中高并发场景(如日均订单 10 万-100 万),对 ID 连续性要求不高的业务。
方案 4:雪花算法(Snowflake,分布式首选)
原理:由 Twitter 开源,基于"时间戳 + 机器 ID + 序列号"生成 64 位 Long 类型 ID,结构如下:
•1 位符号位:固定为 0(确保 ID 为正数);
•41 位时间戳:记录毫秒级时间(从指定起始时间开始,可使用约 69 年);
•10 位机器 ID:区分不同服务器(支持 2^10=1024 台机器);
•12 位序列号:同一毫秒内,同一机器可生成 2^12=4096 个 ID(解决同一毫秒内的并发问题)。
代码示例(Java 实现雪花算法)
java
public class SnowflakeIdGenerator {
// 1. 固定参数
private static final long SIGN_BIT = 1L << 63; // 符号位(固定0,无需使用)
private static final long TIMESTAMP_BITS = 41L; // 时间戳位数
private static final long WORKER_ID_BITS = 10L; // 机器ID位数
private static final long SEQUENCE_BITS = 12L; // 序列号位数
// 2. 位移参数(计算各部分的偏移量)
private static final long WORKER_ID_SHIFT = SEQUENCE_BITS; // 机器ID左移12位
private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; // 时间戳左移22位
// 3. 最大值(防止溢出)
private static final long MAX_WORKER_ID = (1L << WORKER_ID_BITS) - 1; // 机器ID最大为1023
private static final long MAX_SEQUENCE = (1L << SEQUENCE_BITS) - 1; // 序列号最大为4095
// 4. 自定义起始时间(2024-01-01 00:00:00,减少时间戳位数占用)
private static final long START_TIMESTAMP = 1704067200000L;
// 5. 本地变量(线程安全)
private final long workerId; // 当前机器ID
private long lastTimestamp = -1L; // 上一次生成ID的时间戳
private long sequence = 0L; // 当前序列号
// 构造器:传入机器ID(需确保全局唯一,可从配置中心获取)
public SnowflakeIdGenerator(long workerId) {
if (workerId < 0 || workerId > MAX_WORKER_ID) {
throw new IllegalArgumentException("机器ID超出范围(0-1023):" + workerId);
}
this.workerId = workerId;
}
// 生成全局唯一ID(线程安全,加synchronized)
public synchronized long generateId() {
long currentTimestamp = System.currentTimeMillis();
// 1. 防止时间回拨(若当前时间 < 上一次时间,说明时钟回拨,抛出异常)
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,无法生成ID:" + (lastTimestamp - currentTimestamp) + "ms");
}
// 2. 同一毫秒内,序列号自增
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
// 3. 序列号用完(同一毫秒生成4096个ID后,阻塞到下一毫秒)
if (sequence == 0) {
currentTimestamp = waitNextMillis(lastTimestamp);
}
} else {
// 4. 新的毫秒,重置序列号为0
sequence = 0L;
}
// 5. 更新上一次时间戳
lastTimestamp = currentTimestamp;
// 6. 拼接ID:时间戳 + 机器ID + 序列号
return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_SHIFT)
| (workerId << WORKER_ID_SHIFT)
| sequence;
}
// 阻塞到下一毫秒,获取新的时间戳
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
// 测试(机器ID设为1)
public static void main(String[] args) {
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1);
for (int i = 0; i < 5; i++) {
System.out.println("雪花算法生成ID:" + generator.generateId());
}
// 输出示例:15468960768001, 15468960768002, 15468960768003...
}
}
优缺点
•优点:
1.高性能:本地生成,无网络开销,单机每秒可生成百万级 ID;
2.全局唯一:通过机器 ID 区分服务器,通过序列号解决同一毫秒并发;
3.趋势有序:时间戳在前,ID 整体按时间递增,便于数据库索引优化;
4.无依赖:不依赖数据库或 Redis,可用性高。
•缺点:
1.机器 ID 需全局唯一:需通过配置中心(如 Nacos、ZooKeeper)分配机器 ID,避免重复;
2.时钟回拨风险:若服务器时钟回拨(如手动调整时间),会导致 ID 重复,需通过监控或算法规避(如示例中抛出异常)。
适用场景:高并发分布式系统(如秒杀、电商订单),是目前分库分表场景下的"首选方案"。
方案 5:第三方工具(省心省力)
原理:直接使用成熟的分布式 ID 生成工具,无需重复造轮子,常见工具包括:
•美团 Leaf:基于"号段模式 + 雪花算法",支持两种模式切换,提供可视化管理界面;
•百度 UID Generator:优化雪花算法,支持自定义时间戳、机器 ID 位数,解决雪花算法的机器 ID 上限问题;
•Redis 自增:利用 INCR 命令的原子性生成 ID(如 INCR order:id),适合中小并发场景。
好的,先给大家分享这些吧,觉得有帮助的友友们可以点个一键三连,多多关注我,后续持续分享内容哦