开箱即用轻量级雪花算法id生成器Java工具类

开箱即用轻量级雪花算法id生成器Java工具类

    • [1.1 背景](#1.1 背景)
    • [1.2 雪花算法id生成器Java工具类](#1.2 雪花算法id生成器Java工具类)

1.1 背景

在 Java后端研发过程中,对于分布式微服务来说,一般需要分布式 id生成.

这里分享一个非常好用且大多数情况下都可用的开箱即用轻量级雪花算法id生成器Java工具类。

这种方式生成的雪花算法生成器生成的唯一主键id,好处是不依赖第三方组件,轻量级,缺点是服务器的时钟不可以回拨,否则可能会造成生成的主键id冲突。

1.2 雪花算法id生成器Java工具类

雪花算法id生成器Java工具类源码

java 复制代码
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
 
/**
 * id自增器(雪花算法)
 * @author renjie
 * @version 1.0.0
 */
public class SnowFlakeUtils {
    private final static long twepoch = 12888349746579L;
    /**
     * 机器标识位数
     */
    private final static long workerIdBits = 5L;
    /**
     * 数据中心标识位数
     */
    private final static long datacenterIdBits = 5L;
    /**
     * 毫秒内自增位数
     */
    private final static long sequenceBits = 12L;
    /**
     * 机器ID偏左移12位
     */
    private final static long workerIdShift = sequenceBits;
    /**
     * 数据中心ID左移17位
     */
    private final static long datacenterIdShift = sequenceBits + workerIdBits;
    /**
     * 时间毫秒左移22位
     */
    private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
    /**
     * sequence掩码,确保sequnce不会超出上限
     */
    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
    /**
     * 上次时间戳
     */
    private static long lastTimestamp = -1L;
    /**
     * 序列
     */
    private long sequence = 0L;
    /**
     * 服务器ID
     */
    private long workerId = 1L;
    private static long workerMask = -1L ^ (-1L << workerIdBits);
    /**
     * 进程编码
     */
    private long processId = 1L;
    private static long processMask = -1L ^ (-1L << datacenterIdBits);
 
    private static SnowFlakeUtils snowFlakeUtils = null;
 
    static{
        snowFlakeUtils = new SnowFlakeUtils();
    }
    public static synchronized long nextId(){
        return snowFlakeUtils.getNextId();
    }
 
    private SnowFlakeUtils() {
 
        //获取机器编码
        this.workerId=this.getMachineNum();
        //获取进程编码
        RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
        this.processId=Long.valueOf(runtimeMXBean.getName().split("@")[0]).longValue();
 
        //避免编码超出最大值
        this.workerId=workerId & workerMask;
        this.processId=processId & processMask;
    }
 
    public synchronized long getNextId() {
        //获取时间戳
        long timestamp = timeGen();
        //如果时间戳小于上次时间戳则报错
        if (timestamp < lastTimestamp) {
            try {
                throw new Exception("Clock moved backwards.  Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //如果时间戳与上次时间戳相同
        if (lastTimestamp == timestamp) {
            // 当前毫秒内,则+1,与sequenceMask确保sequence不会超出上限
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                // 当前毫秒内计数满了,则等待下一秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }
        lastTimestamp = timestamp;
        // ID偏移组合生成最终的ID,并返回ID
        long nextId = ((timestamp - twepoch) << timestampLeftShift) | (processId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
        return nextId;
    }
 
    /**
     * 再次获取时间戳直到获取的时间戳与现有的不同
     * @param lastTimestamp
     * @return 下一个时间戳
     */
    private long tilNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }
 
    private long timeGen() {
        return System.currentTimeMillis();
    }
 
    /**
     * 获取机器编码
     * @return
     */
    private long getMachineNum(){
        long machinePiece;
        StringBuilder sb = new StringBuilder();
        Enumeration<NetworkInterface> e = null;
        try {
            e = NetworkInterface.getNetworkInterfaces();
        } catch (SocketException e1) {
            e1.printStackTrace();
        }
        while (e.hasMoreElements()) {
            NetworkInterface ni = e.nextElement();
            sb.append(ni.toString());
        }
        machinePiece = sb.toString().hashCode();
        return machinePiece;
    }
}

注意事项

这个雪花算法实现是一个常见的分布式唯一ID生成器,但它也有一些潜在的问题和优化空间:

  • 唯一性保证
    • 该算法的唯一性依赖于每个节点的机器ID(workerId)和数据中心ID(processId)。如果这些ID在不同节点上配置不正确,可能导致ID冲突。
    • 唯一性还依赖于时钟同步。如果时钟回拨(Clock Drift)或不稳定,可能会导致生成的ID不唯一。
  • 时钟回拨问题:
    • 该算法没有显式处理时钟回拨情况。如果系统时钟发生回拨,可能会导致生成的ID不连续,甚至出现重复。
    • 优化:您可以考虑在时钟回拨检测到时采取适当的措施,例如等待或重新生成ID。
  • 长期运行问题
    • 如果您的应用长期运行,可能会达到每毫秒生成的ID数量的限制,尤其是在高并发情况下。
    • 优化:可以根据实际需求调整位数,以提高每毫秒生成ID的上限。
  • 可扩展性问题
    • 该算法中的位数分配不一定适用于所有应用。如果您的应用需要更多的数据中心或机器节点,需要重新分配位数。
    • 优化:可以根据需求增加位数分配的灵活性,以支持更多的数据中心或机器。
  • 不适用于移动设备:
    • 该算法依赖于机器ID和数据中心ID的配置,对于移动设备等无法确定唯一ID的环境不适用。
    • 优化:可以考虑在这种情况下使用其他唯一ID生成策略。
  • 异常处理
    • 该算法在出现异常情况时没有提供良好的处理机制,例如时钟回拨异常。
    • 优化:添加适当的异常处理,以确保生成的ID在异常情况下也是正确的。
  • 总的来说,这个雪花算法实现在大多数情况下都能正常工作,但在特定情况下可能会出现问题。
  • 优化的策略会根据应用的具体需求而变化,可以根据需要进行调整,以确保生成的ID满足唯一性、连续性和性能要求。
  • 如果您需要更高级的解决方案,还可以考虑使用分布式ID生成服务或其他专用工具。
相关推荐
Mr_Xuhhh36 分钟前
项目需求分析(2)
c++·算法·leetcode·log4j
橙*^O^*安1 小时前
Go 语言基础:变量与常量
运维·开发语言·后端·golang·kubernetes
NiKo_W1 小时前
Linux 文件系统与基础指令
linux·开发语言·指令
c++bug1 小时前
六级第一关——下楼梯
算法
BillKu1 小时前
推荐 Eclipse Temurin 的 OpenJDK
java·ide·eclipse
Morri31 小时前
[Java恶补day53] 45. 跳跃游戏Ⅱ
java·算法·leetcode
悟能不能悟1 小时前
eclipse怎么把项目设为web
java·eclipse
工程师小星星1 小时前
Golang语言的文件组织方式
开发语言·后端·golang
乂爻yiyao1 小时前
java 代理模式实现
java·开发语言·代理模式
林木辛2 小时前
LeetCode热题 15.三数之和(双指针)
算法·leetcode·双指针