• 实现有序的UUID


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

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

    实现思路

    目标

    1. 全局唯一性
    2. 有序性

    全局唯一性

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

    1. 同JVM实例下,ID唯一

    2. 多JVM实例下,ID唯一

    首先,考虑实现同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标识-状态相关字符集
         * 

    该表根据需要随意替换相同数量字符集

    */ 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标识-IP相关字符集 *

    如需IP隐秘性,可根据需要随意替换相同数量字符集

    */ 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位替换 *

    如需IP隐秘性,可根据需要随意替换相同数量字符集

    *

    由于私网IP网段有限,容易通过确定的网段结合统计攻击猜测IP,可设置替换特定IP位增加猜测难度

    */ 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位32进制,足以表示以毫秒计的3万年时间(当前系统时间肯定大于1970年,因此直接省略符号位处理) 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--) { // 获取字节的低5位有效值 int j = (int) (val & 0x1f); chars[i] = digit[j]; val = val >> 5; } return new String(chars); } public static final String JVM_ID = initJvm(); /** * spi机制获取IP */ private static byte[] getIpBySpi() { byte[] address = null; ServiceLoader providerServiceLoader = ServiceLoader.load(Ipv4Provider.class); Iterator iterator = providerServiceLoader.iterator(); if (iterator.hasNext()) { // SPI 机制获取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; } /** * 从环境变量获取重写的IP位 */ 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机制获取IP byte[] address = getIpBySpi(); if (address == null) { address = InetAddress.getLocalHost().getAddress(); } // 从环境变量获取替换IP Byte[] rewriteIpSegments = getRewriteIpSegmentsByEnv(); if (rewriteIpSegments == null) { rewriteIpSegments = REWRITE_IP_SEGMENT; } // 替换特定位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; // 单实例每毫秒最大允许产生 33554431 个ID, // MAC i7 4核机器3线程测试每毫秒产生ID数在1.9万左右,故该容量导致ID重复的概率几乎为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()); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156
    • 157
    • 158
    • 159
    • 160
    • 161
    • 162
    • 163
    • 164
    • 165
    • 166
    • 167
    • 168
    • 169
    • 170
    • 171
    • 172
    • 173
    • 174
    • 175
    • 176
    • 177
    • 178
    • 179
    • 180
    • 181
    • 182
    • 183
    • 184
    • 185
    • 186
    • 187
    • 188
    • 189

    使用

    测试

    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地址

    1. 实现com.uetty.common.tool.core.string.OUIDGenerator.Ipv4Provider接口

    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 {
    
                // ............ 具体业务逻辑
                String ip = "192.168.0.16";
    
                InetAddress inetAddress = Inet4Address.getByName(ip);
    
                return inetAddress.getAddress();
            } catch (UnknownHostException e) {
                throw new RuntimeException(e);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    1. 在实现类所在项目上,添加spi声明文件(文件名为接口名,文件内容为实现类)

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

    # 实现接口的类全名
    com.uetty.common.tool.core.string.Ipv4ProviderImpl
    
    • 1
    • 2
    1. 运行

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

    地址:https://github.com/Uetty/common-tool/blob/dev/src/main/java/com/uetty/common/tool/core/string/OUIDGenerator.java

  • 相关阅读:
    Linux0.11内核源码解析01
    redis系列之——高可用(主从、哨兵)
    Apache ShenYu 学习笔记一
    java基础15
    运行期获得文件名和行号
    Springboot整合WebSocket实现浏览器和服务器交互
    2023开学礼《乡村振兴战略下传统村落文化旅游设计》许少辉八一新书海口经济学院图书馆
    网络与信息安全基础知识 (软件设计师笔记)
    详解Spring的循环依赖
    《数据结构、算法与应用C++语言描述》使用C++语言实现数组双端队列
  • 原文地址:https://blog.csdn.net/Vincent_Field/article/details/127822701