分库分表下的 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)。

相关推荐
松涛和鸣10 小时前
72、IMX6ULL驱动实战:设备树(DTS/DTB)+ GPIO子系统+Platform总线
linux·服务器·arm开发·数据库·单片机
likangbinlxa10 小时前
【Oracle11g SQL详解】UPDATE 和 DELETE 操作的正确使用
数据库·sql
r i c k11 小时前
数据库系统学习笔记
数据库·笔记·学习
野犬寒鸦11 小时前
从零起步学习JVM || 第一章:类加载器与双亲委派机制模型详解
java·jvm·数据库·后端·学习
IvorySQL12 小时前
PostgreSQL 分区表的 ALTER TABLE 语句执行机制解析
数据库·postgresql·开源
·云扬·12 小时前
MySQL 8.0 Redo Log 归档与禁用实战指南
android·数据库·mysql
IT邦德12 小时前
Oracle 26ai DataGuard 搭建(RAC到单机)
数据库·oracle
惊讶的猫12 小时前
redis分片集群
数据库·redis·缓存·分片集群·海量数据存储·高并发写
不爱缺氧i12 小时前
完全卸载MariaDB
数据库·mariadb
纤纡.13 小时前
Linux中SQL 从基础到进阶:五大分类详解与表结构操作(ALTER/DROP)全攻略
linux·数据库·sql