10分鐘看懂: zookeeper snowflake 雪花演算法
瘋狂創客圈 Java 分散式聊天室【 億級流量】實戰系列之 -25【 部落格園 總入口 】
文章目錄
寫在前面
大家好,我是作者尼恩。目前和幾個小夥伴一起,組織了一個高併發的實戰社群【瘋狂創客圈】。正在開始高併發、億級流程的 IM 聊天程式 學習和實戰
前面,已經完成一個高效能的 Java 聊天程式的四件大事:
接下來,需要進入到分散式開發的環節了。 分散式的中介軟體,瘋狂創客圈的小夥伴們,一致的選擇了zookeeper,不僅僅是由於其在大資料領域,太有名了。更重要的是,很多的著名框架,都使用了zk。
**本篇介紹 ZK 的分散式命名服務 ** 中的 節點命名服務和 snowflake 雪花演算法。
1.1.1. 叢集節點的命名服務
前面講到,在分散式叢集中,可能需要部署的大量的機器節點。在節點少的受,可以人工維護。在量大的場景下,手動維護成本高,考慮到自動部署、運維等等問題,節點的命名,最好由系統自動維護。
節點的命名,主要是為節點進行唯一編號。主要的訴求是,不同節點的編號,是絕對的不能重複。一旦編號重複,就會導致有不同的節點碰撞,導致叢集異常。
有以下兩個方案,可供生成叢集節點編號:
(1)使用資料庫的自增ID特性,用資料表,儲存機器的mac地址或者ip來維護。
(2)使用ZooKeeper持久順序節點的次序特性。來維護節點的編號。
這裡,我們採用第二種,通過ZooKeeper持久順序節點特性,來配置維護節點的編號NODEID。
叢集節點命名服務的基本流程是:
(1)啟動節點服務,連線ZooKeeper, 檢查命名服務根節點根節點是否存在,如果不存在就建立系統根節點。
(2)在根節點下建立一個臨時順序節點,取回順序號做節點的NODEID。如何臨時節點太多,可以根據需要,刪除臨時節點。
基本的演算法,和生成分散式ID的大部分是一致的,主要的程式碼如下:
package com.crazymakercircle.zk.NameService;
import com.crazymakercircle.util.ObjectUtil;
import com.crazymakercircle.zk.ZKclient;
import lombok.Data;
import org.apache.curator.framework.CuratorFramework;
import org.apache.zookeeper.CreateMode;
/**
* create by 尼恩 @ 瘋狂創客圈
**/
@Data
public class SnowflakeIdWorker {
//Zk客戶端
private CuratorFramework client = null;
//工作節點的路徑
private String pathPrefix = "/test/IDMaker/worker-";
private String pathRegistered = null;
public static SnowflakeIdWorker instance = new SnowflakeIdWorker();
private SnowflakeIdWorker() {
instance.client = ZKclient.instance.getClient();
instance.init();
}
// 在zookeeper中建立臨時節點並寫入資訊
public void init() {
// 建立一個 ZNode 節點
// 節點的 payload 為當前worker 例項
try {
byte[] payload = ObjectUtil.Object2JsonBytes(this);
pathRegistered = client.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.EPHEMERAL_SEQUENTIAL)
.forPath(pathPrefix, payload);
} catch (Exception e) {
e.printStackTrace();
}
}
public long getId() {
String sid=null;
if (null == pathRegistered) {
throw new RuntimeException("節點註冊失敗");
}
int index = pathRegistered.lastIndexOf(pathPrefix);
if (index >= 0) {
index += pathPrefix.length();
sid= index <= pathRegistered.length() ? pathRegistered.substring(index) : null;
}
if(null==sid)
{
throw new RuntimeException("節點ID生成失敗");
}
return Long.parseLong(sid);
}
}
1.1.2. snowflake 的ID演算法改造
Twitter的snowflake 演算法,是一種著名的分散式伺服器使用者ID生成演算法。SnowFlake演算法所生成的ID 是一個64bit的長整形數字。這個64bit被劃分成四部分,其中後面三個部分,分別表示時間戳、機器編碼、序號。
(1)第一位
佔用1bit,其值始終是0,沒有實際作用。
(2)時間戳
佔用41bit,精確到毫秒,總共可以容納約69年的時間。
(3)工作機器id
佔用10bit,最多可以容納1024個節點。
(4)序列號
佔用12bit,最多可以累加到4095。這個值在同一毫秒同一節點上從0開始不斷累加。
總體來說,在工作節點達到1024頂配的場景下,SnowFlake演算法在同一毫秒內最多可以生成多少個全域性唯一ID呢?這是一個簡單的乘法:
同一毫秒的ID數量 = 1024 X 4096 = 4194304
400多萬個ID,這個數字在絕大多數併發場景下都是夠用的。
snowflake 演算法中,第三個部分是工作機器ID,可以結合上一節的命名方法,並通過Zookeeper管理workId,免去手動頻繁修改叢集節點,去配置機器ID的麻煩。
/**
* create by 尼恩 @ 瘋狂創客圈
**/
public class SnowflakeIdGenerator {
/**
* 單例
*/
public static SnowflakeIdGenerator instance =
new SnowflakeIdGenerator();
/**
* 初始化單例
*
* @param workerId 節點Id,最大8091
* @return the 單例
*/
public synchronized void init(long workerId) {
if (workerId > MAX_WORKER_ID) {
// zk分配的workerId過大
throw new IllegalArgumentException("woker Id wrong: " + workerId);
}
instance.workerId = workerId;
}
private SnowflakeIdGenerator() {
}
/**
* 開始使用該演算法的時間為: 2017-01-01 00:00:00
*/
private static final long START_TIME = 1483200000000L;
/**
* worker id 的bit數,最多支援8192個節點
*/
private static final int WORKER_ID_BITS = 13;
/**
* 序列號,支援單節點最高每毫秒的最大ID數1024
*/
private final static int SEQUENCE_BITS = 10;
/**
* 最大的 worker id ,8091
* -1 的補碼(二進位制全1)右移13位, 然後取反
*/
private final static long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
/**
* 最大的序列號,1023
* -1 的補碼(二進位制全1)右移10位, 然後取反
*/
private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BITS);
/**
* worker 節點編號的移位
*/
private final static long APP_HOST_ID_SHIFT = SEQUENCE_BITS;
/**
* 時間戳的移位
*/
private final static long TIMESTAMP_LEFT_SHIFT = WORKER_ID_BITS + APP_HOST_ID_SHIFT;
/**
* 該專案的worker 節點 id
*/
private long workerId;
/**
* 上次生成ID的時間戳
*/
private long lastTimestamp = -1L;
/**
* 當前毫秒生成的序列
*/
private long sequence = 0L;
/**
* Next id long.
*
* @return the nextId
*/
public Long nextId() {
return generateId();
}
/**
* 生成唯一id的具體實現
*/
private synchronized long generateId() {
long current = System.currentTimeMillis();
if (current < lastTimestamp) {
// 如果當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過,出現問題返回-1
return -1;
}
if (current == lastTimestamp) {
// 如果當前生成id的時間還是上次的時間,那麼對sequence序列號進行+1
sequence = (sequence + 1) & MAX_SEQUENCE;
if (sequence == MAX_SEQUENCE) {
// 當前毫秒生成的序列數已經大於最大值,那麼阻塞到下一個毫秒再獲取新的時間戳
current = this.nextMs(lastTimestamp);
}
} else {
// 當前的時間戳已經是下一個毫秒
sequence = 0L;
}
// 更新上次生成id的時間戳
lastTimestamp = current;
// 進行移位操作生成int64的唯一ID
//時間戳右移動23位
long time = (current - START_TIME) << TIMESTAMP_LEFT_SHIFT;
//workerId 右移動10位
long workerId = this.workerId << APP_HOST_ID_SHIFT;
return time | workerId | sequence;
}
/**
* 阻塞到下一個毫秒
*/
private long nextMs(long timeStamp) {
long current = System.currentTimeMillis();
while (current <= timeStamp) {
current = System.currentTimeMillis();
}
return current;
}
}
上面的程式碼中,大量的使用到了位運算。
如果對位運算不清楚,估計很難看懂上面的程式碼。
這裡需要強調一下,-1 的8位二進位制編碼為 1111 1111,也就是全1。
為什麼呢?
因為,8位二進位制場景下,-1的原碼是1000 0001,反碼是 1111 1110,補碼是反碼加1。計算後的結果是,-1 的二進位制編碼為全1。16位、32位、64位的-1,二進位制的編碼也是全1。
上面用到的二進位制位移演算法,以及二進位制按位或的演算法,都比較簡單。如果不懂,可以去檢視java的基礎書籍。
總的來說,以上的程式碼,是一個相對比較簡單的snowflake實現版本,關鍵的演算法解釋如下:
(1)在單節點上獲得下一個ID,使用Synchronized控制併發,而非CAS的方式,是因為CAS不適合併發量非常高的場景。
(2)如果當前毫秒在一臺機器的序列號已經增長到最大值4095,則使用while迴圈等待直到下一毫秒。
(3)如果當前時間小於記錄的上一個毫秒值,則說明這臺機器的時間回撥了,丟擲異常。
SnowFlake演算法的優點:
(1)生成ID時不依賴於資料庫,完全在記憶體生成,高效能高可用。
(2)容量大,每秒可生成幾百萬ID。
(3)ID呈趨勢遞增,後續插入資料庫的索引樹的時候,效能較高。
SnowFlake演算法的缺點:
(1)依賴於系統時鐘的一致性。如果某臺機器的系統時鐘回撥,有可能造成ID衝突,或者ID亂序。
(2)還有,在啟動之前,如果這臺機器的系統時間回撥過,那麼有可能出現ID重複的危險。
寫在最後
下一篇:基於zk,實現分散式鎖。
瘋狂創客圈 億級流量 高併發IM 實戰 系列
- Java (Netty) 聊天程式【 億級流量】實戰 開源專案實戰
- Netty 原始碼、原理、JAVA NIO 原理
- Java 面試題 一網打盡
- 瘋狂創客圈 【 部落格園 總入口 】