分布式唯一ID

Posted by mjTree on April 7, 2024

更新于:2024-04-07 20:00

一、分布式ID

分布式ID(Distributed ID)是在分布式系统中用于唯一标识和区分不同实体或数据的一种标识符。在分布式系统中,由于系统的分布性和并发性,通常需要为每个数据记录或实体分配一个唯一的标识符,以确保数据的唯一性和一致性。

二、常见方案

1、uuid

uuid的底层是一组32位数的16进制数字构成,生成过程要用到mac地址、时间戳、芯片ID码和随机数等,理论上很难用完。业务场景:可用于普通资源的兑换码使用,比如说网站做一些小活动送优惠卷了,可以当作卡券兑换码之类的。也可以表示分布式下机器的节点id、消息的id、请求日志追踪记录等等。

import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyUUID {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 10, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20));
        for (int i = 0; i < 20; i++) {
            if (poolExecutor.getActiveCount() <= poolExecutor.getMaximumPoolSize()) {
                poolExecutor.execute(() -> {
                    try {
                        System.out.println(getUUID());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
            }
        }
        TimeUnit.SECONDS.sleep(3);
        poolExecutor.shutdown();
    }

    public static String getUUID() {
        return UUID.randomUUID().toString().replace("-","");
    }
}

2、database

涉及到数据库的方案,一般不是很推荐。系统都开始用分布式系统了,数据库肯定也集群了,再去使用数据库帮忙有点本末倒置了。而且通过java服务代码调用mysql都是借用ORM框架和springboot框架帮忙,不直接代码操作数据库表记录读取id。

3、redis

目前公司通过Redis自增命令与时间戳相结合来提供线程安全的分布式唯一标识符生成策略。原理就是通过redis的原子性自增命令(单线程)+时间戳+填充的数字(高位补0、自己填一部分、随机数)组成一个32位的数字。因为是有序的id,可用于当作主键id,订单id等。

//<dependency>
//    <groupId>redis.clients</groupId>
//    <artifactId>jedis</artifactId>
//    <version>3.7.0</version>
//</dependency>

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class RedisID {
    private static volatile JedisPool jedisPool = null;

    private RedisID() {}

    public static JedisPool getJedisPoolInstance() {
        if (null == jedisPool) {
            synchronized (RedisID.class) {
                if (null == jedisPool) {
                    JedisPoolConfig poolConfig = new JedisPoolConfig();
                    poolConfig.setMaxTotal(1000);
                    poolConfig.setMaxIdle(32);
                    poolConfig.setMaxWaitMillis(100 * 1000);
                    poolConfig.setTestOnBorrow(true);
                    jedisPool = new JedisPool(poolConfig, "127.0.0.1", 6379);
                }
            }
        }
        return jedisPool;
    }

    public static void release(JedisPool jedisPool, Jedis jedis) {
        if (null != jedis) {
            Jedis jedis2 = null;
            try {
                jedis2 = jedisPool.getResource();
            } finally {
                jedis2.close();
            }
        }
    }

    public static String getRedisID(Jedis jedis) {
        return String.valueOf(jedis.incr("id"));
    }

    public static void main(String[] args) throws InterruptedException {
        JedisPool jedisPool = RedisID.getJedisPoolInstance();
        Jedis jedis = null;
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(3, 8, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
        for (int i = 0; i < 10; i++) {
            if (threadPoolExecutor.getActiveCount() <= 8) {
                threadPoolExecutor.execute(() -> {
                    try {
                        System.out.println(getRedisID(jedisPool.getResource()));
                    } catch (Exception e) {
                        e.printStackTrace();
                    } finally {
                        RedisID.release(jedisPool, jedis);
                    }
                });
            }
        }
        TimeUnit.SECONDS.sleep(2);
        threadPoolExecutor.shutdown();
    }
}

4、zookeeper

通过创建临时节点来完成,不同客户端每次只能一个创建节来获取节点path来得到唯一id。业务场景:做订单号,数据库表主键。

//<dependency>
//    <groupId>org.apache.zookeeper</groupId>
//    <artifactId>zookeeper</artifactId>
//    <version>3.7.0</version>
//</dependency>

import org.apache.zookeeper.*;
import java.util.concurrent.*;

public class ZooKeeperID {
    //  zk的连接串
    private static final String IP = "127.0.0.1:2181";
    //  计数器对象
    private static CountDownLatch countDownLatch = new CountDownLatch(1);
    //  连接对象
    private static ZooKeeper zooKeeper;

    public static String generateId() throws KeeperException, InterruptedException {
        return zooKeeper.create("/id", new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL).substring(3);
    }


    public static void main(String[] args) throws Exception {
        zooKeeper = new ZooKeeper(IP, 5000, new ZKWatcher());
        countDownLatch.await();
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10));
        for (int i = 0; i < 10; i++) {
            threadPoolExecutor.execute(() -> {
                try {
                    System.out.println(generateId());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
        TimeUnit.SECONDS.sleep(2);
        threadPoolExecutor.shutdown();
    }

    static class ZKWatcher implements Watcher {
        @Override
        public void process(WatchedEvent watchedEvent) {
            countDownLatch.countDown();
            System.out.println("zk的监听器" + watchedEvent.getType());
        }
    }
}

5、雪花算法

1位符号位+41位时间戳+10位数据机器位+12位毫秒内的序列。机器位是机房id+机房里面服务器id凑的,后面序列是保证1s内多并发。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Snowflakes {
    //下面两个每个5位,加起来就是10位的工作机器id
    private long workerId;      //工作id
    private long datacenterId;  //数据id
    private long sequence;      //12位的序列号

    public Snowflakes(long workerId, long datacenterId, long sequence) {
        // sanity check for workerId
        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));
        }
        System.out.printf("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d \n",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

    //初始时间戳
    private long twepoch = 1288834974657L;
    //长度为5位
    private long workerIdBits = 5L;
    private long datacenterIdBits = 5L;
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    private long sequenceBits = 12L;    //序列号id长度
    private long sequenceMask = -1L ^ (-1L << sequenceBits);    //序列号最大值

    //工作id需要左移的位数,12位
    private long workerIdShift = sequenceBits;
    //数据id需要左移位数 12+5=17位
    private long datacenterIdShift = sequenceBits + workerIdBits;
    //时间戳需要左移位数 12+5+5=22位
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    //上次时间戳,初始值为负数
    private long lastTimestamp = -1L;

    public long getWorkerId() {
        return workerId;
    }

    public long getDatacenterId() {
        return datacenterId;
    }

    public long getTimestamp() {
        return System.currentTimeMillis();
    }

    //下一个ID生成算法
    public synchronized long nextId() {
        long timestamp = timeGen();
        //获取当前时间戳如果小于上次时间戳,则表示时间戳获取出现异常
        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds",
                    lastTimestamp - timestamp));
        }
        //获取当前时间戳如果等于上次时间戳(同一毫秒内),则在序列号加一;否则序列号赋值为0,从0开始。
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }
        //将上次时间戳值刷新
        lastTimestamp = timestamp;

        /**
         * 返回结果:
         * (timestamp - twepoch) << timestampLeftShift) 表示将时间戳减去初始时间戳,再左移相应位数
         * (datacenterId << datacenterIdShift) 表示将数据id左移相应位数
         * (workerId << workerIdShift) 表示将工作id左移相应位数
         * | 是按位或运算符,例如:x | y,只有当x,y都为0的时候结果才为0,其它情况结果都为1。
         * 因为个部分只有相应位上的值有意义,其它位上都是0,所以将各部分的值进行 | 运算就能得到最终拼接好的id
         */
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                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) throws InterruptedException {
        Snowflakes worker = new Snowflakes(2, 3, 4);
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor
                (2, 10, 0, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20));
        for (int i = 0; i < 30; i++) {
            if (threadPoolExecutor.getActiveCount() <= 10) {
                threadPoolExecutor.execute(() -> {
                    try {
                        System.out.println(worker.nextId());
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                });
            }
        }
        TimeUnit.SECONDS.sleep(2);
        threadPoolExecutor.shutdown();

        /*for (int i = 0; i < 30; i++) {
            System.out.println(worker.nextId());
        }*/
    }
}

5、其他开源工具

还有其他大厂提供的组件可以直接使用,例如滴滴的TinyID、百度的Uidgenerator、美团的Leaf等。

三、小结

在分布式系统中,生成唯一标识符(ID)是一个关键问题。本文介绍了几种常见的分布式ID生成方案,包括使用UUID、Redis、Zookeeper、雪花算法等技术。由于不同的分布式ID生成方案各有优缺点,需要根据具体业务需求和环境来选择合适的方案。

贝氏倭狐猴

贝氏倭狐猴
    贝氏倭狐猴🐒:体长9-11厘米,尾长12-14厘米,体重30克。这个物种具有非常大的前向眼睛,具有很强的夜视功
    能。上体毛皮呈红棕色,背部的中线有一条黑色条纹延伸至尾部。下体毛皮是奶油色或浅灰色。头部有明显的标记,
    鼻子上方有一个沉闷的白色斑块,眼睛周围有肉桂环。像其他的倭狐猴一样,有一条长尾巴,耳朵比较大并裸露无毛
    。它们出没于潮湿的热带雨林,树栖,群居,通常结对生活在小溪或河边。以水果和虫子为主食。白天躲在树洞中,
    傍晚时等到日落才会活都,是一种在夜间活动的孤独者。通过树木和低层植被,寻找昆虫、水果、壁虎和变色龙等
    小型爬行动物。它们主要食物来源是由蛾蜡蝉幼虫产生的含糖分泌物(或称“蜜露”)。由于非法采伐木材造成的栖息
    地遭到破坏以及农业扩张,该物种有灭绝的危险。

2014年濒危物种红色名录ver3.1——濒危(EN)。 

老博客原文链接