首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >分库分表下的 ID 冲突问题与雪花算法讲解

分库分表下的 ID 冲突问题与雪花算法讲解

作者头像
工藤学编程
发布2025-12-22 09:01:43
发布2025-12-22 09:01:43
00
举报

大家好,我是工藤学编程 🦉

一个正在努力学习的小博主,期待你的关注

实战代码系列最新文章😉

C++实现图书管理系统(Qt C++ GUI界面版)

SpringBoot实战系列🐷

【SpringBoot实战系列】Sharding-Jdbc实现分库分表到分布式ID生成器Snowflake自定义wrokId实战

环境搭建大集合

环境搭建大集合(持续更新)

分库分表

分库分表技术栈讲解-Sharding-JDBC


前情摘要:

1、数据库性能优化 2、分库分表之优缺点分析 3、分库分表之数据库分片分类 4、分库分表之策略 5、分库分表技术栈讲解-Sharding-JDBC

前言:在进行实操之前,我们还需要走最后一步,那就是了解分库分表下的 ID 冲突问题

(一) 分库分表下的ID冲突问题与分布式ID生成方案

传统自增ID的局限性

  • 单库环境:MySQL通过AUTO_INCREMENT自动生成唯一主键
  • 分库分表后:不同分片的自增ID会重复(如库1的订单表ID为1,库2的订单表ID也可能为1)

冲突示例

代码语言:javascript
复制
-- 分库前:单库自增ID保证唯一性
INSERT INTO orders(id, user_id) VALUES(NULL, 1001); -- 自动生成ID=1

-- 分库后:库1和库2各自生成ID=1
库1: INSERT INTO orders(id, user_id) VALUES(NULL, 1001); -- ID=1
库2: INSERT INTO orders(id, user_id) VALUES(NULL, 1002); -- ID=1(冲突)
分布式ID生成方案对比
1. 数据库自增ID(改进版)

原理:通过设置不同的自增步长和初始值,使各库生成不重复ID。 配置示例

代码语言:javascript
复制
-- 库1:从1开始,步长2(生成1,3,5...)
SET @@auto_increment_offset = 1; 
SET @@auto_increment_increment = 2;

-- 库2:从2开始,步长2(生成2,4,6...)
SET @@auto_increment_offset = 2; 
SET @@auto_increment_increment = 2;

优缺点: ✅ 实现简单,依赖数据库原生功能 ❌ 扩容困难(新增分片需重新规划步长) ❌ 主从切换可能导致ID重复 ❌ 性能瓶颈(单库生成ID)

2. UUID(通用唯一识别码)

原理:基于随机数或时间戳生成全局唯一字符串(如550e8400-e29b-41d4-a716-446655440000)。 Java实现

代码语言:javascript
复制
String uuid = UUID.randomUUID().toString();

优缺点: ✅ 无网络开销,性能高 ✅ 完全去中心化,生成逻辑简单 ❌ 无序字符串,不适合作为索引(影响查询性能) ❌ 存储空间占用大(36字节) ❌ 不具备趋势自增特性(不利于数据库分区分页)

3. Redis发号器

原理:利用Redis的原子操作INCRINCRBY生成唯一ID。 示例代码

代码语言:javascript
复制
// 获取下一个订单ID
Long orderId = redisTemplate.opsForValue().increment("order_id_generator", 1);

优缺点: ✅ 高性能(Redis单线程原子操作) ✅ 支持批量生成(减少网络调用) ❌ 依赖外部服务(Redis故障影响ID生成) ❌ 需维护Redis集群,增加系统复杂度

4. Snowflake雪花算法

原理:生成64位长整型ID,结构如下:

代码语言:javascript
复制
1位符号位 | 41位时间戳 | 5位数据中心ID | 5位机器ID | 12位序列号  
  • 时间戳:精确到毫秒级,保证生成的ID按时间趋势递增
  • 机器ID:确保不同服务器生成不同ID
  • 序列号:同一毫秒内生成的不同ID

优缺点: ✅ 高性能(本地生成,无网络开销) ✅ 趋势自增(有利于数据库索引优化) ✅ 可自定义位分配(适应不同业务场景) ❌ 依赖系统时钟(时钟回拨可能导致ID重复) ❌ 机器ID需提前规划(分布式环境下需唯一分配)

数据库号段模式(Leaf-Segment)

原理:从数据库批量获取ID号段,本地内存分配,减少数据库访问。 示例

  1. 数据库表存储当前号段的最大值(如max_id=1000
  2. 应用获取号段(如1-1000),本地自增生成ID
  3. 用完后再从数据库获取下一号段(如1001-2000

优缺点: ✅ 高性能(本地生成,仅号段用完时访问数据库) ✅ 不依赖时钟 ❌ 存在ID空洞(号段未用完时应用重启) ❌ 需数据库表支持

美团Leaf方案

特点:结合Snowflake和号段模式,提供两种ID生成方式:

  1. Leaf-Segment:数据库号段模式,适合对时钟敏感的业务
  2. Leaf-Snowflake:雪花算法,通过ZooKeeper分配机器ID
方案选型建议

方案

性能

唯一性

趋势自增

依赖外部服务

时钟敏感性

适用场景

数据库自增ID

✅(数据库)

小规模分库(<4个节点)

UUID

对ID格式无要求的场景

Redis发号器

中高

✅(Redis)

已有Redis集群的场景

Snowflake

高性能、分布式场景

数据库号段模式

中高

✅(数据库)

对时钟回拨敏感的业务

(二)雪花算法(Snowflake)详解

一、雪花算法的本质与起源

定义:由Twitter开源的分布式ID生成算法,通过64位长整型数字(long类型)生成全局唯一、趋势递增的ID。 核心优势

  • 高性能(本地生成,无网络开销)
  • 趋势递增(适合数据库索引优化)
  • 结构可解析(通过ID反推生成时间、机器等信息)
二、64位ID的结构拆解
代码语言:javascript
复制
1位符号位 | 41位时间戳 | 5位数据中心ID | 5位机器ID | 12位序列号  
  • 1位符号位:固定为0(保证生成正数)。
  • 41位时间戳
    • 精确到毫秒,可使用69年((2^41-1)/1000/60/60/24/365 ≈ 69年)。
    • 通常设置为“起始时间戳”(如2015-01-01)与当前时间的差值,避免负数。
  • 5位数据中心ID:最多支持32个数据中心(2^5=32)。
  • 5位机器ID:每个数据中心最多支持32台机器(2^5=32)。
  • 12位序列号:同一毫秒内最多生成4096个ID(2^12=4096)。
三、Java实现示例(含时钟回拨处理)
代码语言:javascript
复制
public class SnowflakeIdGenerator {
    // 起始时间戳(2021-01-01 00:00:00)
    private final long startTimestamp = 1609459200000L;
    
    // 各部分位数
    private final long dataCenterIdBits = 5L;  // 数据中心ID位数
    private final long workerIdBits = 5L;       // 机器ID位数
    private final long sequenceBits = 12L;     // 序列号位数
    
    // 最大取值计算
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);  // 31
    private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);  // 31
    private final long maxSequence = -1L ^ (-1L << sequenceBits);  // 4095
    
    // 位移偏移量
    private final long workerIdShift = sequenceBits;
    private final long dataCenterIdShift = sequenceBits + workerIdBits;
    private final long timestampShift = sequenceBits + workerIdBits + dataCenterIdBits;
    
    // 实例变量
    private long dataCenterId;  // 数据中心ID
    private long workerId;      // 机器ID
    private long sequence = 0L; // 序列号
    private long lastTimestamp = -1L; // 上次生成ID的时间戳
    
    // 构造函数(参数需提前规划分配)
    public SnowflakeIdGenerator(long dataCenterId, long workerId) {
        // 参数校验
        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
            throw new IllegalArgumentException(
                String.format("DataCenter ID can't be greater than %d or less than 0", maxDataCenterId));
        }
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(
                String.format("Worker ID can't be greater than %d or less than 0", maxWorkerId));
        }
        this.dataCenterId = dataCenterId;
        this.workerId = workerId;
    }
    
    // 同步生成ID(避免并发冲突)
    public synchronized long nextId() {
        long timestamp = currentTimeMillis();
        
        // 时钟回拨处理(核心坑点)
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                // 短时间回拨:等待至lastTimestamp后再生成
                try {
                    wait(offset);
                    timestamp = currentTimeMillis();
                    if (timestamp < lastTimestamp) {
                        throw new RuntimeException("Clock moved backwards. Refusing to generate id.");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            } else {
                // 长时间回拨:直接抛异常(需人工处理)
                throw new RuntimeException(
                    String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
            }
        }
        
        // 同一毫秒内
        if (timestamp == lastTimestamp) {
            // 序列号自增,达到上限则等待下一毫秒
            sequence = (sequence + 1) & maxSequence;
            if (sequence == 0) {
                timestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            // 新毫秒,序列号重置为0
            sequence = 0L;
        }
        
        lastTimestamp = timestamp;
        
        // 组合各部分生成ID
        return ((timestamp - startTimestamp) << timestampShift) |
               (dataCenterId << dataCenterIdShift) |
               (workerId << workerIdShift) |
               sequence;
    }
    
    // 获取当前时间戳
    private long currentTimeMillis() {
        return System.currentTimeMillis();
    }
    
    // 等待至下一毫秒
    private long waitNextMillis(long lastTimestamp) {
        long timestamp = currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = currentTimeMillis();
        }
        return timestamp;
    }
}
四、雪花算法的核心坑点与解决方案
坑一:分布式环境下workId重复

问题:不同机器分配相同workId,导致生成ID重复。 解决方案

  1. 人工分配:小规模集群手动规划(如数据中心ID=0,机器ID按机房机柜编号分配)。
  2. 自动分配
    • 通过ZooKeeper抢占节点(如在/snowflake/worker_ids下创建临时顺序节点,节点序号作为workId)。
    • 基于数据库表记录已分配的workId,获取时自增+1。
坑二:系统时钟回拨导致ID重复

问题场景

  • 手动修改服务器时间(如NTP时钟同步)。
  • 虚拟机暂停后恢复(CPU时间片调度导致时间回拨)。 解决方案
  1. 轻度回拨(<5ms)
    • 等待回拨时间后再生成ID(如代码示例中的wait(offset))。
  2. 重度回拨(>5ms)
    • 抛异常阻断业务(适合强一致性场景)。
    • 切换至备用ID生成方案(如UUID),并记录告警。
  3. 预防措施
    • 禁止生产环境手动修改系统时间。
    • 服务器开启NTP自动同步(避免大幅时间偏差)。
五、雪花算法的适用场景

适用场景

  1. 核心业务ID生成:订单号、用户ID、交易ID等(需趋势递增,便于数据库排序)。
  2. 高并发系统:如秒杀、抢购场景(高性能+无网络依赖)。
  3. 分布式微服务:跨节点ID唯一性要求高的场景。

不适用场景

  • 对ID安全性要求高的场景(ID结构可解析,可能泄露业务量等信息)。
  • 对时钟敏感的场景(如金融交易,时钟回拨可能引发严重问题)。
六、与其他ID方案的对比

方案

雪花算法

UUID

Redis发号器

数据库号段

唯一性

趋势递增

性能

高(本地计算)

高(无网络)

中(依赖网络)

中(批量获取)

依赖

系统时钟

Redis集群

数据库

时钟敏感

✅(回拨需处理)

总结

雪花算法通过“时间戳+机器标识+序列号”的结构,在分布式场景下实现了高性能、唯一且有序的ID生成。其核心挑战在于时钟回拨处理机器ID分配,生产环境中需结合业务特点制定针对性方案。对于追求高性能和ID有序性的场景,雪花算法是首选;若对时钟敏感或ID安全性要求高,则需考虑其他方案(如数据库号段或UUID)。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • (一) 分库分表下的ID冲突问题与分布式ID生成方案
    • 分布式ID生成方案对比
      • 1. 数据库自增ID(改进版)
      • 2. UUID(通用唯一识别码)
      • 3. Redis发号器
      • 4. Snowflake雪花算法
      • 数据库号段模式(Leaf-Segment)
      • 美团Leaf方案
    • 方案选型建议
  • (二)雪花算法(Snowflake)详解
    • 一、雪花算法的本质与起源
    • 二、64位ID的结构拆解
    • 三、Java实现示例(含时钟回拨处理)
    • 四、雪花算法的核心坑点与解决方案
    • 五、雪花算法的适用场景
    • 六、与其他ID方案的对比
    • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档