实现有序的UUID

为什么需要重新实现一个UUID

JVM自带版本实现了UUID的全局唯一性,它使用网卡MAC地址计算UUID机器位,并且使用了摘要算法使其结果足够随机。但有时候实际项目的需求有所不同,UUID的随机性反而增加了调试与维护时的麻烦,我们仅仅需要一个全局唯一的UUID,并且需要能够按时间有序。

实现思路

对于全局唯一性要求,我们仅需分步实现以下两点即可:

首先,考虑实现 同JVM实例下ID的唯一性

考虑到唯一性,我们可能可以想到最原始的计数器,每生成一个ID,值自增1,只需要简单的线程安全级别的计数器即可,当然考虑到ID是全局使用,得考虑容量需要足够大至少需要long类型,因此 AtomicLong是个很好的选择。很好,这跟Mysql的自增ID是一样的了,既实现了唯一性又满足了有序性,但是接下来就会发现有一个问题,Mysql是长时间不关机的并且使用文件存储数据的,而JVM随时可能关机重启,重启后上一次计数到哪个数值就不知道了,需要保证重启后仍然唯一是个大问题。

这时候就得考虑将计数器换成一个不受开关机影响的因子。很快就能想到了生成ID的时间,不管是开机还是关机系统时间总是一直往前的,只要与生成ID的时间挂钩同样能够提供唯一性,并且保证重启后生成的ID不与重启前生成的ID重复,同时又能顺带满足有序性的要求。时间虽然是连续且唯一的概念,但是系统中的时间却是精度有限的,精确到毫秒级( System.currentTimeMillis()),同一时间有可能生成多个ID,因此还需要保证同一毫秒内同一实例生成的的ID不重复。

Tip:虽然对于时间有粒度更细的 System.nanoTime()可以精确到纳秒,但 System.nanoTime()是以启动时为0秒计算的,并不能保证重启后不重复

不过既然已经控制到毫秒级别了,这就很简单了,只要重新在每一毫秒内加上计数器即可,由于一毫秒的时间足够短,能生成的ID数量有限,因此只需要小容量的线程安全级别的计数器( AtomicInteger)即可,同一毫秒内每生成一个ID,计数器递增。这样毫秒级别时间戳加上一个计数器,我们达到了同JVM实例下ID唯一的目标。

接下来,考虑 多JVM实例下ID的唯一性

既然同一JVM下ID已经唯一了,那么给ID再加上一个实例唯一的标识不就行了吗。思路很明显了,只需要给实例自动生成唯一的标识符即可。机器上能够提供唯一性的元素很多,比如MAC地址,IP地址,当然这些也是相对唯一的,在特定的情境下可以认为是唯一的。由于自己确定的项目使用,也不需要太复杂的方案,选取了一个简单易获取的元素——IP地址,考虑到仅仅IP地址仍旧不够保险,再三考虑后增加了一个元素——类初始化时间——与IP共同构成了JVM标识。很好,到这里就满足了多JVM实例下ID的唯一性了。

为了保证ID的唯一性,已经确定选取了4个元素作为ID的组成部分:IP地址、类初始化时间、生成ID的时间、计数器。再需要保证有序性,只需要调整ID元素部分的顺序即可,由于生成ID的时候是保证有序性的关键,因此需要将其放置第一个部分。

ID不希望太长 从字符集方面进行优化,优化字符集到base32(由于UUID仅使用base16编码,使其即使长度达到了32位,携带的信息却并不多),优化到base32字符集后,虽然携带的信息量多了不少,但长度仍然仅为32位

IP做为ID一部分存在少量信息暴露的考虑 从IP匿名化和字符集多样化方面进行优化:1.通过IP替换方式来变更IP部分单元,2.IP地址部分使用独立字符集编码,不同项目可根据需要单独修改掉该字符集

自定义IP需求 从环境变量机制和SPI机制方面进行优化:1.从环境变量传递参数替换部分IP单元,2.通过SPI机制传递自定义IP

代码实现

package com.uetty.common.tool.core.string;

import java.net.InetAddress;
import java.util.Iterator;
import java.util.ServiceLoader;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Ordered Universally Unique Identifier
 * 时间顺序的通用唯一标识符
 * @author vince
 */
public class OUIDGenerator {

    public interface Ipv4Provider {
        byte[]  getIp();
    }

    /**
     * 开头时间戳编码表(为了保持有序性,该表即使替换字符集也需保持Ascii有序性)
     */
    final static char[] TIMESTAMP_DIGITS = {
            '0' , '1' , '2' , '3' , '4' , '5' ,
            '6' , '7' , '8' , '9' , 'a' , 'b' ,
            'c' , 'd' , 'e' , 'f' , 'g' , 'h' ,
            'j' , 'k' , 'm' , 'n' , 'p' , 'r' ,
            's' , 't' , 'u' , 'v' , 'w' , 'x' ,
            'y' , 'z'
    };

    /**
     * JVM标识-状态相关字符集
     * <p>&#x8BE5;&#x8868;&#x6839;&#x636E;&#x9700;&#x8981;&#x968F;&#x610F;&#x66FF;&#x6362;&#x76F8;&#x540C;&#x6570;&#x91CF;&#x5B57;&#x7B26;&#x96C6;</p>
     */
    final static char[] JVM_STAT_DIGITS = {
            'a' , 'b' , 'c' , 'd' , 'e' , 'f' ,
            'g' , 'h' , 'j' , 'k' , 'm' , 'n' ,
            'p' , 'r' , 's' , 't' , 'u' , 'v' ,
            'w' , 'x' , 'y' , 'z' , '0' , '1' ,
            '2' , '3' , '4' , '5' , '6' , '7' ,
            '8' , '9'
    };

    /**
     * JVM&#x6807;&#x8BC6;-IP&#x76F8;&#x5173;&#x5B57;&#x7B26;&#x96C6;
     * <p>&#x5982;&#x9700;IP&#x9690;&#x79D8;&#x6027;&#xFF0C;&#x53EF;&#x6839;&#x636E;&#x9700;&#x8981;&#x968F;&#x610F;&#x66FF;&#x6362;&#x76F8;&#x540C;&#x6570;&#x91CF;&#x5B57;&#x7B26;&#x96C6;</p>
     */
    final static char[] JVM_IP_DIGITS = {
            'a' , 'b' , 'c' , 'd' , 'e' , 'f' ,
            '0' , '1' , '2' , '3' , '4' , '5' ,
            '6' , '7' , '8' , '9' , 'g' , 'h' ,
            'j' , 'k' , 'm' , 'n' , 'p' , 'r' ,
            's' , 't' , 'u' , 'v' , 'w' , 'x' ,
            'y' , 'z'
    };

    /**
     * IP&#x4F4D;&#x66FF;&#x6362;
     * <p>&#x5982;&#x9700;IP&#x9690;&#x79D8;&#x6027;&#xFF0C;&#x53EF;&#x6839;&#x636E;&#x9700;&#x8981;&#x968F;&#x610F;&#x66FF;&#x6362;&#x76F8;&#x540C;&#x6570;&#x91CF;&#x5B57;&#x7B26;&#x96C6;</p>
     * <p>&#x7531;&#x4E8E;&#x79C1;&#x7F51;IP&#x7F51;&#x6BB5;&#x6709;&#x9650;&#xFF0C;&#x5BB9;&#x6613;&#x901A;&#x8FC7;&#x786E;&#x5B9A;&#x7684;&#x7F51;&#x6BB5;&#x7ED3;&#x5408;&#x7EDF;&#x8BA1;&#x653B;&#x51FB;&#x731C;&#x6D4B;IP&#xFF0C;&#x53EF;&#x8BBE;&#x7F6E;&#x66FF;&#x6362;&#x7279;&#x5B9A;IP&#x4F4D;&#x589E;&#x52A0;&#x731C;&#x6D4B;&#x96BE;&#x5EA6;</p>
     */
    private final static Byte[] REWRITE_IP_SEGMENT = { (byte) 112, null, null, null};

    public static String generate() {
        return getTime(TIMESTAMP_DIGITS) +
                JVM_ID +
                getSerial();
    }

    private static String getTime(char[] digits) {
        long currentTimeMillis = System.currentTimeMillis();
        // 10&#x4F4D;32&#x8FDB;&#x5236;&#xFF0C;&#x8DB3;&#x4EE5;&#x8868;&#x793A;&#x4EE5;&#x6BEB;&#x79D2;&#x8BA1;&#x7684;3&#x4E07;&#x5E74;&#x65F6;&#x95F4;&#xFF08;&#x5F53;&#x524D;&#x7CFB;&#x7EDF;&#x65F6;&#x95F4;&#x80AF;&#x5B9A;&#x5927;&#x4E8E;1970&#x5E74;&#xFF0C;&#x56E0;&#x6B64;&#x76F4;&#x63A5;&#x7701;&#x7565;&#x7B26;&#x53F7;&#x4F4D;&#x5904;&#x7406;&#xFF09;
        return format32(digits, currentTimeMillis, 10);
    }

    private static String format32(char[] digit, long val, int len) {
        char[] chars = new char[len];
        for (int i = chars.length - 1; i >= 0; i--) {
            // &#x83B7;&#x53D6;&#x5B57;&#x8282;&#x7684;&#x4F4E;5&#x4F4D;&#x6709;&#x6548;&#x503C;
            int j = (int) (val & 0x1f);
            chars[i] = digit[j];
            val = val >> 5;
        }
        return new String(chars);
    }

    public static final String JVM_ID = initJvm();

    /**
     * spi&#x673A;&#x5236;&#x83B7;&#x53D6;IP
     */
    private static byte[] getIpBySpi() {
        byte[] address = null;
        ServiceLoader<ipv4provider> providerServiceLoader = ServiceLoader.load(Ipv4Provider.class);
        Iterator<ipv4provider> iterator = providerServiceLoader.iterator();
        if (iterator.hasNext()) {
            // SPI &#x673A;&#x5236;&#x83B7;&#x53D6;IP
            Ipv4Provider provider = iterator.next();
            address = provider.getIp();
            if (address != null && address.length != 4) {
                new IllegalArgumentException("ipv4 provider not provide ipv4 address").printStackTrace();
                return null;
            }
        }
        return address;
    }

    /**
     * &#x4ECE;&#x73AF;&#x5883;&#x53D8;&#x91CF;&#x83B7;&#x53D6;&#x91CD;&#x5199;&#x7684;IP&#x4F4D;
     */
    private static Byte[] getRewriteIpSegmentsByEnv() {
        Byte[] rewriteIpBytes = null;
        String ouidRewriteIp = System.getenv("OUID_REWRITE_IP");
        if (ouidRewriteIp != null) {
            String[] split = ouidRewriteIp.split("\\.");
            rewriteIpBytes = new Byte[4];
            try {
                for (int i = 0; i < 4; i++) {
                    if ("%".equals(split[i])) {
                        continue;
                    }
                    int num = Integer.parseInt(split[i]);
                    if (num > 255 || num < 0) {
                        throw new IllegalArgumentException("invalid rewrite ipv4 address " + ouidRewriteIp);
                    }
                    rewriteIpBytes[i] = (byte) (num > 127 ? num - 256 : num);
                }
            } catch (Exception e) {
                e.printStackTrace();
                rewriteIpBytes = null;
            }
        }
        return rewriteIpBytes;
    }

    @SuppressWarnings({"ConstantConditions", "RedundantSuppression"})
    private static String initJvm() {
        long ipAddr;
        try {

            // spi&#x673A;&#x5236;&#x83B7;&#x53D6;IP
            byte[] address = getIpBySpi();
            if (address == null) {
                address = InetAddress.getLocalHost().getAddress();
            }

            // &#x4ECE;&#x73AF;&#x5883;&#x53D8;&#x91CF;&#x83B7;&#x53D6;&#x66FF;&#x6362;IP
            Byte[] rewriteIpSegments = getRewriteIpSegmentsByEnv();
            if (rewriteIpSegments == null) {
                rewriteIpSegments = REWRITE_IP_SEGMENT;
            }

            // &#x66FF;&#x6362;&#x7279;&#x5B9A;&#x4F4D;IP
            for (int i = 0; i < address.length; i++) {
                if (rewriteIpSegments.length > i && rewriteIpSegments[i] != null) {
                    address[i] = rewriteIpSegments[i];
                }
            }

            ipAddr = toLong(address);
        } catch (Exception e) {
            e.printStackTrace();
            ipAddr = 0;
        }

        return format32(JVM_IP_DIGITS, ipAddr, 7) + getTime(JVM_STAT_DIGITS);
    }

    private static final AtomicInteger SEQ = new AtomicInteger((int) (Math.random() * Integer.MAX_VALUE / 1000));

    private static String getSerial() {
        long serial = SEQ.incrementAndGet() & 0x1ffffff;
        // &#x5355;&#x5B9E;&#x4F8B;&#x6BCF;&#x6BEB;&#x79D2;&#x6700;&#x5927;&#x5141;&#x8BB8;&#x4EA7;&#x751F; 33554431 &#x4E2A;ID&#xFF0C;
        // MAC i7 4&#x6838;&#x4E0B;3&#x7EBF;&#x7A0B;&#x6D4B;&#x8BD5;&#x6BCF;&#x6BEB;&#x79D2;&#x4EA7;&#x751F;ID&#x6570;&#x5728;1.9&#x4E07;&#x5DE6;&#x53F3;&#xFF0C;&#x6545;&#x8BE5;&#x5BB9;&#x91CF;&#x5BFC;&#x81F4;ID&#x91CD;&#x590D;&#x7684;&#x6982;&#x7387;&#x51E0;&#x4E4E;&#x4E3A;0
        return format32(TIMESTAMP_DIGITS, serial, 5);
    }

    private static long toLong(byte[] bytes) {
        long result = 0;
        for (int i = 0; i < 4; i++) {
            result = (result << 8) + (0xff & bytes[i]);
        }
        return result;
    }

    public static void main(String[] args) {
        System.out.println(generate());
    }
}
</ipv4provider></ipv4provider>

root@hecs-225836:~# java com.uetty.common.tool.core.string.OUIDGenerator

01ghnf29zvb24uadnabghn9c3zv1urtm

通过环境变量修改IP部分单元

root@hecs-225836:~# # 将IP 192.168.0.101 的第一个网段修改为113,即伪装本机 IP 为 113.168.0.101
root@hecs-225836:~# export OUID_REWRITE_IP=”113.%.%.%”
root@hecs-225836:~# java com.uetty.common.tool.core.string.OUIDGenerator

root@hecs-225836:~# # 将IP 192.168.0.101 的IP伪装为113.101.145.18
root@hecs-225836:~# export OUID_REWRITE_IP=”113.101.145.18″
root@hecs-225836:~# java com.uetty.common.tool.core.string.OUIDGenerator

通过SPI机制提供IP地址

Ipv4ProviderImpl

package com.uetty.common.tool.core.string;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * @author vince
 */
public class Ipv4ProviderImpl implements OUIDGenerator.Ipv4Provider {

    @Override
    public byte[] getIp() {
        try {

            // ............ &#x5177;&#x4F53;&#x4E1A;&#x52A1;&#x903B;&#x8F91;
            String ip = "192.168.0.16";

            InetAddress inetAddress = Inet4Address.getByName(ip);

            return inetAddress.getAddress();
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        }
    }
}

src/main/resources/META-INF/services/com.uetty.common.tool.core.string.OUIDGenerator$Ipv4Provider

&#x5B9E;&#x73B0;&#x63A5;&#x53E3;&#x7684;&#x7C7B;&#x5168;&#x540D;
com.uetty.common.tool.core.string.Ipv4ProviderImpl

root@hecs-225836:~# java com.uetty.common.tool.core.string.OUIDGenerator

Original: https://blog.csdn.net/Vincent_Field/article/details/127822701
Author: Vince352
Title: 实现有序的UUID

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/658632/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球