流水号其实是个学问,分布式系统生成唯一标识的流水号

如果你公司的流水号是20231120162518321678,可以看出来,这是个日期加时分秒毫秒加3位随机数字(本身也是无序的),如果在分布式系统中,这样的流水号在高并行并发是会重复的,就算不是分布式也会重复,就算你加锁也没有用,即使你用的是线程安全的生成时间轴的类并且时间精确到毫秒,同一时间你的随机数仍有重复的可能。那恭喜你,你遇到坑了,它之前流水号的需求是不重复且能含有时间,你最好的做法,也就是去掉毫秒并让后6位不重复了,因为公司的业务流水号已固定且其他平台也有规范。但它并不能解决在分布式系统中的重复问题

能更好一点的解决方式也就是以下这两种:(非分布式的情况下是全局唯一的)

第一种:

    private static final DateTimeFormatter format = new DateTimeFormatterBuilder().appendPattern("yyyyMMddHHmmss").toFormatter();
    private static AtomicInteger atomicInteger = new AtomicInteger(-1);
    public static String getStreamingNo1() {
        LocalDateTime localDateTime = Instant.now().atZone(ZoneId.systemDefault()).toLocalDateTime();
        return localDateTime.format(format) + String.format("%06d", atomicInteger.incrementAndGet() % 1000000);
    }

先讲讲这个原子类,当AtomicInteger的值溢出时,它会循环回到负的最小整数值。具体而言,当AtomicInteger的值增加到Integer.MAX_VALUE(2^31-1)时,下一次增量操作将导致其值变为Integer.MIN_VALUE(-2^31),然后继续递增。

如果你在使用AtomicInteger时担心溢出问题,可以考虑使用AtomicLong,它的取值范围更大(从Long.MIN_VALUE到Long.MAX_VALUE)。

2^31-1等于21亿多张单才会变负,也不知道你的公司有没有机会接到这么多单。

就算溢出流水号也不会重复,他会变成这种形式20231120163834-345920,能确保唯一。

第二种:(按照分布式的理念,我们不仅要不重复还要有序)

写得更好一点也就是有序且没有负数了:只要同一时间不超过999999个请求不会重复,不够用继续增加流水号长度就可以了6位不够就10位,你的系统也没怎么强的处理能力,1秒999999个。

    private static final DateTimeFormatter format2 = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
    private static final AtomicInteger atomicInteger2 = new AtomicInteger(1);
    public static synchronized String getStreamingNo2() {
        LocalDateTime localDateTime = Instant.now().atZone(ZoneId.systemDefault()).toLocalDateTime();
        int count = atomicInteger2.getAndIncrement() % 1000000; // 范围[1,999999]
        atomicInteger2.compareAndSet(1000000, 1); // 如果计数器已经达到999999,则重置为1
        return localDateTime.format(format2) + String.format("%06d", count);
    }

分布式系统生成唯一标识的流水号你要考虑以下因素:

  1. 全局唯一性:不同节点生成的流水号必须是全局唯一的。

  2. 有序性:生成的流水号应该是有序的,方便后续的排序、查询等操作。

  3. 高并发性:在高并发的情况下,生成流水号的方式不能成为瓶颈,否则会影响系统性能。

其实只要业务流水号在所有流经系统全局唯一且业务日志关键信息都有打印,业务问题都是能解决的。那在分布式系统要怎么办呢,介绍一种算法:

Snowflake算法的核心思想是将时间戳、数据中心ID、机器ID和序列号组合成一个64位的长整型ID。在这个实现中,第1位为0。对于Java的long类型而言,它的最高位是符号位,0表示正数,1表示负数,而在Snowflake算法中,我们用到的是一个非负整数,因此符号位并没有实际意义。我们只需要保证生成的ID是一个非负整数即可。时间戳占据了高41位,数据中心ID占据了中间的5位,机器ID占据了接下来的5位,最后的12位用于表示序列号。

Snowflake算法是一种经典的分布式ID生成算法,它能够在分布式环境下生成全局唯一的ID,具备一定的性能和可用性。它的优点包括:

  1. 简单易用:Snowflake算法相对简单,实现也较为容易理解和使用。
  2. 高性能:Snowflake算法通过位运算实现ID的生成,不依赖外部存储和网络通信,因此性能较高。
  3. 时间有序:Snowflake算法生成的ID中包含时间戳信息,保证了生成的ID按照时间有序递增。
  4. 可扩展性:Snowflake算法支持配置数据中心ID和机器ID,可以灵活扩展到多个数据中心和多台机器。

然而,Snowflake算法也存在一些限制和缺点:

  1. 依赖时钟回拨:如果系统时钟发生回拨,可能导致生成的ID不唯一或者乱序。要注意时钟同步的问题。
  2. 有限的容量:Snowflake算法中,时间戳占用的位数决定了ID的时间范围,如果超出了时间范围,可能导致ID重复。
  3. 数据中心ID和机器ID的限制:Snowflake算法中,数据中心ID和机器ID的位数限制了可用的数据中心数量和机器数量。
  4. 有一定的并发限制:在同一毫秒内生成ID时,需要对并发访问进行控制,以确保ID的唯一性,可能需要引入锁机制或其他并发控制手段。

Java中有很多开源的工具类库提供了Snowflake算法的实现,比较常用的有:

  1. Twitter的Snowflake算法实现类:Twitter的Snowflake算法是Snowflake算法的最初实现,它提供了Java和Scala两种语言的实现类。该实现类比较简单,使用也比较方便。GitHub地址:GitHub - twitter-archive/snowflake: Snowflake is a network service for generating unique ID numbers at high scale with some simple guarantees.

  2. Leaf-segment算法:Leaf-segment算法是美团点评公司开源的一款ID生成框架,它基于Snowflake算法实现,支持高并发场景下的ID生成。Leaf-segment算法提供了Java和Go两种语言的实现。GitHub地址:https://github.com/Meituan-Dianping/Leaf

  3. Hutool的Snowflake算法实现类:Hutool是一个Java工具类库,提供了丰富的工具类和函数库,其中包括Snowflake算法的实现类。GitHub地址:GitHub - dromara/hutool: 🍬A set of tools that keep Java sweet.

这些工具类库提供了比较成熟、稳定的Snowflake算法实现,可以根据自己的需要进行选择和使用。以下是使用Java实现Snowflake算法的示例代码:

/**
 * 雪花算法ID生成器
 */
public class SnowflakeIdGenerator {
    // 数据中心ID(可以根据实际情况进行配置)
    private final long dataCenterId;
    // 机器ID(可以根据实际情况进行配置)
    private final long machineId;
    // 序列号
    private long sequence = 0L;
    // 上一次生成ID的时间戳
    private long lastTimestamp = -1L;

    // 时间戳占用的位数
    private static final long TIMESTAMP_BITS = 41L;
    // 数据中心ID占用的位数
    private static final long DATACENTER_ID_BITS = 5L;
    // 机器ID占用的位数
    private static final long MACHINE_ID_BITS = 5L;
    // 序列号占用的位数
    private static final long SEQUENCE_BITS = 12L;

    // 最大支持的数据中心ID,结果是31
    private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
    // 最大支持的机器ID,结果是31
    private static final long MAX_MACHINE_ID = ~(-1L << MACHINE_ID_BITS);
    // 最大支持的序列号,结果是4095
    private static final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);

    // 时间戳左移位数,结果是22
    private static final long TIMESTAMP_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS + DATACENTER_ID_BITS;
    // 数据中心ID向左移位数,结果是17
    private static final long DATACENTER_ID_SHIFT = SEQUENCE_BITS + MACHINE_ID_BITS;
    // 机器ID向左移位数,结果是12
    private static final long MACHINE_ID_SHIFT = SEQUENCE_BITS;

    /**
     * 构造函数
     *
     * @param dataCenterId 数据中心ID
     * @param machineId    机器ID
     */
    public SnowflakeIdGenerator(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATACENTER_ID || dataCenterId < 0) {
            throw new IllegalArgumentException("Data center ID can't be greater than " + MAX_DATACENTER_ID + " or less than 0");
        }
        if (machineId > MAX_MACHINE_ID || machineId < 0) {
            throw new IllegalArgumentException("Machine ID can't be greater than " + MAX_MACHINE_ID + " or less than 0");
        }
        this.dataCenterId = dataCenterId;
        this.machineId = machineId;
    }

    /**
     * 生成唯一ID
     *
     * @return 生成的唯一ID
     */
    public synchronized long generateId() {
        long timestamp = System.currentTimeMillis();

        // 检查时间戳是否小于上一次生成ID的时间戳,如果小于,则表示时钟回拨了,抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("Clock moved backwards. Refusing to generate ID for " + (lastTimestamp - timestamp) + " milliseconds");
        }

        // 如果时间戳与上一次生成ID的时间戳相同,则在同一毫秒内需要生成多个ID
        if (timestamp == lastTimestamp) {
            // 序列号加1,并判断序列号是否超过了最大值
            sequence = (sequence + 1) & MAX_SEQUENCE;

            // 如果序列号超过了最大值,需要等待下一毫秒
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 时间戳不同,序列号重置为0
            sequence = 0L;
        }

        // 更新上一次生成ID的时间戳为当前时间戳
        lastTimestamp = timestamp;

        // 生成最终的ID
        return ((timestamp << TIMESTAMP_SHIFT) |
                (dataCenterId << DATACENTER_ID_SHIFT) |
                (machineId << MACHINE_ID_SHIFT) |
                sequence);
    }

    /**
     * 等待下一毫秒,直到获取到比上一次生成ID的时间戳更大的时间戳
     *
     * @param lastTimestamp 上一次生成ID的时间戳
     * @return 下一毫秒的时间戳
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = System.currentTimeMillis();

        // 循环等待,直到获取到比上一次生成ID的时间戳更大的时间戳
        while (timestamp <= lastTimestamp) {
            timestamp = System.currentTimeMillis();
        }

        return timestamp;
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值