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

大家好,我是工藤学编程 🦉 一个正在努力学习的小博主,期待你的关注
实战代码系列最新文章😉 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)

冲突示例

sql 复制代码
-- 分库前:单库自增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。
配置示例

sql 复制代码
-- 库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实现

java 复制代码
String uuid = UUID.randomUUID().toString();

优缺点

✅ 无网络开销,性能高

✅ 完全去中心化,生成逻辑简单

❌ 无序字符串,不适合作为索引(影响查询性能)

❌ 存储空间占用大(36字节)

❌ 不具备趋势自增特性(不利于数据库分区分页)

3. Redis发号器

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

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

优缺点

✅ 高性能(Redis单线程原子操作)

✅ 支持批量生成(减少网络调用)

❌ 依赖外部服务(Redis故障影响ID生成)

❌ 需维护Redis集群,增加系统复杂度

4. Snowflake雪花算法

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

复制代码
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的结构拆解
复制代码
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实现示例(含时钟回拨处理)
java 复制代码
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)。

相关推荐
qq_49244844611 分钟前
Dynatrace,rancher监测接口,根据接口查找services,根据service查询cluster和namespace
数据库
越甲八千23 分钟前
pyqt 简单条码系统
数据库·microsoft·pyqt
isNotNullX37 分钟前
ETL连接器好用吗?如何实现ETL连接?
大数据·数据库·数据仓库·信息可视化·etl
Databend38 分钟前
利用 Graviton 和 Spot 实例打造 Databend 高性能数据平台
数据库
钟琛......1 小时前
Java事务失效(面试题)的常见场景
java·数据库·spring
uwvwko1 小时前
数据结构学习——树的储存结构
数据库·学习·算法·
熙客2 小时前
MongoDB:索引
数据库·mongodb
孟猛20232 小时前
Unix ODBC和Mysql ODBC
数据库
代码or搬砖2 小时前
Spring JDBC配置与讲解
java·数据库·spring
白总Server2 小时前
轻量化分布式AGI架构:基于区块链构建终端神经元节点的互联网智脑
分布式·microsoft·中间件·架构·区块链·github·agi