• 数据库主键设计



    前言

    数据库主键的设计是数据库架构中的一个重要环节,不同的主键生成策略适用于不同的场景和需求


    以下是几种常见的主键设计方法及其优缺点比较:

    1. 自增ID(Auto-Increment)

    优点:

    • 实现简单,数据库自动管理,无需开发者介入。
    • 递增的特性使得数据插入速度快,因为插入总是发生在索引的末尾。
    • 易于理解和使用,便于查询和排序。

    缺点:

    • 分布式系统中难以保证全局唯一,因为每个节点的计数器独立增长。
    • 数据泄露风险,自增ID容易暴露数据库的规模和增长速度。
    • 如果发生大量删除操作,可能导致主键ID不连续,影响美观但不影响功能。

    2. GUID (Globally Unique Identifier)

    优点:

    • 全球唯一,无论在任何系统、任何地点生成,都能保证唯一性。
    • 无需依赖数据库,可以在客户端生成,适合分布式系统。
    • 支持提前生成ID,有利于并行处理和离线操作。

    缺点:

    • 长度较大(通常为32字符),占用更多的存储空间和索引空间。
    • 无序的特性可能导致索引碎片,降低插入性能。
    • 不易读,不便于人工识别和调试。

    3. 雪花算法(Snowflake)

    雪花算法(Snowflake Algorithm)是一种用于生成唯一ID的算法,最初由Twitter公司开发。它是为了解决分布式系统中生成全局唯一ID的需求而设计的。在分布式系统中,如果不同节点生成的ID可能会发生冲突,这就需要一种机制来保证生成的ID在整个系统中唯一。

    雪花算法的设计考虑了以下几个因素:

    1. 时间戳(Timestamp):使用当前时间来确保生成的ID是递增的,这样可以保证生成的ID是有序的。
    2. 机器ID(Machine ID):将机器的唯一标识(比如机器的MAC地址)作为一部分ID,确保不同机器生成的ID不会冲突。
    3. 序列号(Sequence Number):用来解决同一毫秒内生成多个ID时的冲突问题。

    Java中如何使用雪花算法来设计数据库主键呢?下面是一个简单的示例:

    public class SnowflakeIdGenerator {
        // 定义机器ID,可以通过配置文件或其他方式设置
        private long machineId;
        // 定义序列号
        private long sequence = 0L;
        // 定义初始时间戳
        private long twepoch = 1622874000000L; // 2021-06-05 00:00:00
    
        // 定义各部分占位数
        private long machineIdBits = 5L;
        private long maxMachineId = -1L ^ (-1L << machineIdBits);
        private long sequenceBits = 12L;
        private long sequenceMask = -1L ^ (-1L << sequenceBits);
    
        // 定义机器ID左移位数
        private long machineIdShift = sequenceBits;
        // 定义时间戳左移位数
        private long timestampLeftShift = sequenceBits + machineIdBits;
    
        // 上次生成ID的时间戳
        private long lastTimestamp = -1L;
    
        public SnowflakeIdGenerator(long machineId) {
            if (machineId > maxMachineId || machineId < 0) {
                throw new IllegalArgumentException("Machine ID can't be greater than " + maxMachineId + " or less than 0");
            }
            this.machineId = machineId;
        }
    
        public synchronized long nextId() {
            long timestamp = timeGen();
    
            if (timestamp < lastTimestamp) {
                throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
            }
    
            if (lastTimestamp == timestamp) {
                sequence = (sequence + 1) & sequenceMask;
                if (sequence == 0) {
                    // 当同一毫秒内的序列号超过上限时,等待下一毫秒
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0L;
            }
    
            lastTimestamp = timestamp;
    
            return ((timestamp - twepoch) << timestampLeftShift) | (machineId << machineIdShift) | sequence;
        }
    
        private long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
        private long timeGen() {
            return System.currentTimeMillis();
        }
    
        public static void main(String[] args) {
            SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1); // 传入机器ID
            for (int i = 0; i < 10; i++) {
                System.out.println(idGenerator.nextId());
            }
        }
    }
    

    在这个示例中,我们通过nextId()方法来生成雪花算法生成的唯一ID。首先,我们需要设置一个机器ID,确保不同的机器有不同的ID。然后,调用nextId()方法即可生成一个唯一的ID,这个ID包含了时间戳、机器ID和序列号三部分。最后,我们可以将生成的ID作为数据库表的主键。

    值得注意的是,雪花算法生成的ID是趋势递增的,因此在数据库中使用时可能会带来一定的优势,比如辅助索引的性能优化。但也要注意在高并发情况下可能出现的一些问题,比如时钟回拨等。

    优点:

    • 结合了自增ID和GUID的优点,生成的ID是趋势递增的,且全局唯一。
    • 高性能,适用于分布式环境,能够按需分配workerId和数据中心id,保证唯一性。
    • ID较短(一般为64位),相比GUID节省存储空间。
    • 有序性有助于索引优化。

    缺点:

    • 需要一个中心节点(或者多个,但需要协调)来生成ID,有一定的运维成本。
    • 时钟回拨问题可能会影响ID的生成,需要特殊处理。

    然而,雪花算法依赖于时间戳,因此时钟回拨(clock rollback)会对其造成问题。

    处理时钟回拨的方法

    1. 简单等待

    当检测到时钟回拨时,直接等待直到时间回到正确的时间。这是最简单的处理方式,但会导致 ID 生成暂停一段时间。

    public class SnowflakeIdGenerator {
        private long lastTimestamp = -1L;
    
        public synchronized long nextId() {
            long timestamp = timeGen();
    
            // 如果当前时间小于上一次生成ID的时间戳,说明系统时钟回拨
            if (timestamp < lastTimestamp) {
                // 等待直到时钟追上
                while (timestamp < lastTimestamp) {
                    timestamp = timeGen();
                }
            }
    
            lastTimestamp = timestamp;
            return generateId(timestamp);
        }
    
        private long timeGen() {
            return System.currentTimeMillis();
        }
    
        private long generateId(long timestamp) {
            // 生成ID的逻辑
            return timestamp;
        }
    }
    
    2. 配置时钟回拨安全窗口

    允许一定范围内的时钟回拨,在这个范围内继续生成 ID,但如果超出这个范围则抛出异常或采取其他措施。

    public class SnowflakeIdGenerator {
        private long lastTimestamp = -1L;
        private static final long MAX_BACKWARD_MS = 5L; // 允许的最大时钟回拨时间
    
        public synchronized long nextId() {
            long timestamp = timeGen();
    
            if (timestamp < lastTimestamp) {
                long offset = lastTimestamp - timestamp;
                if (offset <= MAX_BACKWARD_MS) {
                    // 等待,直到时钟追上
                    try {
                        Thread.sleep(offset + 1);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                        throw new RuntimeException("Clock moved backwards. Refusing to generate id");
                    }
                } else {
                    throw new RuntimeException("Clock moved backwards. Refusing to generate id");
                }
            }
    
            lastTimestamp = timestamp;
            return generateId(timestamp);
        }
    
        private long timeGen() {
            return System.currentTimeMillis();
        }
    
        private long generateId(long timestamp) {
            // 生成ID的逻辑
            return timestamp;
        }
    }
    
    3. 使用不同的机器 ID

    在分布式系统中,每台机器有唯一的机器 ID。当检测到时钟回拨时,改变机器 ID 来避免冲突。这种方法需要协调机器 ID 的分配。

    public class SnowflakeIdGenerator {
        private long lastTimestamp = -1L;
        private long machineId;
        private static final long MAX_MACHINE_ID = 1023L;
    
        public SnowflakeIdGenerator(long machineId) {
            if (machineId < 0 || machineId > MAX_MACHINE_ID) {
                throw new IllegalArgumentException("Machine ID out of range");
            }
            this.machineId = machineId;
        }
    
        public synchronized long nextId() {
            long timestamp = timeGen();
    
            if (timestamp < lastTimestamp) {
                machineId = (machineId + 1) & MAX_MACHINE_ID;
                if (machineId == 0) {
                    // 如果机器ID回到0,说明时钟回拨过大,拒绝生成ID
                    throw new RuntimeException("Clock moved backwards. Refusing to generate id");
                }
                timestamp = timeGen();
            }
    
            lastTimestamp = timestamp;
            return generateId(timestamp, machineId);
        }
    
        private long timeGen() {
            return System.currentTimeMillis();
        }
    
        private long generateId(long timestamp, long machineId) {
            // 生成ID的逻辑,包含时间戳和机器ID
            return (timestamp << 22) | (machineId << 12);
        }
    }
    

    小结

    1. 简单等待:当检测到时钟回拨时,等待直到时钟恢复到正确时间。这种方法简单但会导致 ID 生成暂停。
    2. 时钟回拨安全窗口:允许一定范围内的时钟回拨,如果超出这个范围则抛出异常或采取其他措施。
    3. 不同的机器 ID:当检测到时钟回拨时,改变机器 ID 来避免冲突。这种方法需要协调机器 ID 的分配。

    稳定的雪花算法实现方案

    以下是一个经过优化的方案,涵盖时钟回拨问题、分布式系统中的唯一性问题和高可用性问题

    1. 机器 ID 和数据中心 ID:通过配置不同的机器 ID 和数据中心 ID 来确保分布式系统中的唯一性。
    2. 时钟回拨处理:使用递增序列和缓存的时间戳来处理时钟回拨问题。
    3. 高可用性:结合 Redis 或数据库来生成分布式唯一 ID。

    示例实现

    1. 定义雪花算法类
    public class SnowflakeIdGenerator {
        private static final long EPOCH = 1609459200000L; // 自定义纪元时间(2021-01-01)
        private static final long DATA_CENTER_ID_BITS = 5L;
        private static final long MACHINE_ID_BITS = 5L;
        private static final long SEQUENCE_BITS = 12L;
    
        private static final long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);
        private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
        private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
    
        private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;
        private static final long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
        private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS + DATA_CENTER_ID_BITS;
    
        private final long dataCenterId;
        private final long machineId;
        private long sequence = 0L;
        private long lastTimestamp = -1L;
    
        public SnowflakeIdGenerator(long dataCenterId, long machineId) {
            if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
                throw new IllegalArgumentException(String.format("DataCenter ID can't be greater than %d or less than 0", MAX_DATA_CENTER_ID));
            }
            if (machineId > MAX_MACHINE_ID || machineId < 0) {
                throw new IllegalArgumentException(String.format("Machine ID can't be greater than %d or less than 0", MAX_MACHINE_ID));
            }
            this.dataCenterId = dataCenterId;
            this.machineId = machineId;
        }
    
        public synchronized long nextId() {
            long timestamp = timeGen();
    
            if (timestamp < lastTimestamp) {
                throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
            }
    
            if (timestamp == lastTimestamp) {
                sequence = (sequence + 1) & MAX_SEQUENCE;
                if (sequence == 0) {
                    timestamp = tilNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0L;
            }
    
            lastTimestamp = timestamp;
    
            return ((timestamp - EPOCH) << TIMESTAMP_SHIFT)
                    | (dataCenterId << DATA_CENTER_ID_SHIFT)
                    | (machineId << MACHINE_ID_SHIFT)
                    | sequence;
        }
    
        private long tilNextMillis(long lastTimestamp) {
            long timestamp = timeGen();
            while (timestamp <= lastTimestamp) {
                timestamp = timeGen();
            }
            return timestamp;
        }
    
        private long timeGen() {
            return System.currentTimeMillis();
        }
    }
    
    2. 使用 Redis 或数据库实现分布式唯一 ID

    为了进一步提高高可用性和唯一性,可以结合 Redis 或数据库实现分布式唯一 ID 生成。这里是一个使用 Redis 的示例:

    import redis.clients.jedis.Jedis;
    
    public class DistributedIdGenerator {
        private final SnowflakeIdGenerator snowflakeIdGenerator;
        private final Jedis jedis;
    
        public DistributedIdGenerator(long dataCenterId, long machineId, String redisHost, int redisPort) {
            this.snowflakeIdGenerator = new SnowflakeIdGenerator(dataCenterId, machineId);
            this.jedis = new Jedis(redisHost, redisPort);
        }
    
        public long nextId() {
            long id = snowflakeIdGenerator.nextId();
            String key = "snowflake:" + id;
            while (jedis.exists(key)) {
                id = snowflakeIdGenerator.nextId();
                key = "snowflake:" + id;
            }
            jedis.setex(key, 3600, "1"); // 设置过期时间,避免长期存储
            return id;
        }
    }
    

    解释

    1. 基本雪花算法

      • EPOCH:自定义的纪元时间。
      • DATA_CENTER_ID_BITSMACHINE_ID_BITSSEQUENCE_BITS:数据中心 ID、机器 ID 和序列号的位数。
      • nextId 方法:生成唯一 ID,并处理时钟回拨问题。
    2. 分布式唯一 ID

      • 使用 Redis 确保 ID 唯一性:在生成 ID 后,将其存储在 Redis 中,检查是否重复。
      • jedis.setex(key, 3600, "1"):使用带过期时间的键来避免长期存储。
    3. 时钟回拨处理

      • 当检测到时钟回拨时,抛出异常或等待时间前进。
      • 使用 tilNextMillis 方法等待直到时间前进。

    小结

    这种方案结合了雪花算法的高性能和 Redis 的分布式存储能力,解决了时钟回拨问题,并确保在分布式环境下生成唯一 ID。通过这些措施,可以实现一个稳定、高效的分布式唯一 ID 生成系统。

    其他方法

    • 复合主键:结合多个字段作为主键,适用于表中没有自然唯一标识符的场景。但增加了查询和维护的复杂性。
    • 业务相关ID:如订单号,易于理解且与业务紧密相关,但可能需要额外的逻辑来保证唯一性,且扩展性较差。

    总结

    选择哪种主键生成策略取决于具体的应用场景:

    • 对于单体应用或简单的分布式系统,自增ID可能是最简单高效的选择。
    • 在分布式系统中,尤其是跨多个数据中心时,雪花算法因其高性能和全局唯一性成为优选。
    • 当全局唯一性是首要考虑因素,且对存储空间不太敏感时,GUID是合适的选择。
    • 具体场景下,也可以根据业务需求考虑复合主键或业务相关ID的方案。
  • 相关阅读:
    854. 相似度为 K 的字符串(每日一难phase2--day20)
    harbor企业级镜像仓库搭建
    ssm基于微信小程序的医学健康管理系统
    前端base64转文件输出
    机器学习常规操作流程(代码解读)
    vue3 + element plus实现侧边栏
    Spark 安装与启动
    数据结构与算法9-排序算法:选择排序、冒泡排序、快速排序
    Android程序设计之学生考勤管理系统
    【虚拟语气练习题】对过去的虚拟
  • 原文地址:https://blog.csdn.net/Bruce__taotao/article/details/139483788