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