TimeId 基于时间戳的自增ID算法(Java版)

常用的全局唯一ID算法

1、UUID

首先是大名鼎鼎的 UUID,UUID 是通用唯一识别码(Universally Unique Identifier)的缩写。
UUID是一个128比特的数值,是基于当前时间、计数器(counter)和硬件标识(通常为无线网卡的MAC地址)等数据计算生成的。
虽然 UUID 碰撞几率不为零,但它足够接近于零,可以忽略不计,可以认为UUID能够保证全局唯一。
但是 UUID 有一些缺点,首先他相对比较长(36个字符,去掉多余的“-”后依旧有32个字符),另外UUID一般是无序的(默认的版本4)。

2、SnowFlake

SnowFlake 雪花算法是 Twitter 开源的分布式 ID 生成算法,其具有简洁、高性能、低延迟、ID 按时间趋势有序等特点。
雪花算法生成后是一个 64bit 的 Long 型的数值,其中有1位标识,41位时间截,10位的数据机器位,12位毫秒内的计数。
0 | timestamp (41 bits) | node ID (10 bits) | sequence number (12 bits)
因为第一位标识符是 0(保证long是一个正整数),后面就是时间戳,所以整个ID基本保持了自增。
雪花算法较高的性能和吞吐量,生成时不依赖于数据库,完全在内存中生成,每秒能够产生26万ID左右,能够满足绝大多数高并发场景下的互联网应用的要求,并且因为其自增的特性,数据库索引效率也很高。(在分布式系统多节点的情况下,所有节点的时钟并不能保证不完全同步,所以有可能会出现不是全局递增的情况,但是总体是递增的。)
默认的雪花算法41位时间戳可以使用69年,如果时间戳从2015年开始的,到了2084年会出现ID冲突问题。
算法实现:https://www.cnblogs.com/relucent/p/4955340.html

3、NanoID

NanoID 是一个用于生成小型、安全且唯一标识符(ID)的 JavaScript 库(目前也有其他语言版本的实现)。它专门设计用于在不同的应用场景中生成短、易于处理的标识符,例如用作数据库记录的主键、URL 缩短服务的短链接标识等。
NanoID 它通过使用高质量的随机数生成算法,能够确保生成的标识符是唯一的并且难以预测。NanoID 与 UUID v4 (基于随机) 相当,它们在 ID 中有相似数量的随机位 (NanoID 为126,UUID 为122),因此它们的冲突概率相似。
NanoID 比 UUID 更加紧凑,使用更大的字母表(A-Za-z0-9_-)。 因此,ID 大小从36个符号减少到21个符号。NanoID使用URL友好字符(A-Za-z0-9_-)。非常适合web应用程序中的唯一标识符。
默认的 NanoID 中包含了字母的大小写,在大小写不敏感的情况下(例如对属性大小写不敏感的数据库),冲突概率会增加,但是可以通过自定义字母表以及增加长度,对该问题进行优化。
此外,NanoID生成是无序的,对聚类索引的数据列(B-TREE索引列)并不友好。

TimeId 算法

1、算法介绍

TimeId 是一种时间序列ID算法(20位字符串),其组成结构如下:
时间戳 + 循环计数 + 随机数 + 网络地址 + 进程号
优势:
1、TimeId 是一种时间戳算法,所以保证了其数值整体有序的(有序但不连贯),数据库索引效率也很高。
2、与 SnowflakeId 算法相比,TimeId 的冲突的概率比SnowflakeId低,也没有69年的限制。
3、与 UUID 相比,TimeId 大小从36个符号减少到20个符号。
缺点:
1、因为 TimeId 是一个字符串,相对 SnowflakeId(长整型)会占用更大的空间,但是依旧比 UUID 紧凑。
2、安全性上与 UUID v1 (根据时间和节点生成)类似,使用了节点ID来确保唯一性,雪花算法也存在类似问题。

2、TimeId 的实现(JAVA版本)

注:以下代码实现的网络地址获取部分只考虑了IPv4,如果需要IPv6,请自己修改 isValidIPv4 方法。

import java.lang.management.ManagementFactory;
import java.math.BigInteger;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.security.SecureRandom;
import java.util.Enumeration;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

/**
 * 时间序列ID生成器(20位)<br>
 * 时间戳+循环计数+随机数+网络地址+进程号<br>
 * 优势:<br>
 * TimeId 是有序的(有序但不连贯)<br>
 * TimeId 使用了和 SnowflakeIdWorker 类似的时间戳算法,但是冲突的概率更低<br>
 * TimeId 与 UUID 相比,大小从36个符号减少到20个符号<br>
 * 劣势:<br>
 * TimeId 是字符串,相对 SnowflakeIdWorker( 长整型) 占用更大的空间,但是比UUID紧凑
 */
public class TimeId {

    private static final int RADIX36 = 36;
    private static final int TIMESTAMP_LENGTH = 9;
    private static final int COUNTER_LENGTH = 5;
    private static final int MAC_LENGTH = 2;
    private static final int PID_LENGTH = 1;
    private static final int RANDOM_LENGTH = 3;
    private static final int COUNTER_MOD = computeRadix36Mod(COUNTER_LENGTH);
    private static final int MAC_MOD = computeRadix36Mod(MAC_LENGTH);
    private static final int PID_MOD = computeRadix36Mod(PID_LENGTH);
    private static final int RANDOM_MOD = computeRadix36Mod(RANDOM_LENGTH);
    private static final char ZERO_CHAR = '0';

    private static final String LOCALHOST_IPV4 = "127.0.0.1";
    private static final String ANYHOST_IPV4 = "0.0.0.0";
    private static final String BROADCAST_IPV4 = "255.255.255.255";
    private static final Pattern IPV4_PATTERN = Pattern.compile(//
            "^(2(5[0-5]{1}|[0-4]\\d{1})|[0-1]?\\d{1,2})(\\.(2(5[0-5]{1}|[0-4]\\d{1})|[0-1]?\\d{1,2})){3}$"//
    );

    /** 循环序列号 */
    private static final AtomicInteger NEXT_COUNTER = new AtomicInteger(new SecureRandom().nextInt());

    /** 此类用来创建随机数的随机数生成器 */
    private static class Holder {
        static final SecureRandom NUMBER_GENERATOR = new SecureRandom();
        static final long MAC_VALUE = getMac();
        static final long PID_VALUE = getPid();
    }

    /**
     * 构造函数
     */
    protected TimeId() {
    }

    /**
     * 生成 TimeId
     * @return TimeId 字符串
     */
    public static String nextId() {
        StringBuilder buffer = new StringBuilder();
        append(buffer, timeGen(), TIMESTAMP_LENGTH);
        append(buffer, Math.abs(NEXT_COUNTER.getAndIncrement() % COUNTER_MOD), COUNTER_LENGTH);
        append(buffer, Math.abs(Holder.MAC_VALUE % MAC_MOD), MAC_LENGTH);
        append(buffer, Math.abs(Holder.PID_VALUE % PID_MOD), PID_LENGTH);
        append(buffer, Math.abs(Holder.NUMBER_GENERATOR.nextLong() % RANDOM_MOD), RANDOM_LENGTH);
        return buffer.toString();
    }

    /**
     * 将数值添加到字符串缓冲中
     * @param buffer 字符串缓冲中
     * @param value 数值
     * @param length 添加的位数
     */
    private static void append(StringBuilder buffer, long value, int length) {
        buffer.append(leftPad(Long.toString(value, RADIX36), length, ZERO_CHAR));
    }

    /**
     * 返回以毫秒为单位的当前时间
     * @return 当前时间(毫秒)
     */
    private static long timeGen() {
        return System.currentTimeMillis();
    }

    /**
     * 获得MAC值
     * @return MAC值
     */
    private static long getMac() {
        long mac = 0;
        try {
            byte[] address = getHardwareAddress();
            mac = new BigInteger(address).longValue();
        } catch (Exception | Error e) {
            mac = Holder.NUMBER_GENERATOR.nextLong();
        }
        return mac;
    }

    /**
     * 获得当前进程ID
     * @return 进程ID
     */
    private static long getPid() {
        long pid = 0;
        String name = ManagementFactory.getRuntimeMXBean().getName();
        try {
            pid = Long.parseLong(name.split("@")[0]);
        } catch (Exception | Error e) {
            pid = Holder.NUMBER_GENERATOR.nextLong();
        }
        return pid;
    }

    /**
     * 计算模数
     * @param length 长度
     * @return 进制模(32)
     */
    private static int computeRadix36Mod(int length) {
        int value = 1;
        for (int i = 0; i < length; i++) {
            value *= RADIX36;
        }
        return value - 1;
    }

    /**
     * 左填充指定字符的字符串.
     * @param cs 需要填充的字符串
     * @param size 要填充到的大小
     * @param padChar 要填充的字符
     * @return 左填充字符串或原始字符串(如果不需要填充)
     */
    private static String leftPad(final CharSequence cs, final int size, final char padChar) {
        if (cs == null) {
            return null;
        }
        final int pads = size - cs.length();
        if (pads <= 0) {
            return cs.toString();
        }
        StringBuilder builder = new StringBuilder(size);
        for (int i = 0; i < pads; i++) {
            builder.append(padChar);
        }
        builder.append(cs);
        return builder.toString();
    }

    /**
     * 获得本机 MAC地址
     * @return 本机MAC地址
     */
    private static final byte[] getHardwareAddress() {
        try {
            for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
                NetworkInterface networkInterface = en.nextElement();
                for (Enumeration<InetAddress> addrs = networkInterface.getInetAddresses(); addrs.hasMoreElements();) {
                    String ip = addrs.nextElement().getHostAddress();
                    if (isValidIPv4(ip)) {
                        return networkInterface.getHardwareAddress();
                    }
                }
            }
        } catch (SocketException e) {
            // Ignore
        }
        return new byte[0];
    }

    /**
     * 判断是否是有效IPv4地址
     * @param ip IP地址
     * @return 如果是有效IPv4地址返回true,否则返回false
     */
    private static boolean isValidIPv4(String ip) {
        return (ip != null //
                && !ANYHOST_IPV4.equals(ip) //
                && !LOCALHOST_IPV4.equals(ip) //
                && !BROADCAST_IPV4.equals(ip) //
                && IPV4_PATTERN.matcher(ip).matches());
    }
}

使用方式

// 获取全局唯一ID
String id = TimeId.nextId();
posted @ 2023-08-09 12:45  relucent  阅读(835)  评论(0编辑  收藏  举报