雪花算法生成主键ID

今天学习的内容是使用雪花算法生成主键id。

一、算法原理

  • SnowFlake算法生成id的结果是一个64bit大小的整数,它的结构如下图:
    在这里插入图片描述

1. 1bit,预留位,不使用

因为二进制中最高位是符号位,1表示负数,0表示正数。生成的id一般都是用整数,所以最高位固定为0。

2. 41bit-时间戳

用来记录时间戳,毫秒级。41位可以表示个数字,如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至(2^41-1),减1是因为可表示的数值范围是从0开始算的,而不是1。
也就是说41位可以表示2^41-1个毫秒的值,转化成单位年则是69年
(2^41-1)/(3652460601000)=69年。

3. 10bit-工作机器id

用来记录工作机器id。可以部署在2^10 = 1024个节点,
包括5位datacenterId和5位workerId,5位(bit)可以表示的最大正整数是2^5-1 = 31,即可以用0、1、2、3、….31这32个数字来表示不同的datecenterId或workerId。

4. 12bit-序列号

序列号用来记录同毫秒内产生的不同id。12位(bit)可以表示的最大正整数是2^12-1 = 4095,即可以用0、1、2、3、….4094这4095个数字来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。
由于在Java中64bit的整数是long类型,所以在Java中SnowFlake算法生成的id就是long来存储的。

二、SnowFlake生成主键id优点

  1. 所有生成的id按时间趋势递增;
  2. 整个分布式系统内不会产生重复id(因为有datacenterId和workerId来做区分);

三、算法实现

1. 在common模块的utils中创建SnowflakeIdWorkerUtil类

在这里插入图片描述

2. SnowflakeIdWorkerUtil代码清单

package yooo.yun.com.common.utils;

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.SystemUtils;

import java.net.Inet4Address;
import java.net.UnknownHostException;
import java.util.Random;

/**
 * 雪花算法生成ID
 *
 * @author WangJiao
 * @since 2020/12/21
 */
@Slf4j
public class SnowflakeIdWorkerUtil {
  /** 开始时间截 (2020-01-01) */
  private final long startEpoch = 1577808000000L;

  /** 机器id所占的位数 */
  private final long workerIdBits = 4L;

  /** 数据标识id所占的位数 */
  private final long dataCenterIdBits = 4L;

  /** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
  private final long maxWorkerId = 15L;

  /** 支持的最大数据标识id,结果是31 */
  private final long maxDataCenterId = 15L;

  /** 序列在id中占的位数 */
  private final long sequenceBits = 5L;

  /** 机器ID向左移12位 */
  private final long workerIdShift = 5L;

  /** 数据标识id向左移17位(12+5) */
  private final long dataCenterIdShift = 9L;

  /** 时间截向左移22位(5+5+12) */
  private final long timestampLeftShift = 13L;

  /** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
  private final long sequenceMask = 31L;

  /** 工作机器ID(0~31) */
  private long workerId;

  /** 数据中心ID(0~31) */
  private long dataCenterId;

  /** 毫秒内序列(0~4095) */
  private long sequence = 0L;

  /** 上次生成ID的时间截 */
  private long lastTimestamp = -1L;

  private static final SnowflakeIdWorkerUtil idWorker;

  static {
    idWorker = new SnowflakeIdWorkerUtil(getWorkId(), getDataCenterId());
  }

  public SnowflakeIdWorkerUtil(long workerId, long dataCenterId) {
    log.info("SnowflakeIdWorkerUtil:[workerId:{},dataCenterId:{}]", workerId, dataCenterId);
    if (workerId <= 15L && workerId >= 0L) {
      if (dataCenterId <= 15L && dataCenterId >= 0L) {
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
      } else {
        throw new IllegalArgumentException(
            String.format("datacenter Id can't be greater than %d or less than 0", 15L));
      }
    } else {
      throw new IllegalArgumentException(
          String.format("worker Id can't be greater than %d or less than 0", 15L));
    }
  }

  /**
   * 工作机器id
   *
   * @return workId
   */
  private static Long getWorkId() {
    try {
      // 获取本机IP地址
      String hostAddress = Inet4Address.getLocalHost().getHostAddress();
      log.info("getWorkId:本地ip地址[hostAddress:{}]", hostAddress);
      int[] ints = StringUtils.toCodePoints(hostAddress);
      int sums = 0;
      for (int b : ints) {
        sums += b;
      }
      long l = (sums % 32);
      // WorkId太长,数据库使用的是long类型,如果按照原长度返回给前端,出现存入数据库正常,查询返回给前端后后两位变为0的情况,导致不正确.
      // js支持的最大整数是2的53次方减1,所以损失了精度;
      //
      // 解决办法:
      // 1.存储到数据库为varchar
      // 2.取出后返回前端前转为String类型
      // 3.取长度15位
      return l > 15 ? new Random().nextInt(15) : l;
    } catch (UnknownHostException e) {
      // 如果获取失败,则使用随机数备用
      return RandomUtils.nextLong(0, 15);
    }
  }

  private static Long getDataCenterId() {
    String hostName = SystemUtils.getHostName();
    log.info("getDataCenterId:[hostName:{}]", hostName);
    int[] ints = StringUtils.toCodePoints(hostName);
    int sums = 0;
    for (int i : ints) {
      sums += i;
    }
    long l = (sums % 32);
    return l > 15 ? new Random().nextInt(15) : l;
  }

  /**
   * 静态工具类
   *
   * @return id
   */
  public static synchronized Long generateId() {
    return idWorker.nextId();
  }

  public synchronized long nextId() {
    long timestamp = this.timeGen();
    if (timestamp < this.lastTimestamp) {
      throw new RuntimeException(
          String.format(
              "Clock moved backwards.  Refusing to generate id for %d milliseconds",
              this.lastTimestamp - timestamp));
    } else {
      if (this.lastTimestamp == timestamp) {
        this.sequence = this.sequence + 1L & 31L;
        if (this.sequence == 0L) {
          timestamp = this.tilNextMillis(this.lastTimestamp);
        }
      } else {
        this.sequence = 0L;
      }

      this.lastTimestamp = timestamp;
      return timestamp - 1529942400000L << 13
          | this.dataCenterId << 9
          | this.workerId << 5
          | this.sequence;
    }
  }

  protected long tilNextMillis(long lastTimestamp) {
    long timestamp = this.timeGen();
    while (timestamp <= lastTimestamp) {
      timestamp = this.timeGen();
    }

    return timestamp;
  }

  protected long timeGen() {
    return System.currentTimeMillis();
  }
}
  • 算法中会使用到RandomUtils和SystemUtils工具类,这两个工具类是apache.commons.lang3中的,项目中需要使用就要导入依赖;
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>${commons-lang3.version}</version>
</dependency>
<properties>
    <commons-lang3.version>3.9</commons-lang3.version>
</properties>
  • 生成的id是64位的,数据库使用的是long类型,存入数据库正常,如果按照原长度返回给前端,查询返回给前端后后几位变为0的情况,前端的再通过该不正常的id查询数据时,会报错,该数据不存在,导致不正确的原因:
    js支持的最大整数是2的53次方减1,所以损失了精度;
    解决办法:
    1.存储到数据库为varchar;
    2.取出后返回前端前转为String类型;
    3.取长度15位;(项目中我们取长度为15位)

3. 在common模块的config中创建CustomIdGenerator

在这里插入图片描述

4. CustomIdGenerator的代码清单

编写CustomIdGenerator实现IdentifierGenerator,在nextId()方法中调用我们自动生成的雪花算法主键id的方法,生成一个主键id。

package yooo.yun.com.common.config;

import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import org.springframework.stereotype.Component;
import yooo.yun.com.common.utils.SnowflakeIdWorkerUtil;

/**
 * 自定义ID生成器
 *
 * @author WangJiao
 * @since 2020/12/21
 */
@Component
public class CustomIdGenerator implements IdentifierGenerator {

  @Override
  public Long nextId(Object entity) {

    return SnowflakeIdWorkerUtil.generateId();
  }
}

5. 修改表主键id,设置成不自增id

在这里插入图片描述

6. 将实体内的主键id设置成自动生成id

  • 使用自增id时实体id配置如下:
@TableId(value = "id", type = IdType.AUTO)
private Long id;
  • 使用自动生成id作为主键,实体id配置如下:
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;

四、测试

1. 启动项目

在这里插入图片描述

2. postman调用接口

在这里插入图片描述

3. 查看数据库主键id

主键id为自动生成的15位的id
在这里插入图片描述
-:到这里,相信你已经get到了喔!!!
在这里插入图片描述

  • 7
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的雪花算法(Snowflake)可以用于生成唯一的主键ID。下面是一个简单的实现示例: ```java public class SnowflakeIdGenerator { private final static long START_TIMESTAMP = 1614556800000L; // 设置起始时间戳,这里假设为2021-03-01 00:00:00 private final static long DATA_CENTER_ID_BITS = 5L; // 数据中心ID所占的位数 private final static long WORKER_ID_BITS = 5L; // 工作机器ID所占的位数 private final static long SEQUENCE_BITS = 12L; // 序列号所占的位数 private final static long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_ID_BITS); // 支持的最大数据中心ID数量 private final static long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS); // 支持的最大工作机器ID数量 private final static long WORKER_ID_SHIFT = SEQUENCE_BITS; // 工作机器ID左移位数 private final static long DATA_CENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS; // 数据中心ID左移位数 private final static long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATA_CENTER_ID_BITS; // 时间戳左移位数 private final static long SEQUENCE_MASK = ~(-1L << SEQUENCE_BITS); // 序列号掩码 private long workerId; // 工作机器ID private long dataCenterId; // 数据中心ID private long sequence = 0L; // 序列号 private long lastTimestamp = -1L; // 上次生成ID的时间戳 public SnowflakeIdGenerator(long dataCenterId, long workerId) { 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.dataCenterId = dataCenterId; this.workerId = workerId; } public synchronized long generateId() { long currentTimestamp = System.currentTimeMillis(); if (currentTimestamp < lastTimestamp) { throw new RuntimeException("Clock moved backwards. Refusing to generate ID for " + (lastTimestamp - currentTimestamp) + " milliseconds"); } if (currentTimestamp == lastTimestamp) { sequence = (sequence + 1) & SEQUENCE_MASK; if (sequence == 0) { currentTimestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = currentTimestamp; return ((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT) | (dataCenterId << DATA_CENTER_ID_SHIFT) | (workerId << WORKER_ID_SHIFT) | sequence; } private long tilNextMillis(long lastTimestamp) { long timestamp = System.currentTimeMillis(); while (timestamp <= lastTimestamp) { timestamp = System.currentTimeMillis(); } return timestamp; } } ``` 使用示例: ```java public class Main { public static void main(String[] args) { // 创建一个SnowflakeIdGenerator实例,传入数据中心ID和工作机器ID SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1); // 生成ID long id = idGenerator.generateId(); System.out.println("Generated ID: " + id); } } ``` 这样就可以使用雪花算法Java生成唯一的主键ID了。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值