分布式ID生成的常见方案~

前言

大家好,我是田螺。

我们日常开发中,经常需要使用到分布式ID。我们系统一般都是分布式部署的,一些分布式锁、幂等、数据库的唯一键,都需要分布式ID。

今天田螺哥盘点一些常见的分布式唯一ID生成方案。

1. 数据库自增ID

原理:利用数据库自增字段(如MySQL的AUTO_INCREMENT)生成唯一ID

图片

优点:简单易用、ID有序、索引效率高缺点:单点故障、扩展性差(分库分表困难)
适用场景:单机或简单主从架构系统

代码示例

CREATE TABLE orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    order_data VARCHAR(255)
);

2. UUID

  • 原理:基于MAC地址、时间戳、随机数生成128位字符串

  • 优点:全局唯一、无需中心化服务

  • 缺点:无序导致索引效率低、存储空间大(36字符)

  • 适用场景:日志跟踪、非核心业务(如临时会话)

我们的项目中,有些伙伴为了简单方便,有时候会直接用它,如果业务性比较强的,就在它后缀拼接写个性化标记(业务标记)进来~

代码示例

import java.util.UUID;
String uuid = UUID.randomUUID().toString();

3. 雪花算法(Snowflake)

  • 原理:64位结构 = 时间戳(41位) + 机器ID(10位) + 序列号(12位)

    图片

  • 优点:高性能(单机每秒4万+)、趋势递增29

  • 缺点:依赖时钟同步(时钟回拨会导致重复)

  • 适用场景:分布式高并发系统(如电商订单)

其实,我们现在的系统,很多场景就是用雪花算法生成的,如流水号等等~

代码示例

public class Snowflake {
    private long machineId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public synchronized long nextId() {
        long timestamp = System.currentTimeMillis();
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨!");
        }
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & 4095; // 12位序列号
            if (sequence == 0) timestamp = tilNextMillis(lastTimestamp);
        } else {
            sequence = 0L;
        }
        lastTimestamp = timestamp;
        return (timestamp << 22) | (machineId << 12) | sequence;
    }
}

4. 数据库号段模式

  • 原理:批量获取ID段(如一次取1000个),减少数据库访问

  • 优点:降低数据库压力、可用性高(缓存号段)、速度快

  • 缺点:在服务器重启或故障转移等情况下,可能会导致ID的生成出现不连续的情况。

  • 适用场景:中等并发业务(如用户ID生成)

我们的一些客户号,当前是用号段模式生成的,然后拼一些业务标记

表结构:

CREATE TABLE id_segment (
    biz_tag VARCHAR(50) PRIMARY KEY,
    max_id BIGINT NOT NULL,
    step INT NOT NULL,
    version INT NOT NULL
);

5. Redis分布式ID

  • 原理:利用INCR原子操作生成递增ID

    图片

  • 优点:性能优于数据库、天然有序、高性能、可扩展性强

  • 缺点:依赖Redis可用性

  • 适用场景:按日生成的流水号(如订单号=日期+自增)

代码示例:

Jedis jedis = new Jedis("redis-host");
Long orderId = jedis.incr("order:20240526");

6.百度的uid-generator

优点:避免频繁生成、吞吐量提升至600万/秒
适用场景:超大规模分布式系统

基于Twitter的Snowflake算法进行改进,增加了更多的配置和灵活性。

与原始的snowflake算法不同在于,uid-generator支持自定义时间戳、工作机器ID和 序列号 等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。

代码示例:

import com.baidu.fsg.uid.UidGenerator;  
import com.baidu.fsg.uid.impl.CachedUidGenerator;  
  
public class UidGeneratorDemo {  
  
    public static void main(String[] args) {  
        // 创建一个UidGenerator实例  
        UidGenerator uidGenerator = new CachedUidGenerator();  
  
        // 初始化,这里只是一个简单的示例,实际使用时你可能需要根据你的业务场景进行更复杂的配置  
        // 例如,设置workerId、epoch等  
        // 注意:在多实例部署时,每个实例的workerId必须唯一  
        long workerId = 1L; // 示例ID,实际使用时需要保证每个实例的唯一性  
        long datacenterId = 1L; // 数据中心ID,示例  
        uidGenerator.init(workerId, datacenterId, null);  
  
        // 生成一个UID  
        long uid = uidGenerator.getUID();  
        System.out.println("Generated UID: " + uid);  
    }  
}

    7. 基于Zookeeper的顺序节点

    利用Zookeeper的顺序节点特性来生成全局唯一ID。

    图片

    优点:

    • 利用Zookeeper的集群特性保证高可用。

    • ID全局唯一。

    缺点:

    • 需要依赖Zookeeper集群。

    • 可能会受到Zookeeper性能的限制。

    • 并发竞争较大不适合用Zookeeper

    8. 数据库集群模式

    单库的数据库自增ID会存在单点问题,所以可以用数据库集群模式,去解决这个问题。数据库集群模式:通过多个数据库实例设置不同的起始值和步长来生成全局唯一的ID。

    图片

    数据库集群模式优点

    • 可以有效生成集群中的唯一ID。解决了单点的问题。

    • 降低ID生成数据库操作的负载。

    数据库集群模式缺点

    • 需要独立部署多个数据库实例,成本高。

    • 后期不方便扩展

    9. 美团(Leaf)

    Leaf是美团点评开源的分布式ID生成系统,包含基于数据库和基于Zookeeper的两种实现方式。

    以基于数据库的自增ID生成策略为例(数据库表结构):

    CREATE TABLE leaf_alloc (  
        biz_tag VARCHAR(128) NOT NULL COMMENT '业务key',  
        max_id BIGINT(20) NOT NULL COMMENT '当前已分配的最大id',  
        step INT(11) NOT NULL COMMENT '每次id的增长步长',  
        PRIMARY KEY (biz_tag)  
    ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;
    

    Java 实现:

    import java.sql.*;  
      
    public class LeafIdGenerator {  
      
        private static final String JDBC_URL = "jdbc:mysql://localhost:3306/your_database?useSSL=false&serverTimezone=UTC";  
        private static final String USERNAME = "your_username";  
        private static final String PASSWORD = "your_password";  
      
        private static final String UPDATE_SQL = "UPDATE leaf_alloc SET max_id = max_id + ? WHERE biz_tag = ?";  
        private static final String SELECT_SQL = "SELECT max_id FROM leaf_alloc WHERE biz_tag = ? FOR UPDATE";  
      
        public synchronized long getId(String bizTag) throws SQLException {  
            Connection conn = null;  
            PreparedStatement updateStmt = null;  
            PreparedStatement selectStmt = null;  
            ResultSet rs = null;  
      
            try {  
                conn = DriverManager.getConnection(JDBC_URL, USERNAME, PASSWORD);  
                selectStmt = conn.prepareStatement(SELECT_SQL);  
                selectStmt.setString(1, bizTag);  
                rs = selectStmt.executeQuery();  
      
                if (rs.next()) {  
                    long maxId = rs.getLong("max_id");  
                    int step = 1000; // 假设步长为1000,你可以从数据库中读取这个值  
      
                    // 假设这里只是简单演示,不检查是否超过max_id + step是否溢出  
                    updateStmt = conn.prepareStatement(UPDATE_SQL);  
                    updateStmt.setInt(1, step);  
                    updateStmt.setString(2, bizTag);  
                    updateStmt.executeUpdate();  
      
                    // 返回ID区间中的一个ID,这里简单返回maxId(实际应用中可能需要更复杂的策略)  
                    return maxId;  
                } else {  
                    // 如果没有找到对应的bizTag,则需要初始化  
                    // ... 初始化代码省略 ...  
                    throw new RuntimeException("BizTag not found: " + bizTag);  
                }  
            } finally {  
                // 关闭资源,省略了异常处理  
                if (rs != null) rs.close();  
                if (selectStmt != null) selectStmt.close();  
                if (updateStmt != null) updateStmt.close();  
                if (conn != null) conn.close();  
            }  
        }  
      
        public static void main(String[] args) {  
            LeafIdGenerator generator = new LeafIdGenerator();  
            try {  
                long id = generator.getId("test-biz-tag");  
                System.out.println("Generated ID: " + id);  
            } catch (SQLException e) {  
                e.printStackTrace();  
            }  
        }  
    }
    

    优点

    • 结合了数据库和Zookeeper的优点,提供了高可用和高性能的ID生成服务。缺点:

    • 就是时钟回拨问题、复杂性高。

    10. 滴滴(Tinyid)

    Tinyid是滴滴开源的轻量级分布式ID生成系统,它是基于号段模式原理实现的与Leaf如出一辙,每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]

    图片

    以下是一个简化的Tinyid,服务端的伪代码:

    // 假设我们有一个ID生成器,这里用AtomicLong模拟  
    import java.util.concurrent.atomic.AtomicLong;  
      
    public class TinyidService {  
        private AtomicLong idGenerator = new AtomicLong(0);  
      
        // 模拟的ID生成方法  
        public synchronized long generateId() {  
            return idGenerator.incrementAndGet();  
        }  
      
        // 这里应该是RESTful API的实现,但为简化起见,我们省略了HTTP部分  
        // 客户端应该通过HTTP请求调用此方法  
        public long getIdOverHttp() {  
            return generateId();  
        }  
    }
    

    客户端(Java示例)

    import okhttp3.*;  
      
    public class TinyidClient {  
        private static final String TINYID_SERVICE_URL = "http://localhost:8080/tinyid/generate";  
      
        public static void main(String[] args) {  
            OkHttpClient client = new OkHttpClient();  
      
            Request request = new Request.Builder()  
                    .url(TINYID_SERVICE_URL)  
                    .build();  
      
            client.newCall(request).enqueue(new Callback() {  
                @Override  
                public void onFailure(Call call, IOException e) {  
                    e.printStackTrace();  
                }  
      
                @Override  
                public void onResponse(Call call, Response response) throws IOException {  
                    if (!response.isSuccessful()) {  
                        throw new IOException("Unexpected code " + response);  
                    } else {  
                        // 假设服务端返回的是纯文本格式的ID  
                        String responseBody = response.body().string();  
                        long id = Long.parseLong(responseBody);  
                        System.out.println("Generated ID: " + id);  
                    }  
                }  
            });  
        }  
    }
    
    • 优缺点:简单、轻量级,但性能可能不如其他方案。

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值