自定义主键生成策略之雪花算法(No2)
1. 工具类方式(最普通的雪花算法封装)
来自:https://blog.youkuaiyun.com/lingerlan510/article/details/116207951
import java.util.concurrent.locks.ReentrantLock;
public class IdUtil {
private static volatile SnowflakeIdWorker snowflakeIdWorker;
private static final String PRIMARYKEY_WORKERID = "primarykey.workerid";
private static final ReentrantLock LOCK = new ReentrantLock();
private static SnowflakeIdWorker getSnowflakeIdWorker() {
if(snowflakeIdWorker == null) {
LOCK.lock();
try {
if(snowflakeIdWorker == null) {
// 此处是把机器ID存放到配置文件中进行读取
// 数据中心ID则是通过Zookeeper去进行管理获取
// snowflakeIdWorker = new SnowflakeIdWorker(Long.valueOf(PropertiesUtil.getProperty(PRIMARYKEY_WORKERID)),DataCenterConfig.getDataCenterID());
snowflakeIdWorker = new SnowflakeIdWorker(1L,2L);
}
} finally {
LOCK.unlock();
}
}
return snowflakeIdWorker;
}
public static long getPrimaryKey() {
return getSnowflakeIdWorker().nextId();
}
}
package com.example.createpj.data.utils.snowFlake;
/**
** 问题场景:分布式场景下各节点时钟不可能完全同步
** Twitter_Snowflake
** SnowFlake的结构如下(每部分用-分开):
** 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
** 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0
** 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)得到的值,这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
** 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId
** 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
** 加起来刚好64位,为一个Long型
** SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高
*/
public class SnowflakeIdWorker {
// 开始时间截 (2018-12-03)
private final long twepoch = 1543766400000L;
// 机器id所占的位数
private final long workerIdBits = 5L;
// 数据标识id所占的位数
private final long datacenterIdBits = 5L;
// 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
// 支持的最大数据标识id,结果是31
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
// 序列在id中占的位数
private final long sequenceBits = 12L;
// 机器ID向左移12位
private final long workerIdShift = sequenceBits;
// 数据标识id向左移17位(12+5)
private final long datacenterIdShift = sequenceBits + workerIdBits;
// 时间截向左移22位(5+5+12)
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
// 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
// 工作机器ID(0~31)
private long workerId;
// 数据中心ID(0~31)
private long datacenterId;
// 毫秒内序列(0~4095)
private long sequence = 0L;
// 上次生成ID的时间截
private long lastTimestamp = -1L;
/**
* * 构造函数
* @param workerId 工作ID (0~31)
* @param datacenterId 数据中心ID (0~31)
*/
public SnowflakeIdWorker(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public SnowflakeIdWorker() {}
/**
* * 获得下一个ID (该方法是线程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
// 如果当前时间小于上一次生成ID的时间戳,说明系统时钟回退过这个时候应当抛出异常
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
// 如果是同一时间生成的,则进行毫秒内序列
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// 毫秒内序列溢出
if (sequence == 0) {
// 阻塞到下一个毫秒,获得新的时间戳
timestamp = tilNextMillis(lastTimestamp);
}
}
// 时间戳改变,毫秒内序列重置
else {
sequence = 0L;
}
// 上次生成的时间截
lastTimestamp = timestamp;
// 移位并通过或运算拼到一起组成64位的ID
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
/**
* * 阻塞到下一个毫秒,直到获得新的时间戳
* @param lastTimestamp 上次生成ID的时间截
* @return 当前时间戳
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* * 返回以毫秒为单位的当前时间
* @return 当前时间(毫秒)
*/
private long timeGen() {
return System.currentTimeMillis();
}
}
2. 工厂方式(返回String类型,可按模块或业务分类)
来自:https://www.manongdao.com/article-2333359.html
package com.example.createpj.data.utils.snowFlake;
import com.example.createpj.data.utils.snowFlake.enumUtils.BRStyle;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* 雪花算法,解决时间回拨问题
* 整合业务,需要根据业务编号生成id
* @date 2020-05-27 15:41 注意部署所在服务,如果时间回调超过5ms 该方法就无法工作了
* com.example.createpj.data.utils.snowFlake.SnowFlakeFactory
*/
@Component
public class SnowFlakeFactory {
public SnowFlakeFactory() {
}
/**
* 起始的时间戳
* 2020-01-01 00:00:00 毫秒
*/
private final static long START_STMP = 1577808000000L;
/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long MACHINE_BIT = 5; //机器标识占用的位数
private final static long DATACENTER_BIT = 5;//数据中心占用的位数
/**
* 每一部分的最大值
* MAX_DATACENTER_NUM = 31
* MAX_MACHINE_NUM = 31
* MAX_SEQUENCE = 4095
*/
private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;
private long datacenterId = 0; //数据中心
private long machineId = 0; //机器标识
private long sequence = 0L; //序列号
private long lastStmp = -1L;//上一次时间戳
/**
* 最大容忍时间, 单位毫秒, 即如果时钟只是回拨了该变量指定的时间, 那么等待相应的时间即可;
* 考虑到sequence服务的高性能, 这个值不易过大
*/
private static final long MAX_BACKWARD_MS = 5;
//最大扩展字段
private long maxExtension = 2L;
/**
* 保留machineId和lastTimestamp, 以及备用machineId和其对应的lastTimestamp
*/
private static Map<Long, Long> machineIdLastTimeMap = new ConcurrentHashMap<>();
/**
* 初始化数据中心位,和机器标识
* 0 < datacenterId < MAX_DATACENTER_NUM 31
* 0 < machineId < MAX_MACHINE_NUM 31
* @param datacenterId
* @param machineId
*/
public SnowFlakeFactory(long datacenterId, long machineId) {
if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
throw new IllegalArgumentException(" datacenterId 必须介于[0,31] ");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException(" machineId 必须介于[0,31] ");
}
this.datacenterId = datacenterId;
this.machineId = machineId;
//初始化时间 machineIdLastTimeMap
machineIdLastTimeMap.put(machineId, getNewstmp());
}
/**
* 产生下一个ID
* @return
*/
public synchronized String nextId(BRStyle brStyle) {
//现存的扩展字段
long extension = 0L;
//获取当前时间毫秒数
long currStmp = getNewstmp();
//lastStmp = currStmp + 100;
if (currStmp < lastStmp) {
//throw new RuntimeException("时钟向后移动,拒绝生成id");
// 如果时钟回拨在可接受范围内, 等待即可
long offset = lastStmp - currStmp;
//如果回拨时间不超过5毫秒,就等待相应的时间
if (offset <= MAX_BACKWARD_MS) {
try {
//睡(lastTimestamp - currentTimestamp)ms让其追上
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(offset));
currStmp = getNewstmp();
//如果时间还小于当前时间,那么利用扩展字段加1
//或者是采用抛异常并上报
if (currStmp < lastStmp) {
//扩展字段
extension += 1;
if (extension > maxExtension) {
//服务器时钟被调整了,ID生成器停止服务.
throw new RuntimeException(String.format("时钟向后移动。拒绝生成的id %d 毫秒", lastStmp - currStmp));
}
}
} catch (Exception e) {
e.printStackTrace();
}
} else {
//扩展字段
extension += 1;
if (extension > maxExtension) {
//服务器时钟被调整了,ID生成器停止服务.
throw new RuntimeException(String.format("时钟向后移动,超出扩展位,拒绝生成的id %d 毫秒", lastStmp - currStmp));
}
//获取可以用的workid,对应的时间戳,必须大于当前时间戳
tryGenerateKeyOnBackup(currStmp);
}
}
if (currStmp == lastStmp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currStmp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}
lastStmp = currStmp;
long id = (currStmp - START_STMP) << (TIMESTMP_LEFT - extension) //时间戳部分
| datacenterId << DATACENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
//如果时间戳回拨就让时间少移动一位
return brStyle.getCode() + id;
}
/**
* 自旋锁获取当前时间戳
*
* @return
*/
private long getNextMill() {
long mill = getNewstmp();
while (mill <= lastStmp) {
mill = getNewstmp();
}
return mill;
}
/**
* 获取当前时间毫秒数
*
* @return
*/
private long getNewstmp() {
return System.currentTimeMillis();
//测试时间回拨
//return 53501026489350000l;
}
/**
* 尝试在machineId的备份machineId上生成
* 核心优化代码在方法tryGenerateKeyOnBackup()中,BACKUP_COUNT即备份machineId数越多,
* sequence服务避免时钟回拨影响的能力越强,但是可部署的sequence服务越少,
* 设置BACKUP_COUNT为3,最多可以部署1024/(3+1)即256个sequence服务,完全够用,
* 抗时钟回拨影响的能力也得到非常大的保障。
*
* @param currentMillis 当前时间
*/
private long tryGenerateKeyOnBackup(long currentMillis) {
// 遍历所有machineId(包括备用machineId, 查看哪些machineId可用)
for (Map.Entry<Long, Long> entry : machineIdLastTimeMap.entrySet()) {
this.machineId = entry.getKey();
// 取得备用machineId的lastTime
Long tempLastTime = entry.getValue();
lastStmp = tempLastTime == null ? 0L : tempLastTime;
// 如果找到了合适的machineId,返回合适的时间,
if (lastStmp <= currentMillis) {
return lastStmp;
}
}
// 如果所有machineId以及备用machineId都处于时钟回拨, 那么抛出异常
throw new IllegalStateException("时钟在向后移动,当前时间是 " + currentMillis + " 毫秒,machineId映射 = " + machineIdLastTimeMap);
}
}
package com.example.createpj.data.utils.snowFlake;
import lombok.Getter;
import lombok.Setter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Getter
@Setter
public class IDGenderConfig {
//数据中心[0,31] 配置文件中不配置就是0
private long datacenterId;
//机器标识[0,31] 配置文件中不配置就是0
private long machineId;
//生命周期内,单例唯一
@Bean
public SnowFlakeFactory getSnowFlakeFactory() {
SnowFlakeFactory snowFlakeFactory = new SnowFlakeFactory(datacenterId,machineId);
return snowFlakeFactory;
}
}
package com.example.createpj.data.utils.snowFlake.enumUtils;
public enum BRStyle {
CO("CP", "测试模块"),
;
private final String code;
private final String info;
BRStyle(String code, String info) {
this.code = code;
this.info = info;
}
public String getCode() {
return code;
}
public String getInfo() {
return info;
}
}