实用向—总结一些唯一ID生成方式

摘要:
在日常项目开发中,我们经常遇到需要生成唯一ID的业务场景。不同的企业有不同的生成唯一ID的方法和要求。首先,生成唯一ID的方法有很多,如UUID、雪花算法、数据库增量等;其次,业务需求也不同。有些只需要确保唯一性,有些需要添加时间戳,有些需要确保它们按顺序增加。
在日常的项目开发中,我们经常会遇到需要生成唯一ID的业务场景,不同的业务对唯一ID的生成方式与要求都会不尽相同,一是生成方式多种多样,如UUID、雪花算法、数据库递增等;其次业务要求上也各有不同,有的只要保证唯一性即可,有的需要加上时间戳,有的要保证按顺序递增等。以下是我结合实际业务中的使用总结了几种唯一ID的生成方式,  要求就是在一般的应用场景下一方面能满足一定数据量级(千万级)的需要,另一方面使用方式相对简单轻量,不需要过多依赖第三方,同时从并发安全、冲突率、生成性能上做了一些简单的测试,大家可以略做参考

一、生成方式

1、UUID产生命令唯一标识,32位的字母数字组合

    /**
     * 根据UUID产生命令唯一标识
     * 
     * @throws InterruptedException
     */
    public static String getUUIDHashCode() {
        String orderSeq = UUID.randomUUID().toString();
        return orderSeq;

    }

2、UUID取hash值+随机数,16位纯数字

    /**
     * 根据UUID取hash值+随机数,产生命令唯一标识
     * 
     * @throws InterruptedException
     */
    public static String getOrderSeq() {
        String orderSeq = Math.abs(UUID.randomUUID().toString().hashCode()) + "";
        while (orderSeq.length() < 16) {
            orderSeq = orderSeq + (int) (Math.random() * 10);
        }
        return orderSeq;
    }

3、十六进制随机数 ,长度16的十六进制字符串

    //十六进制随机数  16位的十六进制字符串
    public static String getRandomHexString() {
        try {
            StringBuffer result = new StringBuffer();
            for (int i = 0; i < 16; i++) {
                result.append(Integer.toHexString(new Random().nextInt(16)));
            }
            return result.toString().toUpperCase();

        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();

        }
        return null;

    }

4、雪花算法

长度不超过20的纯数字,时间戳不同,长度会产生变化

    /** 开始时间戳 */
    private final long twepoch = 1420041600000L;

    /** 机器id所占的位数 */
    private final long workerIdBits = 5L;

    /** 数据标识id所占的位数 */
    private final long datacenterIdBits = 5L;

    /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 支持的最大数据标识id,结果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /** 序列在id中占的位数 */
    private final long sequenceBits = 12L;

    /** 机器ID向左移12位 */
    private final long workerIdShift = sequenceBits;

    /** 数据标识id向左移17位(12+5) */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /** 时间截向左移22位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工作机器ID(0~31) */
    private long workerId;

    /** 数据中心ID(0~31) */
    private long datacenterId;

    /** 毫秒内序列(0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的时间戳 */
    private long lastTimestamp = -1L;

 
    /**
     * 构造函数
     * @param workerId 工作ID (0~31)
     * @param datacenterId 数据中心ID (0~31)
     */
    public SnowFlake(long workerId, long datacenterId) {
        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));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    
    /**
     * 获得下一个ID (该方法是线程安全的)
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒内序列溢出
            if (sequence == 0) {
                //阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的时间截
        lastTimestamp = timestamp;

        //移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (datacenterId << datacenterIdShift) //
                | (workerId << workerIdShift) //
                | sequence;
    }

    /**
     * 阻塞到下一个毫秒,直到获得新的时间戳
     * @param lastTimestamp 上次生成ID的时间戳
     * @return 当前时间戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

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

5、Redis Incr 命令

Redis Incr 命令会将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。

这里以jedis为例提供两种自增ID的生成方式

第一种方式直接通过Incr命令获取自增ID

JedisUtils.incr("inc-test1")

第二张方式获取带有时间戳的唯一编码,时间细度为分钟

    /**
     * 基于Redis生成 时间+递增ID 的唯一编码
     * @param key
     * @return
     */
    public static String getRedisTimeSeq(String key) {
        
        String time = DateUtils.parseDateToStr("yyyyMMddHHmm",new Date());
        
        StringBuilder sBuilder = new StringBuilder(time);
    
        sBuilder.append(JedisUtils.incr(key+":"+time,120));//保证一分钟内KEY有效
        
        return sBuilder.toString();
    }

二、测试

下面我们从冲突率与时间性能上,对以上几种唯一ID的生成方式进行一个简单的测试,同时基于并发安全的考虑,测试分为单线程与多线程两种

    // ---------------测试---------------
    public static void main(String[] args) throws InterruptedException {
        
        int length = 10000000;
        SnowFlake snowFlake = new SnowFlake(1, 1);
        
        final CountDownLatch countDownLatch = new CountDownLatch(10);

        Map<String, String> map = new ConcurrentHashMap<String, String>();
        long begin = System.currentTimeMillis();
        for(int i=0;i<10;i++) {
            Thread thread = new Thread(new Runnable() {
                public void run() {
                    for (int i = 0; i != 1000000; ++i) {

                        String str = String.valueOf(snowFlake.nextId());
                        
//                        String str = StringUtils.getUUIDHashCode(); //根据UUID产生命令唯一标识  长度 32  字母数字组合
//                        
//                        String str = StringUtils.getOrderSeq();//根据UUID取hash值+随机数,产生命令唯一标识 长度 16位纯数字
//                        
//                        String str =StringUtils.getRandomHexString(); //长度16的16进制字符串
                                    
                        map.put(str, str);
                    

                    }
                    countDownLatch.countDown();
                    
                }
            });
            
            thread.start();
        }

        countDownLatch.await();
        
        System.out.println("冲突数为: " + (length - map.size()));
        System.out.println("sync time: " + (System.currentTimeMillis() - begin));
        
        
        Map<String, String> map1 = new ConcurrentHashMap<String, String>();
        begin = System.currentTimeMillis();
        for (int i = 0; i != length; ++i) {

            String str = String.valueOf(snowFlake.nextId());
            
//            String str = StringUtils.getUUIDHashCode();//根据UUID产生命令唯一标识
            
//            String str = StringUtils.getOrderSeq();//根据UUID取hash值+随机数,产生命令唯一标识
            
//            String str =StringUtils.getRandomHexString();
            
            map1.put(str, str);

        }
        System.out.println("冲突数为: " + (length - map1.size()));
        System.out.println("sync time: " + (System.currentTimeMillis() - begin));
    }

测试结果如下:

生成方式生成总数并发冲突数  耗时
UUID产生命令唯一标识
1000W单线程0 26166ms
UUID产生命令唯一标识
1000W多线程0 27915ms
根据UUID取hash值+随机数,产生命令唯一标识
1000W单线程 0 25405ms
根据UUID取hash值+随机数,产生命令唯一标识
1000W 多线程 0 25023ms
十六位随机的十六进制字符串
1000W  单线程   0  25723ms
十六位随机的十六进制字符串
 
1000W   多线程  0 28094ms
雪花算法
1000W   单线程 010100ms
雪花算法
1000W  多线程 011713ms

针对 Redis Incr 命令进行了本地和局域网两种测试, 由于千万级数据耗时太长,数据量改为了百万级,结果如下:

生成方式网络环境生成总数并发冲突数耗时
Redis Incr命令获取自增ID 本地 100W 单线程  0   72445ms
Redis Incr命令获取自增ID 本地 100W 多线程  0   47879ms 
Redis Incr命令获取自增ID局域网 100W  单线程  0   71447ms
Redis Incr命令获取自增ID局域网 100W  多线程  0   45888ms  

Redis Incr命令生成 时间+递增ID 的唯一编码

局域网 100W 单线程  0 236795ms

Redis Incr命令生成 时间+递增ID 的唯一编码

局域网 100W 多线程  0 39281ms

可以看到Redis相比前面一些轻量级的ID生成方式,生成效率上有明显差距,但在分布式环境下,且业务场景对全局唯一ID的生成样式有要求,   redis做为统一的ID生成器还是很有必要的。

由于测试受机器配置、网络带宽等条件影响,以上得出的结果只是一个简单的测试结果,证明这几种唯一ID生成方式具备一定的可用性,大家如果有兴趣可以进行更深入的测试与优化; 

三、总结

其实在日常开发中唯一ID的的生成方式与整体服务的架构与复杂度是密切相关的,本文从并发安全、冲突率、性能等多个方面列举了几种唯一ID的生成方式,相对比较简单实用,但在更复杂的架构与业务场景下,对唯一ID生成的方式的考量就需要更加全面,如并发量、持久化、获取方式等,都需要具体问题具体分析,这里不再深入探讨,希望本文对大家能有所帮助,其中如有不足与不正确的地方还望指出与海涵。

关注微信公众号,查看更多技术文章。

 

实用向—总结一些唯一ID生成方式第1张

 
 

免责声明:文章转载自《实用向—总结一些唯一ID生成方式》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Python_实现json数据的jsonPath(精简版)定位及增删改操作CollectionUtils下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

C#中的String类2

                          深入C# String类 C#中的String类 他是专门处理字符串的(String),他在System的命名空间下,在C#中我们使用的是string 小写的string只是大写的String的一个别名(外号)使用大写和小写都是一样的 Using  == import 常用的字符串处理法 Java中常用的...

数字签名(代码签名)流程

数字签名(代码签名)流程 Authenticode : 这里翻译为数字认证代码。 code sign : 字面的翻译为代码签名,但是通常的我们称为数字签名,以下的文中均称为数字签名。一 数字认证码如果你是软件开发人员,你可能已经知道windows系统和一些浏览器(例如IE,Firefox)使用一种称为数字认证代码的技术来标识软件的发行商,来检查软件没有被...

HBase性能优化方法总结

4.1 HBase性能优化方法总结(一):表的设计 4.1.1 Pre-Creating Regions 默认情况下,在创建HBase表的时候会自动创建一个region分区,当导入数据的时候,所有的HBase客户端都向这一个region写数据,直到这个region足够大了才进行切分。一种可以加快批量写入速度的方法是通过预先创建一些空的regions,这样...

利用Func封装“方法重试”功能

利用Func封装“方法重试”功能   忙余,写个利用Func封装方法重试的功能。该方法主要实现带有返回参数的方法实现多次重试,只要返回的结果不是所限定的返回值,则自动重试X次。Talk is cheap. Show me the code. /// <summary> /// 执行重试方法 /// </summary> /// &l...

C# 文件操作 全收录 追加、拷贝、删除、移动文件、创建目录、递归删除文件夹及文件....

本文收集了目前最为常用的C#经典操作文件的方法,具体内容如下:C#追加、拷贝、删除、移动文件、创建目录、递归删除文件夹及文件、指定文件夹下 面的所有内容copy到目标文件夹下面、指定文件夹下面的所有内容Detele、读取文本文件、获取文件列表、读取日志文件、写入日志文件、创建HTML 文件、CreateDirectory方法的使用C#追加文件Stream...

Android JNI和NDK学习(08)JNI实例一 传递基本类型数据

Android JNI和NDK学习(08)--JNI实例一 传递基本类型数据 本文介绍在Java和JNI之间相互传递基本数据类型的方法。 由于前面已经详细介绍搭建和建立NDK工程的完整流程(参考“静态实现流程”或“动态实现流程”),这里就不再介绍流程;而是将重点放在说明如何实现Java和JNI之间相互传递基本数据。 1 建立eclipse工程 建立工程Nd...