雪花id相关

雪花ID

1.为什么用雪花id

因为自增id有很多缺点

1.在数据量大的时候,需要分库分表,这时会出现重复的订单编号

2.自增id 是数据库生成的,效率很低,不能满足同时生成很多订单编号

2.自定义算法生成唯一编号,作为主键

1.UUID

2.雪花id

3.自定义算法生成

3.机器时钟回拨问题-->雪花id重复

解决方案:

<1s在代码中睡眠(等机器时钟回拨完成后再生成雪花id)

1-3s换成一个机器生成雪花id

>3s让该机器下线,(时钟对好再上线)

一、UUID

UUID(Universally Unique Identifier,通用唯一识别码)是按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片 ID 码和许多可能的数字。

UUID 是由一组 32 位数组成,由16 进制数字所构成,是故 UUID 理论上的总数为16的32次方。这个总数是多大呢?打个比方,如果每纳秒产生 1 百万个 UUID,要花 100 亿年才会将所有 UUID 用完。

UUID 通常以连字号分隔的五组来显示,形式为 8-4-4-4-12,总共有 36 个字符(即 32 个英数字母和 4 个连字号)。例如: 123e4567-e89b-12d3-a456-426655440000

JDK 从 1.5 开始在 java.util 包下提供了一个 UUID 类用来生成 UUID:

UUID uuid = UUID.randomUUID();
String uuidStr1 = uuid.toString();
String uuidStr2 = uuidStr1.replaceAll("-","");

1、UUID 的缺点和一个『好』ID 的标准

UUID的缺点:

为了得到一个全局唯一 ID,很自然地就会想到 UUID 算法。但是,UUID 算法有明显的缺点:

  • UUID 太长了,通常以 36 长度的字符串表示,很多场景不适用。

  • 非纯数字。UUID 中会出现 ABCDEF 这些十六进制的字母,因此,在数据库和代码中,自然就不能存储在整型字段或变量。因此,在数据库中以它作为主键,建立索引的代价比较大,性能有影响。

  • 不安全。UUID 中会包含网卡的 MAC 地址。

一个『好』ID 的标准应该有哪些:

  • 最好是由纯数字组成。

  • 越短越好,最好能存进整型变量和数据库的整型字段中。

  • 信息安全。另外,『ID 连续』并非好事情。

  • 在不连续的情况下,最好是递增的。即便不是严格递增,至少也应该是趋势递增。

二、Twitter 的雪花算法(SnowFlake)

Snowflake 是 Twitter(美国推特公司)开源的分布式 ID 生成算法。最初 Twitter 把存储系统从 MySQL 迁移到 Cassandra(它是NoSQL数据库),因为Cassandra 没有顺序 ID 生成机制,所以 Twitter 开发了这样一套全局唯一 ID 生成服务。

SnowFlake 优点:

整体上按照时间自增排序,并且整个分布式系统内不会产生 ID 碰撞(由数据中心 ID 和机器 ID 作区分),并且效率较高。经测试,SnowFlake 每秒能够产生 26 万 ID 左右。

Snowflake 会生成一个 long 类型的数值,long是8个字节,一共是64位,Snowflake 对于 long 的各个位都有固定的规范:

  • 最高位标识(1 位)

    由于 long 基本类型在 Java 中是带符号的,最高位是符号位,正数是 0,负数是 1,因为 id 一般是正数,所以最高位是 0 。

  • 毫秒级时间戳(41 位)

    注意,41 位时间戳不是存储当前时间的时间戳,而是存储时间的差值(当前时间戳 - 开始时间戳) 得到的值,这里的的开始时间,一般是我们的 id 生成器开始使用的时间,由我们程序来指定的(如下面程序 IdGenerator 类的 startTime 属性)。

    41 位的时间截,可以使用 69 年。

    2的41次方 除以 (1000毫秒 * 60 * 60 * 24 * 365) = 69

  • 数据机器位(10 位)

    10-bit机器可以分别表示1024台机器,这 10 位的机器位实际上是由 5 位的 互联网数据中心(datacenterId) 和 5 位的工作机器id(workerId) 。这样就可以有32个互联网数据中心(机房)(2的5次方),每个互联网数据中心可以有32台工作机器 。即,总共允许存在 1024 台电脑各自计算 ID 。

    每台电脑都由 data-center-id 和 worker-id 标识,逻辑上类似于联合主键的意思。

  • 12位的自增序列号,用来记录同毫秒内产生的不同id,就是一毫秒内最多可以产生4096个id

    毫秒内的计数,12为的自增序列号 支持每个节点每毫秒(同一机器,同一时间截)产生 4096(2的12次方) 个 ID 序号,这种分配方式可以保证在任何一个互联网数据中心的任何一台工作机器在任意毫秒内生成的ID都是不同的

面试常问:如果是并发量高,同一台机器一毫秒有5000个id,那么id会不会重复?

不会,根据源码如果一毫秒内超过4096个id,则会阻塞到下一毫秒再生成

1、Snowflake 实现源码

public class SnowflakeIdGenerator {
   package com.zzw.util;

public class SnowflakeIdGenerator {
    // ==============================Fields===========================================

    // 所占位数、位移、掩码/极大值
    private static final long sequenceBits = 12;  //序列号占用位数
    private static final long workerIdBits = 5;  //工作机器占用位数
    private static final long dataCenterIdBits = 5;  //数据中心占用位数(机房)

    //~表示非,例如 01 的非  10     负数的二进制 = 该正数的二进制取反+1
    //为什么不直接写4095呢?(主要计算机运算的时候是二进制,如果写4095的话,还是要转二进制,效率低)
    private static final long sequenceMask = ~(-1L << sequenceBits);  //4095  (0到4095 刚好是4096个)  


    private static final long workerIdShift = sequenceBits; //12 
    private static final long workerIdMask = ~(-1L << workerIdBits); //31


    private static final long dataCenterIdShift = sequenceBits + workerIdBits;  //17
    private static final long dataCenterIdMask = ~(-1L << dataCenterIdBits); //31 

    private static final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;//22
    //private static final long timestampBits = 41L; 
    //private static final long timestampMask = ~(-1L << timestampBits);//2199023255551

    /**
     * 开始时间截 (2015-01-01)  1420070400000L/1000/60/60/24/30/12 = 25+1970 = 2015-01-01
     */
    private static final long twepoch = 1420070400000L;

    private long sequence = 0;  //序列号
    private long workerId;      //工作机器标识
    private long dataCenterId;  //数据中心
    private long lastTimestamp = -1L; //上次生成 ID 的时间截

    //==============================Constructors=====================================

    public SnowflakeIdGenerator() {
        this(0, 0);
    }

    /**
     * 构造函数
     *
     * @param workerId     工作ID (0~31)
     * @param dataCenterId 数据中心 ID (0~31)
     */
    public SnowflakeIdGenerator(long workerId, long dataCenterId) {
        if (workerId > workerIdMask || workerId < 0) {
            throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", workerIdMask));
        }

        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }

    // ============================== Methods ==========================================

    /**
     * 获得下一个 ID (该方法是线程安全的,synchronized)
     */
    public synchronized long nextId() throws InterruptedException {
        long timestamp = timeGen(); //获取当前服务器时间

        // 如果当前时间小于上一次 ID 生成的时间戳,说明系统时钟回退过,这个时候应当抛出异常。
        // 出现这种原因是因为系统的时间被回拨,或出现闰秒现象。
        // 你也可以不抛出异常,而是调用 tilNextMillis 进行等待
        if (timestamp < lastTimestamp) {

            Thread.sleep(3000);
            timestamp = timeGen();
            if (timestamp < lastTimestamp) {

                throw new RuntimeException(
                        String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
            }

            throw new RuntimeException(
                    String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        // 如果是同一时间生成的,则时并发量高的情况下,同一毫秒内最大支持4096个id,否则阻塞到下一秒生成
        if (lastTimestamp == timestamp) {
            // 相同毫秒内,序列号自增  , sequence = 4095时, 0 = (sequence + 1) & sequenceMask
            sequence = (sequence + 1) & sequenceMask;
            // 毫秒内序列溢出,即,同一毫秒的序列数已经达到最大
            if (sequence == 0) {
                // 阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        // 时间戳改变,毫秒内序列重置
        else {
            sequence = 0L;
        }

        // 将当前生成的时间戳记录为『上次时间戳』。『下次』生成时间戳时要用到。
        lastTimestamp = timestamp;

        // 移位并通过或运算拼到一起组成 64 位的 ID = 8个字节
        return ((timestamp - twepoch) << timestampLeftShift) // 时间毫秒数左移22位
                | (dataCenterId << dataCenterIdShift) //数据中心节点左移17位
                | (workerId << workerIdShift) // 机器节点左移12位
                | sequence;
    }

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

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

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

    //==============================Test=============================================

    /**
     * 测试
     */
    public static void main(String[] args) throws InterruptedException {
        System.out.println(System.currentTimeMillis());
        SnowflakeIdGenerator idWorker = new SnowflakeIdGenerator(1, 1);
        long startTime = System.nanoTime();
        for (int i = 0; i < 50000; i++) {
            long id = idWorker.nextId();
            System.out.println(id);
        }
        System.nanoTime(); //获取当前纳秒
        System.out.println((System.nanoTime() - startTime) / 1000000 + "ms");
    }
}

}

2、解决时间回拨问题

原生的 Snowflake 算法是完全依赖于时间的,如果有时钟回拨的情况发生,会生成重复的 ID,市场上的解决方案也是不少。简单粗暴的办法有:

  • 采用直接抛异常方式:上面就是这种方式,虽然可行,但是这种很不友好,太粗暴

  • 使用阿里云的的时间服务器和自己的服务器进行同步,2017 年 1 月 1 日的闰秒调整,阿里云服务器 NTP 系统 24 小时“消化”闰秒,完美解决了问题。

  • [root@localhost ~]# ntpdate ntp1.aliyun.com

  • 如果发现有时钟回拨,时间很短比如 3 毫秒(一般大于3毫秒就不建议等待),就等待(线程睡3秒再来生成id),然后再生成。

  • public synchronized long nextId() {
            long timestamp = timeGen(); //获取当前服务器时间
    
            if (timestamp < lastTimestamp) {
                Thread.sleep(3000)
                timestamp =   timeGen();
                if(timestamp < lastTimestamp){
                    
                     throw new RuntimeException(
                        String.format("Clock moved backw ....", lastTimestamp - timestamp));
                }
                 
            }
         ......
     }

  • 集群:如某台服务器准备一个备机或者多个备机,当主服务器出现异常情况时,可以选择备机

三、mybatis plus实现雪花id

mybatis-plus已经内置雪花算法生成分布式唯一id。在mybatis-plus特性中已经明确说明了这点。

我们可以直接在IDEA中双击shift搜索Sequence类查看其具体实现,可以发现其实现就是采用了雪花算法。

修改实体类

@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@TableName("rbac_user")
public class UserPo implements Serializable {
    private static final long serialVersionUID = 1L;
   @TableId(value = "id", type = IdType.ASSIGN_ID)  //注意看这里,注意看这里
    private Long id;
    private String username;
    private String password;
    private boolean status;  //true:正常   false:锁定
    private String email;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Spring Boot中使用雪花ID算法可以确保生成分布式系统中唯一的ID雪花ID***ID、机器ID和自增序列号。下面是使用雪花ID生成器的步骤: 1. 添加雪花ID生成器的依赖:在`pom.xml`文件中添加以下依赖: ```xml <dependency> <groupId>com.github.snowflake</groupId> <artifactId>snowflake</artifactId> <version>3.0.0</version> </dependency> ``` 2. 创建一个雪花ID生成器的配置类:在Spring Boot项目中创建一个配置类,例如`SnowflakeConfig.java`,并添加以下代码: ```java import com.github.snowflake.SnowFlakeIdGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SnowflakeConfig { private static final long DATA_CENTER_ID = 1L; // 数据中心ID private static final long MACHINE_ID = 1L; // 机器ID @Bean public SnowFlakeIdGenerator snowFlakeIdGenerator() { return new SnowFlakeIdGenerator(DATA_CENTER_ID, MACHINE_ID); } } ``` 3. 使用雪花ID生成器生成唯一ID:在需要生成唯一ID的地方,注入`SnowFlakeIdGenerator`并调用`nextId()`方法即可生成唯一ID,例如: ```java import com.github.snowflake.SnowFlakeIdGenerator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class MyService { @Autowired private SnowFlakeIdGenerator snowFlakeIdGenerator; public long generateUniqueId() { return snowFlakeIdGenerator.nextId(); } } ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值