在微服務開發中,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();
}