返回

大厂常用的分布式ID生成方案:兼具性能与可靠性

后端

分库分表时代的分布式ID生成之谜

缘起:数据激增带来的挑战

随着企业数字化转型深入,数据量持续攀升,单一数据库早已无法承载海量数据的存储和处理需求。分库分表技术应运而生,将数据分布到多个数据库或表中,缓解单机压力。

然而,分库分表后,一个至关重要的难题随之而来:如何为分布在不同数据库中的数据项生成全局唯一的ID?传统ID生成方法在分库分表环境下将导致ID重复,破坏数据完整性和一致性。

分布式ID生成方案:大厂的智慧结晶

分布式ID生成旨在为分布式系统中的实体分配全局唯一的ID,以保证数据的唯一性。业界为此提出了多种方案,各有千秋,适用于不同的业务场景。本文将重点探讨大厂广泛采用的四种方案:

1. 雪花算法:高性能的雪花纷飞

雪花算法由 Twitter 提出,因其高性能而闻名。它将 ID 划分为三个部分:

  • 时间戳(41 位):表示 ID 生成的时间戳,单位为毫秒。
  • 机器 ID(10 位):表示生成 ID 的机器编号。
  • 序列号(12 位):同一毫秒内生成 ID 的序号,从 0 开始递增。

雪花算法可快速生成大量唯一 ID,且支持按时间或机器 ID 排序。

代码示例:

import java.util.concurrent.atomic.AtomicInteger;

public class SnowflakeIdGenerator {
    private static final long EPOCH = 1420041600000L; // 2015-01-01 00:00:00

    private final long machineId; // 机器 ID,0~1023
    private final long sequenceMask = 0x3FF; // 序列号掩码,0~4095
    private final long timestampShift = 41; // 时间戳左移位数,41 位

    private long sequence = 0L; // 序列号
    private long lastTimestamp = -1L; // 上一次生成 ID 的时间戳

    public SnowflakeIdGenerator(long machineId) {
        if (machineId < 0 || machineId > 1023) {
            throw new IllegalArgumentException("Machine ID must be between 0 and 1023.");
        }
        this.machineId = machineId;
    }

    public long generateId() {
        long timestamp = System.currentTimeMillis();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refuse to generate ID.");
        }
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        return ((timestamp - EPOCH) << timestampShift) | (machineId << 10) | sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }
        return timestamp;
    }
}

2. UUID:无序的独特标识

UUID(Universally Unique Identifier)基于 128 位随机数生成,具有绝对唯一性。它不包含时间信息或排序功能,但生成速度较快。

代码示例:

import java.util.UUID;

public class UUIDIdGenerator {
    public String generateId() {
        return UUID.randomUUID().toString();
    }
}

3. Redis:原子性递增的便捷

Redis 可通过原子性递增操作生成分布式 ID。它在 Redis 中维护一个名为 ID_GENERATOR 的键,每次需要生成 ID 时,对该键进行原子性递增。

代码示例:

import redis.clients.jedis.Jedis;

public class RedisIdGenerator {
    private final Jedis jedis;

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

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

4. ZooKeeper:可靠有序的序列号

ZooKeeper 是一个分布式协调服务,可通过节点的顺序号生成分布式 ID。在 ZooKeeper 中创建一个名为 ID_GENERATOR 的序列节点,每次需要生成 ID 时,从该节点获取一个顺序号。

代码示例:

import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.ZooKeeper;

public class ZooKeeperIdGenerator {
    private final ZooKeeper zooKeeper;

    public ZooKeeperIdGenerator(String connectString) throws KeeperException, InterruptedException {
        zooKeeper = new ZooKeeper(connectString, 10000, null);
    }

    public long generateId() throws KeeperException, InterruptedException {
        return zooKeeper.create("/ID_GENERATOR/id", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL).getId();
    }
}

方案选择:根据需求权衡取舍

选择合适的分布式 ID 生成方案需要权衡以下因素:

  • 性能: ID 生成吞吐量和延迟。
  • 可靠性: ID 生成服务的可用性和稳定性。
  • 唯一性: ID 是否真正全局唯一。
  • 有序性: ID 是否可以按时间或机器 ID 排序。

根据这些因素,可以将上述方案归纳为以下场景:

  • 极致性能: 雪花算法。
  • 绝对唯一性: UUID。
  • 高可靠性和可扩展性: ZooKeeper。
  • 顺序性要求: Redis。

总结:分库分表的ID生成之道

分布式 ID 生成是分库分表系统中至关重要的技术,保障数据的唯一性和一致性。大厂广泛采用的雪花算法、UUID、Redis 和 ZooKeeper 等方案各具特色,企业可根据自身业务需求权衡选择。通过合理的设计和应用,企业可以构建高效、可靠的分布式 ID 生成系统,为业务发展保驾护航。

常见问题解答

1. 分库分表后,为什么不能直接使用自增主键?

因为自增主键在分布式环境下会导致 ID 冲突,破坏数据完整性。

2. 雪花算法的 ID 是否真的全局唯一?

理论上是全局唯一的,但当多个机器同时生成 ID 时,存在极小概率出现重复。

3. UUID 是否可以按时间或机器 ID 排序?

无法排序,因为 UUID 是随机生成的。

4. Redis 的 ID 生成是否会受到故障影响?

是的,当 Redis 故障时,ID 生成将受影响。

5. ZooKeeper 的 ID 生成效率如何?

较低,因为涉及分布式协调操作。