在微服务开发中,ID生成一直是一个十分头疼的问题,网上常见的方式有三种:
- 基于数据库的主键自增
- 基于UUID
- 雪花算法
对于第1种基于数据库主键自增的,当数据量比较少时无所谓,但是当数据量很大需要进行分表分库是扩展时,对于ID的处理就会出现各种麻烦
对于第2种基于UUID的主键,理论上不会出现问题,但是由于UUID是字符串形式,在Mysql数据库中对于字符串的查询没有数值型的效率高
对于第3种基于雪花算法的ID生成器,这种方式也是比较主流的,各大厂的ID生成器也是在此基础之上进行优化改造后实现的。但是由于雪花算法生成的ID是基于64位Long型数值产生的,传递给前端时会出现精度丢失的问题。
本文介绍的方式是在雪花算法的基础之上对ID长度进行调整后生成的, *适用于TPS在100W左右的应用。为了保证返回给前端的ID不会出现溢出,返回给前端的数值不超过9007199254740991,即二进制的53位全1数值,其中使用前40位用作时间戳位,能够保证在35年不会重复,3位作为机器位,即最多允许部署8台机器,对于小型服务基本够用,后10位作为序号位,允许每毫秒最多产生1024个ID。
由于是适用于小型企业的,故不设置数据中心节点。可以自己定制注册中心进行工作机器注册。
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* 适用于TPS在100W左右的应用
* 为了保证返回给前端的ID不会出现溢出,返回给前端的整型不能超过15位
* 后端限制最高返回值是53位二进制的全1
* 使用40位用作时间戳位,能够保证在35年不会重复
* 3位作为机器位,即最多允许部署8台机器,对于小型服务基本够用
* 10位作为序号位,允许每毫秒最多产生1024个ID
*
* @author: LiuMeng
* @date: 2019/11/19
* TODO:
*/
@Slf4j
public class Sequence {
/**
* 时间起始标记点,作为基准,一般取系统的最近时间(一旦确定不能变动)
* 2019-11-20 00:00:00
*/
private final long edenPoint = 1574179200000L;
private final long workerIdBits = 3L;
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
private final long sequenceBits = 10L;
private final long workerIdShift = sequenceBits;
private final long timestampLeftShift = sequenceBits + workerIdBits;
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
private final long workerId;
private final long idBits = 53;
private final long maxLimit = -1L ^ (-1L << idBits);
private long sequence = 0L;
private long lastTimestamp = -1L;
private final ISequenceAdapter sequenceAdapter;
private static Sequence instance;
/**
* 有参构造器
*/
private Sequence(ISequenceAdapter sequenceAdapter) {
this.sequenceAdapter = sequenceAdapter;
long nextWorkId = this.sequenceAdapter.getNextWorkId();
if (nextWorkId > maxWorkerId) {
throw new IllegalStateException("The work Id has reached the maximum");
}
this.workerId = nextWorkId;
this.startSequenceIdRegisterScheduled();
instance = this;
}
/**
* 获取全局对象
*
* @return
*/
public static Sequence getInstance(ISequenceAdapter sequenceAdapter) {
if (null == instance) {
synchronized (Sequence.class) {
if (null == instance) {
instance = new Sequence(sequenceAdapter);
}
}
}
return instance;
}
/**
* 获取全局对象
*
* @return
*/
public static Sequence getInstance() throws SequenceInstanceException {
if (null == instance) {
throw new SequenceInstanceException("the sequence is not instance, you can use [getInstance(ISequenceAdapter)] to get or init.");
}
return instance;
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
try {
wait(offset << 1);
timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", offset));
}
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = ThreadLocalRandom.current().nextLong(1, 3);
}
lastTimestamp = timestamp;
// 时间戳部分 | 机器标识部分 | 序列号部分
return (((timestamp - edenPoint) << timestampLeftShift)
| (workerId << workerIdShift)
| sequence) & maxLimit;
}
/**
* 同一毫秒内Id数量达到最大,等待下一毫秒
*
* @param lastTimestamp
* @return
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
//自旋获取下毫秒
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return sequenceAdapter.getTimestamp();
}
private void startSequenceIdRegisterScheduled() {
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(runnable -> {
Thread thread = new Thread(runnable, "Sequence-Register-Schedule");
thread.setDaemon(true);
return thread;
});
scheduler.scheduleAtFixedRate(() -> this.sequenceAdapter.register(this.workerId), 1, 1, TimeUnit.MINUTES);
}
}
/**
* @author: LiuMeng
* @date: 2019/11/20
* TODO:
*/
public interface ISequenceAdapter {
/**
* 注册服务的工作ID,每分钟一次
* @param workId
*/
void register(long workId);
/**
* 获取服务的下一个工作Id
*/
long getNextWorkId();
/**
* 获取当前的时间戳
* @return
*/
long getTimestamp();
}
本文提出一种基于改进雪花算法的ID生成策略,适用于小型企业TPS约100万的应用场景。通过调整ID长度,确保前端接收的ID不超过15位,避免精度丢失。采用40位时间戳、3位机器位和10位序列号,支持35年内不重复的ID生成,每毫秒最多产生1024个ID。
1205

被折叠的 条评论
为什么被折叠?



