雪花算法(snowflake)优化

雪花算法适用于高性能生成ID的场景,类似于高并发生成用户ID,订单ID这样的场景。

雪花算法用在分布式环境中,需要防止ID冲突问题,以下是解决冲突的一个思路。

package com.xbd.bigdata.util.snowflake;

import com.xbd.bigdata.util.IpUtils;
import lombok.extern.slf4j.Slf4j;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;

/**
 * @Description: 静态内部类单例模式实现雪花算法
 * https://www.cnblogs.com/qdhxhz/p/11372658.html
 * 参考: Java位运算原理及使用讲解 https://www.cnblogs.com/findbetterme/p/10787118.html
 * @Author: usr1999
 * @Create: 2021/10/28
 */
@Slf4j
public class SnowflakeIdUtils {

    /**
     * 私有的 静态内部类
     */
    private static class SnowFlake {

        /**
         * 内部类对象(单例模式)
         */
        private static final SnowflakeIdUtils.SnowFlake SNOW_FLAKE = new SnowflakeIdUtils.SnowFlake();
        /**
         * 起始的时间戳
         */
        private final long START_TIMESTAMP = 1635264000000L;
        /**
         * 序列号占用位数
         */
        private final long SEQUENCE_BIT = 12;
        /**
         * 机器标识占用位数
         */
        private final long MACHINE_BIT = 10;
        /**
         * 时间戳位移位数
         */
        private final long TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT;
        /**
         * 最大序列号(4095)
         */
        private final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
        /**
         * 最大机器编号(1023)
         */
        private final long MAX_MACHINE_ID = ~(-1L << MACHINE_BIT);
        /**
         * 生成id机器标识部分
         */
        private long machineIdPart;
        /**
         * 序列号
         */
        private long sequence = 0L;
        /**
         * 上一次时间戳
         */
        private long lastTimestamp = -1L;

        /**
         * 最大容忍时间, 单位毫秒, 即如果时钟只是回拨了该变量指定的时间, 那么等待相应的时间即可;
         * 考虑到sequence服务的高性能, 这个值不易过大
         */
        private static final long MAX_BACKWARD_MS = 5;

        /**
         * 构造函数初始化机器编码
         */
        private SnowFlake() {
            // 获得本机机器编码
            long localIp;
            String address = null;
            try {
                InetAddress addr = IpUtils.getLocalHostLANAddress();
                address = addr.getHostAddress();
                localIp = IpUtils.ipToLong(address);
            } catch (UnknownHostException e) {
                log.error("获取本机IP异常", e);
                localIp = System.currentTimeMillis();
            }

            // 机器标识: localIp & MAX_MACHINE_ID最大不会超过1023, 再左位移12位
            machineIdPart = (localIp & MAX_MACHINE_ID) << SEQUENCE_BIT;
            log.info("Snowflake初始化, address={}, localIp={}, machineIdPart={}", address, localIp, machineIdPart);
        }

        /**
         * 获取雪花ID
         */
        public synchronized long nextId() {
            long currentTimestamp = timeGen();

            // 闰秒:如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
            if (currentTimestamp < lastTimestamp) {
                long offset = lastTimestamp - currentTimestamp;
                if (offset <= MAX_BACKWARD_MS) {
                    try {
                        this.wait(offset << 1);
                        currentTimestamp = timeGen();
                        if (currentTimestamp < lastTimestamp) {
                            log.error("时钟回拔, 再次获取系统时间依然小于上一次时间戳, currentTimestamp={}, lastTimestamp={}",
                                    currentTimestamp, lastTimestamp);
                            throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                        }
                    } catch (Exception e) {
                        log.error("时钟回拔, 生成SnowflakeID异常", e);
                        throw new RuntimeException(e);
                    }
                } else {
                    log.error("时钟回拔, 当前时间和上一次时间戳的差大于5ms, currentTimestamp={}, lastTimestamp={}",
                            currentTimestamp, lastTimestamp);
                    throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", offset));
                }
            }
            // 避免机器时钟回拨
            /*while (currentStamp < lastStamp) {
                //服务器时钟被调整了,ID生成器停止服务.
                throw new RuntimeException(String.format("时钟已经回拨.  Refusing to generate id for %d milliseconds", lastStamp - currentStamp));
            }*/

            // 同一毫秒内, 序号+1
            if (currentTimestamp == lastTimestamp) {
                sequence = (sequence + 1) & MAX_SEQUENCE;
                // 同一毫秒内序列号溢出
                if (sequence == 0) {
                    // 阻塞到下一个毫秒,获得新的时间戳
                    currentTimestamp = getNextMill();
                }
            } else {
                // 不同毫秒内,序号从0开始
                sequence = 0L;
            }
            lastTimestamp = currentTimestamp;
            // 时间戳部分+机器标识部分+序列号部分 (不同位表示不同的指标数)
            return (currentTimestamp - START_TIMESTAMP) << TIMESTAMP_LEFT | machineIdPart | sequence;
        }

        /**
         * 阻塞到下一个毫秒,直到获得新的时间戳
         */
        private long getNextMill() {
            long mill = timeGen();
            while (mill <= lastTimestamp) {
                mill = timeGen();
            }
            return mill;
        }

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

    /**
     * 获取long类型雪花ID
     */
    public static long uniqueLong() {
        return SnowflakeIdUtils.SnowFlake.SNOW_FLAKE.nextId();
    }

    /**
     * 获取String类型雪花ID
     */
    public static String uniqueLongHex() {
        return String.format("%016x", uniqueLong());
    }

    /**
     * 测试
     */
    public static void main(String[] args) throws InterruptedException {
        //计时开始时间
        long start = System.currentTimeMillis();
        //让100个线程同时进行
        final CountDownLatch latch = new CountDownLatch(100);
        //判断生成的20万条记录是否有重复记录
        final Map<Long, Integer> map = new ConcurrentHashMap();
        for (int i = 0; i < 100; i++) {
            //创建100个线程
            new Thread(() -> {
                for (int s = 0; s < 2000; s++) {
                    long snowID = SnowflakeIdUtils.uniqueLong();
                    log.info("生成雪花ID={}", snowID);
                    Integer put = map.put(snowID, 1);
                    if (put != null) {
                        log.error("主键重复snowID={}", snowID);
                        throw new RuntimeException("主键重复");
                    }
                }
                latch.countDown();
            }).start();
        }

        //让上面100个线程执行结束后,在走下面输出信息
        latch.await();
        log.info("生成20万条雪花ID总用时={}", System.currentTimeMillis() - start);

    }

}

主要优化部分是每个机器节点生成机器标识部分,这里使用IP做为生成机器标识编码的计算因子,先获取不同节点的服务器IP,如果获取IP抛出异常则取当前时间的毫秒数做为IP的long类型数字,目的是保证每个服务器的IP参数不一致。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值