问:分库分表场景下ID的生成策略?

时间:2024-10-29 07:11:37

在数据库架构设计中,分库分表是一种常见的优化策略,用于解决单表数据量过大导致的查询性能下降问题。然而,分库分表后如何处理主键ID成为了一个关键问题。因为每个表如果都从1开始累加,会导致主键冲突,因此需要生成全局唯一的ID来支持。以下是几种常见的ID生成方案。

UUID(Universally Unique Identifier)

UUID是一种全局唯一标识符,基于算法生成,不依赖于数据库。UUID的长度为128位,通常以36位长度的十六进制数字字符串表示(包括四个连字号)。UUID的优点在于全局唯一性,无需担心主键冲突的问题,适用于分布式系统或需要跨多个数据库实例进行数据同步的场景。然而,UUID作为主键存在以下缺点:

  • 存储空间大:UUID长度为36位,占用存储空间较大。
  • 无序性:UUID是无序的,新增数据时如果采用btree索引,每次新增一条数据都需要重新排序,影响性能。
  • 可读性差:UUID的随机性导致其不易读懂。

使用示例

import java.util.UUID;

public class ExampleEntity {
    private String id;
    private String name;

    public ExampleEntity(String name) {
        this.id = UUID.randomUUID().toString();
        this.name = name;
    }

    // 省略 getter 和 setter 方法
}

数据库自增ID

数据库自增ID是一种简单且常用的主键生成方式。MySQL使用auto_increment实现,Oracle则使用序列。然而,分库分表后,如果每个表都从1开始累加,会导致主键冲突。为了避免这种情况,可以采用以下两种方式:

  1. 单独创建主键表维护唯一标识:通过执行SQL获取唯一标识,然后添加到某个分表中。
  2. 设置不同步长:在分布式环境中,设置不同数据库的自增ID步长,确保全局唯一性。例如,设置实例1步长为1,实例2步长为2。

使用示例

-- 创建主键表
CREATE TABLE id_generator (
    id BIGINT AUTO_INCREMENT PRIMARY KEY
);

-- 获取唯一ID
INSERT INTO id_generator () VALUES ();
SELECT LAST_INSERT_ID();

Redis生成ID

Redis是一个高性能的键值存储系统,支持原子性操作如INCRINCRBY,因此可以用来生成全局唯一的ID。Redis生成ID的优点在于不依赖于数据库,性能优越,且支持高并发场景。然而,引入新的组件会增加系统的复杂度,可用性也会受到影响。

使用示例

import redis.clients.jedis.Jedis;

public class RedisIdGenerator {
    private Jedis jedis;
    private String key;

    public RedisIdGenerator(String host, int port, String key) {
        this.jedis = new Jedis(host, port);
        this.key = key;
    }

    public long generateId() {
        return jedis.incr(key);
    }

    // 省略其他方法
}

Snowflake算法

Snowflake算法是由Twitter开源的一种分布式ID生成算法,它将64位的long型ID分为四个部分:

  • 符号位(1位):始终为0,表示正数。
  • 时间戳(41位):精确到毫秒级,支持69年的唯一性。
  • 工作机器ID(10位):分为数据中心ID和工作机器ID各5位,支持最多1024个节点。
  • 序列号(12位):同一毫秒内生成的不同ID,支持每个节点每毫秒产生4096个唯一ID。

Snowflake算法生成的ID有序且唯一,适用于分布式系统,但依赖于机器时钟,如果时钟回拨会导致ID重复。

使用示例

import java.util.concurrent.atomic.AtomicLong;

public class SnowflakeIdGenerator {
    private final long twepoch = 1288834974657L; // 起始时间戳

    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private final long sequenceBits = 12L;

    private final long workerIdShift = sequenceBits;
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    private long workerId;
    private long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    public synchronized long nextId() {
        long timestamp = timeGen();

        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    protected long timeGen() {
        return System.currentTimeMillis();
    }

    // 省略其他方法
}

美团Leaf分布式ID生成系统

Leaf是美团点评开源的分布式ID生成系统,支持多种ID生成策略,包括UUID、Snowflake、Segment等。Leaf的设计理念是简单、高效、安全和可扩展,旨在解决分布式系统中的ID生成问题。Leaf生成的ID具有全局唯一性、趋势递增、单调递增、信息安全等特点,但需要依赖关系数据库、Zookeeper等中间件。

使用示例

import com.meituan.leaf.core.LeafServer;
import com.meituan.leaf.core.LeafServerApplication;

public class LeafServerExample {
    public static void main(String[] args) {
        LeafServerApplication leafServerApplication = new LeafServerApplication();
        leafServerApplication.setZookeeperAddress("127.0.0.1:2181");
        leafServerApplication.setDatabaseUrl("jdbc:mysql://localhost:3306/leaf");
        leafServerApplication.setDatabaseUser("root");
        leafServerApplication.setDatabasePassword("password");

        LeafServer leafServer = new LeafServer(leafServerApplication);
        leafServer.start();
    }
}

总结

方案 描述 优点 缺点 适用场景
UUID 全局唯一标识符 全局唯一,无需额外中间件 性能较低,占用存储空间较大 需要全局唯一标识符但不关心性能和存储空间的场景
数据库自增ID 数据库自带的自增ID 简单易用,ID有序递增 分库分表后需要特殊处理,存在单点故障风险 单库单表环境
Redis生成ID 利用Redis的原子操作生成ID 性能优越,ID有序递增 增加了系统复杂度,需要依赖Redis中间件 对性能要求较高,且可以接受增加系统复杂度的场景
Snowflake算法 基于Twitter的分布式ID生成算法 性能优越,ID唯一且有序,可自定义ID长度和前缀 需要依赖额外的中间件,ID长度受限 大规模分布式系统,需要全局唯一且有序的ID
美团Leaf 美团开源的分布式ID生成系统,支持多种ID生成方式,如号段模式、Snowflake模式等 性能优越,支持多种ID生成方式,灵活性高,ID唯一且有序 需要依赖额外的中间件,配置和运维相对复杂 大规模分布式系统,需要全局唯一且有序的ID,且对灵活性有要求的场景