封装一个id生成器

本文介绍了如何使用基于Twitter的Snowflake算法实现分布式ID生成器,详细解析了算法结构和优化点,包括时间回拨处理、随机序列生成等。并提供了一个Java实现的示例,通过SystemClock提升高并发场景下获取时间戳的性能。此外,还分享了相关的开源项目链接和进一步阅读资源。
摘要由CSDN通过智能技术生成
 
ID号生成器(或:全局唯一ID生成器)是服务端系统的基础设施,而且id后端接触很频繁,是逃不开的。而关于ID生成的算法现在业界首屈一指的当属 Snowflake 雪花算法。本文采用的是 基于Twitter的Snowflake算法实现分布式高效有序ID生产黑科技(sequence)——升级版Snowflake,直接使用Gitee当中的开源项目 https://gitee.com/yu120/sequence
SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 
直接将开源项目中的Sequence类和systemClock类复制到我们本地项目的clock方法当中,Sequence(我这里做了些许改动)
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.concurrent.ThreadLocalRandom;

/**
 * 基于Twitter的Snowflake算法实现分布式高效有序ID生产黑科技(sequence)——升级版Snowflake
 *
 * <br>
 * SnowFlake的结构如下(每部分用-分开):<br>
 * <br>
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
 * <br>
 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
 * <br>
 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
 * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下START_TIME属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
 * <br>
 * 10位的数据机器位,可以部署在1024个节点,包括5位dataCenterId和5位workerId<br>
 * <br>
 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
 * <br>
 * <br>
 * 加起来刚好64位,为一个Long型。<br>
 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
 * <p>
 * <p>
 * 特性:
 * 1.支持自定义允许时间回拨的范围<p>
 * 2.解决跨毫秒起始值每次为0开始的情况(避免末尾必定为偶数,而不便于取余使用问题)<p>
 * 3.解决高并发场景中获取时间戳性能问题<p>
 * 4.支撑根据IP末尾数据作为workerId
 * 5.时间回拨方案思考:1024个节点中分配10个点作为时间回拨序号(连续10次时间回拨的概率较小)
 *
 * @author lry
 * @version 3.0
 */

public final class Sequence {


    /**
     * 起始时间戳
     **/
    private final static long START_TIME = 1288834974657L;

    /**
     * dataCenterId占用的位数:5
     **/
    private final static long DATA_CENTER_ID_BITS = 5L;
    /**
     * workerId占用的位数:5
     **/
    private final static long WORKER_ID_BITS = 5L;
    /**
     * 序列号占用的位数:12(表示只允许workId的范围为:0-4095)
     **/
    private final static long SEQUENCE_BITS = 12L;

    private final static long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);

    private final static long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS);

    private final static long WORKER_ID_SHIFT = SEQUENCE_BITS;
    private final static long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS;


    /**
     * 用mask防止溢出:位与运算保证计算的结果范围始终是 0-4095
     **/
    private final static long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS);

    private final long workerId;
    private final long dataCenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    private static byte LAST_IP = 0;
    private final boolean clock;
    private final long timeOffset;
    private final boolean randomSequence;
    private final ThreadLocalRandom tlr = ThreadLocalRandom.current();

    public Sequence(long dataCenterId) {
        this(dataCenterId, 0x000000FF & getLastIPAddress(), false, 5L, false);
    }

    public Sequence(long dataCenterId, boolean clock, boolean randomSequence) {
        this(dataCenterId, 0x000000FF & getLastIPAddress(), clock, 5L, randomSequence);
    }

    /**
     * 基于Snowflake创建分布式ID生成器
     *
     * @param dataCenterId   数据中心ID,数据范围为0~255
     * @param workerId       工作机器ID,数据范围为0~3
     * @param clock          true表示解决高并发下获取时间戳的性能问题
     * @param timeOffset     允许时间回拨的毫秒量,建议5ms
     * @param randomSequence true表示使用毫秒内的随机序列(超过范围则取余)
     */
    public Sequence(long dataCenterId, long workerId, boolean clock, long timeOffset, boolean randomSequence) {
        if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
            throw new IllegalArgumentException("Data Center Id can't be greater than " + MAX_DATA_CENTER_ID + " or less than 0");
        }
        if (workerId > MAX_WORKER_ID || workerId < 0) {
            throw new IllegalArgumentException("Worker Id can't be greater than " + MAX_WORKER_ID + " or less than 0");
        }

        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
        this.clock = clock;
        this.timeOffset = timeOffset;
        this.randomSequence = randomSequence;
    }

    /**
     * 获取ID
     *
     * @return long
     */
    public synchronized Long nextId() {
        long currentTimestamp = this.timeGen();

        // 闰秒:如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过,这个时候应当抛出异常
        if (currentTimestamp < lastTimestamp) {
            // 校验时间偏移回拨量
            long offset = lastTimestamp - currentTimestamp;
            if (offset > timeOffset) {
                throw new RuntimeException("Clock moved backwards, refusing to generate id for [" + offset + "ms]");
            }

            try {
                // 时间回退timeOffset毫秒内,则允许等待2倍的偏移量后重新获取,解决小范围的时间回拨问题
                this.wait(offset << 1);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            // 再次获取
            currentTimestamp = this.timeGen();
            // 再次校验
            if (currentTimestamp < lastTimestamp) {
                throw new RuntimeException("Clock moved backwards, refusing to generate id for [" + offset + "ms]");
            }
        }

        // 同一毫秒内序列直接自增
        if (lastTimestamp == currentTimestamp) {
            // randomSequence为true表示随机生成允许范围内的序列起始值并取余数,否则毫秒内起始值为0L开始自增
            long tempSequence = sequence + 1;
            if (randomSequence && tempSequence > SEQUENCE_MASK) {
                tempSequence = tempSequence % SEQUENCE_MASK;
            }

            // 通过位与运算保证计算的结果范围始终是 0-4095
            sequence = tempSequence & SEQUENCE_MASK;
            if (sequence == 0) {
                currentTimestamp = this.tilNextMillis(lastTimestamp);
            }
        } else {
            // randomSequence为true表示随机生成允许范围内的序列起始值,否则毫秒内起始值为0L开始自增
            sequence = randomSequence ? tlr.nextLong(SEQUENCE_MASK + 1) : 0L;
        }

        lastTimestamp = currentTimestamp;
        long currentOffsetTime = currentTimestamp - START_TIME;

        /*
         * 1.左移运算是为了将数值移动到对应的段(41、5、5,12那段因为本来就在最右,因此不用左移)
         * 2.然后对每个左移后的值(la、lb、lc、sequence)做位或运算,是为了把各个短的数据合并起来,合并成一个二进制数
         * 3.最后转换成10进制,就是最终生成的id
         */
        return (currentOffsetTime << TIMESTAMP_LEFT_SHIFT) |
                // 数据中心位
                (dataCenterId << DATA_CENTER_ID_SHIFT) |
                // 工作ID位
                (workerId << WORKER_ID_SHIFT) |
                // 毫秒序列化位
                sequence;
    }

    /**
     * 保证返回的毫秒数在参数之后(阻塞到下一个毫秒,直到获得新的时间戳)——CAS
     *
     * @param lastTimestamp last timestamp
     * @return next millis
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            // 如果发现时间回拨,则自动重新获取(可能会处于无限循环中)
            timestamp = this.timeGen();
        }

        return timestamp;
    }

    /**
     * 获得系统当前毫秒时间戳
     *
     * @return timestamp 毫秒时间戳
     */
    private long timeGen() {
        return clock ? SystemClock.INSTANCE.currentTimeMillis() : System.currentTimeMillis();
    }

    /**
     * 用IP地址最后几个字节标示
     * <p>
     * eg:192.168.1.30->30
     *
     * @return last IP
     */
    public static byte getLastIPAddress() {
        if (LAST_IP != 0) {
            return LAST_IP;
        }

        try {
            InetAddress inetAddress = InetAddress.getLocalHost();
            byte[] addressByte = inetAddress.getAddress();
            LAST_IP = addressByte[addressByte.length - 1];
        } catch (Exception e) {
            throw new RuntimeException("Unknown Host Exception", e);
        }

        return LAST_IP;
    }

    /**
     * 数据标识id部分
     */
    protected static long getDatacenterId() {

        long id = 0L;
        try {
            final InetAddress ip = InetAddress.getLocalHost();
            final NetworkInterface network = NetworkInterface.getByInetAddress(ip);
            if (network == null) {
                id = 1L;
            } else {
                final byte[] mac = network.getHardwareAddress();
                if (null != mac) {
                    id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6;
                    id = id % (Sequence.MAX_DATA_CENTER_ID + 1);
                }
            }
        } catch (final Exception e) {
            e.printStackTrace();
        }
        return id;
    }

    /**
     * 获取 maxWorkerId
     */
    protected static long getMaxWorkerId() {

        final StringBuilder mpid = new StringBuilder();
        mpid.append(Sequence.MAX_DATA_CENTER_ID);
        /*
         * MAC + PID 的 hashcode 获取16个低位
         */
        return (mpid.toString().hashCode() & 0xffff) % (Sequence.MAX_WORKER_ID + 1);
    }
}

实际上就是一个雪花算法 。我在上面稍微改动了一点。

之后systemClock类:


import java.sql.Timestamp;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

/**
 * System Clock
 * <p>
 * 利用ScheduledExecutorService实现高并发场景下System.curentTimeMillis()的性能问题的优化.
 *
 * @author lry
 */
public enum SystemClock {

    // ====

    INSTANCE(1);

    private final long period;
    private final AtomicLong nowTime;
    private boolean started = false;
    private ScheduledExecutorService executorService;

    SystemClock(long period) {
        this.period = period;
        this.nowTime = new AtomicLong(System.currentTimeMillis());
    }

    /**
     * The initialize scheduled executor service
     */
    public void initialize() {
        if (started) {
            return;
        }

        this.executorService = new ScheduledThreadPoolExecutor(1, r -> {
            Thread thread = new Thread(r, "system-clock");
            thread.setDaemon(true);
            return thread;
        });
        executorService.scheduleAtFixedRate(() -> nowTime.set(System.currentTimeMillis()),
                this.period, this.period, TimeUnit.MILLISECONDS);
        Runtime.getRuntime().addShutdownHook(new Thread(this::destroy));
        started = true;
    }

    /**
     * The get current time milliseconds
     *
     * @return long time
     */
    public long currentTimeMillis() {
        return started ? nowTime.get() : System.currentTimeMillis();
    }

    /**
     * The get string current time
     *
     * @return string time
     */
    public String currentTime() {
        return new Timestamp(currentTimeMillis()).toString();
    }

    /**
     * The destroy of executor service
     */
    public void destroy() {
        if (executorService != null) {
            executorService.shutdown();
        }
    }

}

在此之后我们封装自己的方法:


import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class IdUtils {

    /**
     * 主机和进程的机器码
     */
    private static Sequence WORKER = new Sequence(Sequence.getDatacenterId(),Sequence.getMaxWorkerId(),true,5,true);



    /**
     * 毫秒格式化时间
     */
    public static final DateTimeFormatter MILLISECOND = DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS");

    public static long getId() {

        return WORKER.nextId();
    }

    public static Long getIdLong() {

        return Long.valueOf(WORKER.nextId());
    }

    public static String getIdStr() {

        return String.valueOf(WORKER.nextId());
    }

    /**
     * 格式化的毫秒时间
     */
    public static String getMillisecond() {

        return LocalDateTime.now().format(MILLISECOND);
    }

    /**
     * 时间 ID = Time + ID
     */
    public static String getTimeId() {

        return getMillisecond() + getId();
    }

}

最后我们可以写个方法测试一下:

  public static void main(String[] args) {
        for (int i = 0;i<100;i++){
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(IdUtils.getId());
                }
            });
            t.start();
        }
    }

结果为:

这样一个id生成器就搞定了。

参考:https://gitee.com/yu120/sequence

另外推荐两个我在学习过程中遇到的文章:封装一个流水号ID生成器:id-spring-boot-starte https://www.codesheep.cn/2019/09/04/id-springbt-starter/

全局唯一id生成器 vesta :https://blog.csdn.net/lyf_ldh/article/details/95203342

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Mybatis-Plus代码生成器一个用于快速生成Java代码的工具,它是基于MyBatis核心框架进行封装的。它结合了Mybatis-Plus自身的代码增强功能和Mybatis的XML配置文件生成器,可以帮助开发者快速生成Java实体类、XML配置文件、Mapper接口以及Service和Controller等代码,从而提高开发效率。 使用Mybatis-Plus代码生成器可以遵循以下步骤: 1. 首先,确保你已经在项目中引入了Mybatis-Plus的依赖。 2. 在项目的配置文件中,配置好数据库连接信息。 3. 创建一个代码生成器类,可以使用Mybatis-Plus提供的`AutoGenerator`类。 4. 在代码生成器类中,设置生成代码的相关配置,例如生成的包名、作者信息、表名等。 5. 调用代码生成器的`execute`方法,开始生成代码。 下面是一个示例代码,演示了如何使用Mybatis-Plus代码生成器生成Java实体类和Mapper接口: ```java import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.config.DataSourceConfig; import com.baomidou.mybatisplus.generator.config.GlobalConfig; import com.baomidou.mybatisplus.generator.config.PackageConfig; import com.baomidou.mybatisplus.generator.config.StrategyConfig; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; public class CodeGenerator { public static void main(String[] args) { // 数据源配置 DataSourceConfig dataSourceConfig = new DataSourceConfig(); dataSourceConfig.setDbType(DbType.MYSQL); dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/mybatis_plus_demo?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8"); dataSourceConfig.setUsername("root"); dataSourceConfig.setPassword("123456"); // 全局配置 GlobalConfig globalConfig = new GlobalConfig(); globalConfig.setOutputDir(System.getProperty("user.dir") + "/src/main/java"); globalConfig.setAuthor("Your Name"); globalConfig.setOpen(false); globalConfig.setIdType(IdType.AUTO); // 包配置 PackageConfig packageConfig = new PackageConfig(); packageConfig.setParent("com.example.demo"); packageConfig.setEntity("entity"); packageConfig.setMapper("mapper"); // 策略配置 StrategyConfig strategyConfig = new StrategyConfig(); strategyConfig.setNaming(NamingStrategy.underline_to_camel); strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel); strategyConfig.setEntityLombokModel(true); strategyConfig.setRestControllerStyle(true); strategyConfig.setInclude("user"); // 要生成代码的表名 // 代码生成器 AutoGenerator autoGenerator = new AutoGenerator(); autoGenerator.setDataSource(dataSourceConfig); autoGenerator.setGlobalConfig(globalConfig); autoGenerator.setPackageInfo(packageConfig); autoGenerator.setStrategy(strategyConfig); // 执行生成代码 autoGenerator.execute(); } } ``` 这段代码会根据配置信息自动生成Java实体类和Mapper接口,生成的文件会保存在指定的包路径下。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值