java怎样生成32位全是整形的主键_你肯定会需要的分布式Id生成算法雪花算法(Java)...
最近公司正好在做數(shù)據(jù)庫遷移從oracle到mysql,因為之前oracle主鍵是使用的 SYS_GUID() 這個oracle提供的函數(shù)來生成全球唯一的標(biāo)識符(原始值)由16個字節(jié)組成。
不過由于mysql默認使用的InnoDB存儲引擎采用的聚簇索引,使用uuid對寫性能有一定的影響。而且為了后續(xù)分庫分表考慮,也不宜采用數(shù)據(jù)庫自增,因此就考慮到需要使用一種可以支持分布式遞增且全局唯一的Id生成算法。經(jīng)過調(diào)研,雪花算法是 Twitter 開源的一種生成分布式全局唯一ID的經(jīng)典算法,且能保證整體上按照時間遞增。
在查看了網(wǎng)上大部分關(guān)于雪花算法的資料后,關(guān)于雪花算法的解讀網(wǎng)上多如牛毛,大多抄來抄去。我發(fā)現(xiàn)這些教程大多有兩點問題:
只是解讀官方算法原理,沒有解決 機器ID(5位)和數(shù)據(jù)中心ID(5位)的配置問題,分布式部署如何保證配置唯一。
都是Demo需要實例化對象,沒有形成開箱即用的工具類,不能直接結(jié)合項目使用。
本文旨在完善上面存在的兩點問題,希望可以幫助和我一樣準(zhǔn)備在項目使用 SnowFlake 算法生成數(shù)據(jù)庫主鍵的小伙伴。
概述
SnowFlake算法生成id的結(jié)果是一個64bit大小的整數(shù),它的結(jié)構(gòu)如下圖:
由于在Java中64bit的整數(shù)是long類型,所以在Java中SnowFlake算法生成的id就是long來存儲的。
SnowFlake可以保證:
所有生成的id按時間趨勢遞增
整個分布式系統(tǒng)內(nèi)不會產(chǎn)生重復(fù)id(因為有datacenterId和workerId來做區(qū)分)
Talk is cheap, show you the code
針對文章開頭提出的兩個問題,筆者的解決方案是,workId使用服務(wù)器IP生成,dataCenterId使用hostName生成,這樣可以最大限度防止10位機器碼重復(fù),但是由于兩個ID都不能超過32,只能取余數(shù),還是難免產(chǎn)生重復(fù),但是實際使用中,hostName和IP的配置一般連續(xù)或相近,只要不是剛好相隔32位,就不會有問題,況且,hostName和IP同時相隔32的情況更加是幾乎不可能的事,平時做的分布式部署,一般也不會超過100臺容器。
上面的方法可以零配置使用雪花算法,雪花算法10位機器碼的設(shè)定理論上可以有1024個節(jié)點,生產(chǎn)上使用docker配置一般是一次編譯,然后分布式部署到不同容器,不會有不同的配置。這里提供幾種可以完全避免產(chǎn)生重復(fù)的方案,可以使用redis自增,在應(yīng)用啟動的時候去獲取分配機器碼。也可以使用zk下發(fā)機器碼,將機器對應(yīng)的機器碼存儲在zk的永久節(jié)點下,每次啟動獲取。不過這兩種方案都是需要配置開發(fā)的,在生產(chǎn)部署機器少以及并發(fā)不太大的情況下,使用本文提供的方案即可。后續(xù)如果真有問題,會采用這種依賴中間件下發(fā)機器碼的方案,到時候在進行補充。
完整代碼如下:
import org.apache.commons.lang3.RandomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.Inet4Address;
import java.net.UnknownHostException;
public class SnowflakeIdWorker{
private static final Logger LOGGER = LoggerFactory.getLogger(SnowflakeIdWorker.class);
/** 工作機器ID(0~31) */
private long workerId;
/** 數(shù)據(jù)中心ID(0~31) */
private long dataCenterId;
/** 毫秒內(nèi)序列(0~4095) */
private long sequence = 0L;
public SnowflakeIdWorker(long workerId, long dataCenterId){
// sanity check for workerId
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));
}
LOGGER.info("worker starting. timestamp left shift = {}, dataCenter id bits = {}, worker id bits = {}, sequence bits = {}, workerid = {}, dataCenterId = {}",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId,dataCenterId);
this.workerId = workerId;
this.dataCenterId = dataCenterId;
}
/**初始時間戳*/
private long twepoch = 1577808000000L;
/**長度為5位*/
private long workerIdBits = 5L;
private long datacenterIdBits = 5L;
/** 支持的最大機器id,結(jié)果是31 (這個移位算法可以很快的計算出幾位二進制數(shù)所能表示的最大十進制數(shù)) */
private long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 支持的最大數(shù)據(jù)標(biāo)識id,結(jié)果是31 */
private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/** 序列在id中占的位數(shù) */
private long sequenceBits = 12L;
/** 生成序列的掩碼,這里為4095 (0b111111111111=0xfff=4095) */
private long sequenceMask = -1L ^ (-1L << sequenceBits);
//工作id需要左移的位數(shù),12位
private long workerIdShift = sequenceBits;
//數(shù)據(jù)id需要左移位數(shù) 12+5=17位
private long datacenterIdShift = sequenceBits + workerIdBits;
//時間戳需要左移位數(shù) 12+5+5=22位
private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
//上次時間戳,初始值為負數(shù)
private long lastTimestamp = -1L;
private static SnowflakeIdWorker idWorker;
static {
idWorker = new SnowflakeIdWorker(getWorkId(),getDataCenterId());
}
/**
* 獲得下一個ID (該方法是線程安全的)
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//獲取當(dāng)前時間戳如果小于上次時間戳,則表示時間戳獲取出現(xiàn)異常
if (timestamp < lastTimestamp) {
LOGGER.error("clock is moving backwards. Rejecting requests until : {}.", lastTimestamp);
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds",
lastTimestamp - timestamp));
}
//獲取當(dāng)前時間戳如果等于上次時間戳(同一毫秒內(nèi)),則在序列號加一;否則序列號賦值為0,從0開始。
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & sequenceMask;
// 毫秒內(nèi)序列溢出
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
//將上次時間戳值刷新
lastTimestamp = timestamp;
/**
* 返回結(jié)果:
* (timestamp - twepoch) << timestampLeftShift) 表示將時間戳減去初始時間戳,再左移相應(yīng)位數(shù)
* (datacenterId << datacenterIdShift) 表示將數(shù)據(jù)id左移相應(yīng)位數(shù)
* (workerId << workerIdShift) 表示將工作id左移相應(yīng)位數(shù)
* | 是按位或運算符,例如:x | y,只有當(dāng)x,y都為0的時候結(jié)果才為0,其它情況結(jié)果都為1。
* 因為各部分只有相應(yīng)位上的值有意義,其它位上都是0,所以將各部分的值進行 | 運算就能得到最終拼接好的id
*/
return ((timestamp - twepoch) << timestampLeftShift) |
(dataCenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence;
}
/**
* 阻塞到下一個毫秒,直到獲得新的時間戳
* @param lastTimestamp 上次生成ID的時間截
* @return 當(dāng)前時間戳
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒為單位的當(dāng)前時間
* @return 當(dāng)前時間(毫秒)
*/
private long timeGen(){
return System.currentTimeMillis();
}
private static Long getWorkId(){
try {
String hostAddress = Inet4Address.getLocalHost().getHostAddress();
char[] chars = hostAddress.toCharArray();
int sums = 0;
for(int b : chars){
sums += b;
}
return (long)(sums % 32);
} catch (UnknownHostException e) {
// 如果獲取失敗,則使用隨機數(shù)備用
return RandomUtils.nextLong(0,31);
}
}
private static Long getDataCenterId(){
try {
char[] chars = Inet4Address.getLocalHost().getHostName().toCharArray();
int sums = 0;
for (int i: chars) {
sums += i;
}
return (long)(sums % 32);
} catch (UnknownHostException e) {
// 如果獲取失敗,則使用隨機數(shù)備用
return RandomUtils.nextLong(0,31);
}
}
/**
* 靜態(tài)工具類
*
* @return
*/
public static Long generateId(){
long id = idWorker.nextId();
return id;
}
/** 測試 */
public static void main(String[] args) {
System.out.println(System.currentTimeMillis());
long startTime = System.nanoTime();
for (int i = 0; i < 50000; i++) {
long id = SnowflakeIdWorker.generateId();
LOGGER.info("id = {}",id);
}
LOGGER.info((System.nanoTime()-startTime)/1000000+"ms");
}
}
擴展
在理解了這個算法之后,其實還有一些擴展的事情可以做:
理論上41位記錄時間戳可以表示69年,而時間戳是從1970年開始算,對于現(xiàn)在來說1970到2019這段時間內(nèi)的毫秒數(shù)已經(jīng)用不上了,因此可以設(shè)置一個初始時間參照點(一般設(shè)置為id生成器開始使用的時間),計算時間戳差值(當(dāng)前時間截 - 開始時間截)。這樣就可以擴展時間戳使用的范圍。
解密id,由于id的每段都保存了特定的信息,所以拿到一個id,應(yīng)該可以嘗試反推出原始的每個段的信息。反推出的信息可以幫助我們分析。比如作為訂單,可以知道該訂單的生成日期,負責(zé)處理的數(shù)據(jù)中心等等。
完善算法中生成機器id的策略,進一步采用zk分發(fā)或redis自增等,實現(xiàn)完全無碰撞的id生成。
根據(jù)自己業(yè)務(wù)修改每個位段存儲的信息。算法是通用的,可以根據(jù)自己需求適當(dāng)調(diào)整每段的大小以及存儲的信息。
參考資料:
由于算法中大量采用了位運算,如果不太了解的朋友可以參考這篇解析
https://segmentfault.com/a/1190000011282426
推薦閱讀
為什么阿里巴巴規(guī)定禁止超過三張表join?
必知必會-存儲器層次結(jié)構(gòu)
每日一道算法題-leetcode189.旋轉(zhuǎn)數(shù)組
點個在看吧,證明你還愛我
總結(jié)
以上是生活随笔為你收集整理的java怎样生成32位全是整形的主键_你肯定会需要的分布式Id生成算法雪花算法(Java)...的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: navicat连接mysql闪退_Nav
- 下一篇: aspose java_Aspose.C