雪花算法生成主键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优点
- 所有生成的id按时间趋势递增;
- 整个分布式系统内不会产生重复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到了喔!!!