背景
近几日,被主键ID生成折磨的不太行,于是就在寻找一种合适的主键生成策略,选择一种合适的主键生成策略,可以大大降低主键ID的维护成本。
主键ID生成方法
最常用的4种主键ID生成方法
- UUID:全局唯一性,但是生成的ID是无序的且长度过长,单纯的就无序这一点,数据库中就不建议使用,因为数据库会为主键创建唯一索引,主键无序的话索引维护代价太大。
- 数据库自增ID:自增ID单机环境其实还好,但是分布式环境下如果并发量过高,就需要集群部署,那么这个时候,为了避免主键冲突,就需要设置步长来自增ID,这样的话成本也是很高的。
- Redis的INCR:这种方式主要是因为redis单线程,天生满足原子性,可以用自带的INCR去生成唯一ID,但是,这样也存在问题,并发量太大的时候,需要集群部署,也就需要设置不同的步长,并且它的key还有过期策略,维护这样的一个ID生成策略成本也是很高的。
- 雪花算法:SnowFlake算法是Twitter公司推出的专门针对分布式ID的解决方案,着重介绍一下。
雪花算法介绍
雪花算法结构:符号位+时间戳+工作进程位+序列号位,一个64bit的整数,8字节,正好为一个long类型数据。
- 从左到右,第一位为符号位,0表示正,1表示负。
- 时间戳(毫秒转化为年):2^41/(365 * 24 * 60 * 60 * 1000)=69.73年。说明雪花算法可表示的范围为69年(从1970年开始),说明雪花算法能用到2039年。
- 10bit工作位是5位数据中心ID和5位工作ID,其中5位的数据中心ID和5位工作ID的范围是:0到2^5 -1 = 31,分布式环境中一般都是通过设置不同的数据中心ID和工作ID来确保生成的ID不会重复。
- 12bit-序列号表示每个机房的每个机器每毫秒可以产生2^12-1(4095)个不同的ID序号。
完整代码
package cn.org.ppdxzz.tool;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
/**
* @author: PeiChen
* @version: 1.0
*/
public class Snowflake implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 起始时间戳
*/
private final long START_TIMESTAMP;
//数据标识占用位数
private final long WORKERID_BIT = 5L;
/**
* 数据中心占用位数
*/
private final long DATACENTER_BIT = 5L;
/**
* 序列号占用的位数
*/
private final long SEQUENCE_BIT = 12L;
/**
* 最大支持机器节点数 0~31
*/
private final long MAX_WORKERID = -1L ^ (-1L << WORKERID_BIT);
/**
* 最大支持数据中心节点数 0~31
*/
private final long MAX_DATACENTER = -1L ^ (-1L << DATACENTER_BIT);
/**
* 最大支持序列号 12位 0~4095
*/
private final long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
private final long WORKERID_LEFT_SHIFT = SEQUENCE_BIT;
private final long DATACENTER_LEFT_SHIFT = SEQUENCE_BIT + WORKERID_BIT;
private final long TIMESTAMP_LEFT_SHIFT = SEQUENCE_BIT + WORKERID_BIT + DATACENTER_BIT;
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
/**
* 上一次的时间戳
*/
private long lastTimestamp = -1L;
public Snowflake(long workerId, long datacenterId) {
this(null,workerId,datacenterId);
}
public Snowflake(LocalDateTime localDateTime, long workerId, long datacenterId) {
if (localDateTime == null) {
//2021-10-23 22:41:08 北京时间
this.START_TIMESTAMP = 1635000080L;
} else {
this.START_TIMESTAMP = localDateTime.toEpochSecond(ZoneOffset.of("+8"));
}
if (workerId > MAX_WORKERID || workerId < 0) {
throw new IllegalArgumentException("workerId can't be greater than MAX_WORKERID or less than 0");
}
if (datacenterId > MAX_DATACENTER || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER or less than 0");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
/**
* 获取下一个ID
* @return long:id
*/
public synchronized long nextId() {
long currentTimestamp = getCurrentTimestamp();
if (currentTimestamp < lastTimestamp) {
throw new IllegalArgumentException("Clock moved backwards. Refusing to generate id.");
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & MAX_SEQUENCE;
//毫秒内序列溢出就取新的时间戳
if (sequence == 0L) {
currentTimestamp = getNextTimestamp(lastTimestamp);
}
} else {
//不同毫秒内,序列号置为0L
sequence = 0L;
}
//更新上次生成ID的时间截
lastTimestamp = currentTimestamp;
//移位并通过或运算拼到一起组成64位的ID
return //时间戳部分
((currentTimestamp - START_TIMESTAMP) << TIMESTAMP_LEFT_SHIFT)
//数据中心部分
| (datacenterId << DATACENTER_LEFT_SHIFT)
//机器标识部分
| (workerId << WORKERID_LEFT_SHIFT)
//序列号部分
| sequence;
}
/**
* 获取字符串类型的下一个ID
* @return String:id
*/
public String nextStringId() {
return Long.toString(nextId());
}
/**
* 获取当前系统时间戳
* @return 时间戳
*/
private long getCurrentTimestamp() {
return LocalDateTime.now().toEpochSecond(ZoneOffset.of("+8"));
}
/**
* 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上一次生成ID的时间戳
* @return 下一个时间戳
*/
private long getNextTimestamp(long lastTimestamp) {
long timestamp = getCurrentTimestamp();
while (timestamp <= lastTimestamp) {
timestamp = getCurrentTimestamp();
}
return timestamp;
}
/**
* 根据ID获取工作机器ID
* @param id 生成的雪花ID
* @return 工作机器标识
*/
public long getWorkerId(long id) {
return id >> WORKERID_LEFT_SHIFT & ~(-1L << WORKERID_BIT);
}
/**
* 根据ID获取数据中心ID
* @param id 生成的雪花ID
* @return 数据中心ID
*/
public long getDataCenterId(long id) {
return id >> DATACENTER_LEFT_SHIFT & ~(-1L << DATACENTER_BIT);
}
public static void main(String[] args) {
Snowflake snowflake = new Snowflake(0,0);
for (int i = 0; i < 20; i++) {
System.out.println(snowflake.nextId());
}
}
}
总结
雪花算法是目前解决分布式唯一ID的一种很好的解决方案,也是目前市面上使用较多的一种方案,具体怎么使用还是得看自己的需求,总之,适合自己系统的才是最好的