第2-2-4章 常見組件與中台化-常用組件服務介紹-分散式ID-附Snowflake雪花演算法的程式碼實現
2.3 分散式ID
2.3.1 功能概述
ID,全稱Identifier,中文翻譯為標識符,是用來唯一標識對象或記錄的符號。比如我們每個人都有自己的身份證號,這個就是我們的標識符,有了這個唯一標識,就能快速識別出每一個人。
在電腦世界裡,複雜的分散式系統中,經常需要對大量的數據、消息、HTTP 請求等進行唯一標識。比如對於分微服務架構的系統中,服務間相互調用需要唯一標識,冪等處理,調用鏈路分析,日誌追蹤的時候都需要使用這個唯一標識,此時我們的系統就迫切的需要一個全局唯一的ID。
另外隨著社會的發展,各種金融、電商、支付、等系統中產生的數據越來越多,對資料庫進行分庫分表是比較常見的,而分庫後則需要有一個唯一ID來標識一條數據或消息,單個資料庫的自增ID顯然不能滿足需求,此時也會需要一個能夠生成全局唯一ID的系統。
工程結構:
2.3.2 應用場景
1、全局唯一
這個最簡單,就是說不能出現重複的ID,既然是唯一標識,這是最基本的要求。比如採用UUID.randomUUID()的方式產生唯一且不重複的分散式主鍵。最終生成一個字元串類型的主鍵。缺點是生成的主鍵無序。
2、趨勢遞增
先來了解下什麼是趨勢遞增?
簡單說就是在一段時間內,生成的ID是遞增的趨勢,而不強求下一個ID必須大於前一ID。例如在一段時間內生成的ID在【0,1000】之間,過段時間生成的ID在【1000,2000】之間。
為什麼要趨勢遞增?
目前大部分的互聯網公司使用了開源的MySQL資料庫,存儲引擎選擇InnoDB。MySQL InnoDB引擎中使用的是聚集索引,由於多數RDBMS資料庫使用B-tree的數據結構來存儲索引數據,在主鍵的選擇上面我們應該盡量使用有序的主鍵,這樣在插入新的數據時B-tree的結構不會時常被打亂重塑,能有效的提高存取效率。
3、單調遞增
通俗的說就是下一個ID一定大於上一個ID,例如事務版本號、IM增量消息、排序等特殊需求。
4、資訊安全
如果ID是連續遞增的,那麼惡意用戶可以根據當前ID推測出下一個ID,爬取系統中數據的工作就非常容易實現,直接按照順序訪問指定URL即可;如果是訂單號就更加危險,競爭對手可以直接知道系統一天的總訂單量。所以在一些應用場景下,會需要ID無規則、不規則,切不易被破解。
5、雪花演算法SNOWFLAKE
雪花演算法,能夠保證不同進程主鍵的不重複性,相同進程主鍵的有序性。二進位形式包含4部分,從高位到低位分表為:1bit符號位、41bit時間戳位、10bit工作進程位以及12bit序列號位。
- 符號位(1bit)
預留的符號位,恆為零。
- 時間戳位(41bit)
41位的時間戳可以容納的毫秒數是2的41次冪,一年所使用的毫秒數是:365 * 24 * 60 * 60 * 1000 Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L) = 69.73年不重複;
- 工作進程位(10bit)
該標誌在Java進程內是唯一的,如果是分散式應用部署應保證每個工作進程的id是不同的。該值默認為0,可通過屬性設置。
- 序列號位(12bit)
該序列是用來在同一個毫秒內生成不同的ID。如果在這個毫秒內生成的數量超過4096(2的12次冪),那麼生成器會等待到下個毫秒繼續生成。
優點:
-
毫秒數在高位,自增序列在低位,整個ID都是趨勢遞增的。
-
不依賴第三方組件,穩定性高,生成ID的性能也非常高。
-
可以根據自身業務特性分配bit位,非常靈活
缺點:
強依賴機器時鐘,如果機器上時鐘回撥,會導致發號重複。
2.3.3 使用說明
分散式ID生成系統部署完成後,第三方系統接入即可直接獲取ID。
-
引入distributedid-client依賴:在項目pom.xml添加坐標
<dependencies> <dependency> <groupId>com.itheima.distributedid</groupId> <artifactId>distributedid-client</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies>
-
分散式ID生成系統客戶端配置,在項目resources目錄下編輯distributedid_client.properties
#伺服器地址 distributedid.server=211.103.136.244:7315 #部署多個的話,可自行添加 #distributedid.server=211.103.136.244:7315,ip2:port,... #超時時間 distributedid.readTimeout=5000 distributedid.connectTimeout=5000
-
獲取ID時,直接調用即可
Long id = 0L; //從服務端獲取自增型ID id = DistributedId.autoincrementId("your service name"); //本地生成雪花演算法ID id = DistributedId.snowflake(); //從服務端獲取雪花演算法ID id = DistributedId.snowflakeFromServer(); //使用號段模式獲取單個ID id = DistributedId.segment();
-
資料庫腳本
/* Navicat Premium Data Transfer Source Server : 本地MySQL資料庫 Source Server Type : MySQL Source Server Version : 50728 Source Host : localhost:3306 Source Schema : distributedid Target Server Type : MySQL Target Server Version : 50728 File Encoding : 65001 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for segment_id_info -- ---------------------------- DROP TABLE IF EXISTS `segment_id_info`; CREATE TABLE `segment_id_info` ( `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵', `biz_type` varchar(63) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '業務類型,唯一', `begin_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '開始id,僅記錄初始值,無其他含義。初始化時begin_id和max_id應相同', `max_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '當前最大id', `step` int(11) NULL DEFAULT 0 COMMENT '步長', `delta` int(11) NOT NULL DEFAULT 1 COMMENT '每次id增量', `remainder` int(11) NOT NULL DEFAULT 0 COMMENT '餘數', `create_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '創建時間', `update_time` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '更新時間', `version` bigint(20) NOT NULL DEFAULT 0 COMMENT '版本號', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uniq_biz_type`(`biz_type`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '號段ID資訊表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for sequence_id -- ---------------------------- DROP TABLE IF EXISTS `sequence_id`; CREATE TABLE `sequence_id` ( `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主鍵', `biz_type` char(10) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '業務類型,唯一', PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `biz_type`(`biz_type`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 62 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; SET FOREIGN_KEY_CHECKS = 1;
2.3.4 項目截圖
- 後端程式碼
- swagger頁面
2.3.5 Snowflake雪花演算法的程式碼實現
package com.itheima.distributedid.core;
import com.itheima.distributedid.core.domain.DistributedIdException;
import java.lang.management.ManagementFactory;
import java.net.InetAddress;
import java.net.NetworkInterface;
/**
* Twitter的Snowflake演算法
* <p>
* 協議格式 1: 41位時間戳 2:5位數據中心標識 3:5位機器標識 4:12位序列號
* <p>
* 1111111111111111111111111111111 11111 11111 111111111111
*/
public class Snowflake {
//起始時間戳,可以修改為伺服器第一次啟動的時間
//一旦服務已經開始使用,起始時間戳就不能改變了,理論上可以使用69年
private final static long START_TIME = 1484754361114L;
/**
* 每一個部分佔用的位數
*/
private final static long SEQUENCE_BIT = 12;//序列號佔用的位數
private final static long MACHINE_BIT = 5;//序機器標識 佔用的位數
private final static long DATA_CENTER_BIT = 5;//數據中心標識佔用的位數
/**
* 每一個部分的最大值 11111111111111111 1111111100000 000000000011111
*/
private final static long MAX_DATA_CENTER_ID = ~(-1L << DATA_CENTER_BIT);
private final static long MAX_MACHINE_ID = ~(-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
/**
* 每一部分向左位移數 1111111111111111111111111111111 11111 11111 111111111111
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
private long dataCenterId;//數據中心ID
private long machineId;//數據中心ID
private long sequence = 0L;//數據中心ID
private long lastTimestamp = -1L;//數據中心ID
/**
* 分散式部署的時候,數據節點標識和機器標識作為聯合鍵,必須唯一的
*
* @param dataCenterId 數據中心標識ID
* @param machineId 機器標識ID
*/
public Snowflake(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATA_CENTER_ID || dataCenterId < 0) {
throw new DistributedIdException("數據中心ID不合法");
}
if (machineId > MAX_MACHINE_ID || machineId < 0) {
throw new DistributedIdException("機器標識ID不合法");
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
/**
* 獲取下一個ID
*
* @return
*/
public synchronized long nextId() {
long currentTmestamp = getNowTimestamp();
if (currentTmestamp < lastTimestamp) {
throw new RuntimeException("時鐘錯誤,拒絕生成ID");
}
if (currentTmestamp == lastTimestamp) {
//相同毫秒內,序列號自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列號已經達到最大
if (sequence == 0L) {
currentTmestamp = getNexMill();
}
} else {
//不同毫秒內,序列號置為0
sequence = 0L;
}
lastTimestamp = currentTmestamp;
return (currentTmestamp - START_TIME) << TIMESTAMP_LEFT //時間戳的部分
| dataCenterId << DATA_CENTER_LEFT //數據中心的部分
| machineId << MACHINE_LEFT //機器標識的部分
| sequence; //序列號的部分
}
/**
* 保證獲取到的毫秒值是在最後一次分發ID的毫秒值之後lastTimestamp
* 當某一個毫秒,序列號用完了之後,等待到下一個毫秒,在進行序列號的使用
*
* @return
*/
private long getNexMill() {
long timestamp = this.getNowTimestamp();
//不斷的遍歷,直到獲取到lastTimestamp下一個毫秒值
while (timestamp <= lastTimestamp) {
//進行時間回撥
timestamp = this.getNowTimestamp();
}
return timestamp;
}
//獲取當前毫秒值
private long getNowTimestamp() {
return System.currentTimeMillis();
}
/**
* 使用當前電腦的MAC生成數據中心標識ID
*
* @param maxDataCenterId
* @return
*/
private static long getDataCenterId(long maxDataCenterId) {
long id = 0L;
try {
InetAddress ip = InetAddress.getLocalHost();
NetworkInterface network = NetworkInterface.getByInetAddress(ip);
if (network == null) {
id = 1L;
} else {
byte[] mac = network.getHardwareAddress();
if (mac != null) {
id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2])
<< 8))) >> 6;
id = id % (maxDataCenterId + 1);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return id;
}
//根據當前電腦的進程PID生成機器識別ID
private static long getMachineId(long dataCenterId, long maxMachineId) {
StringBuilder sb = new StringBuilder();
sb.append(dataCenterId);
//獲取JVM進程的PID
String name = ManagementFactory.getRuntimeMXBean().getName();
if (name != null) {
sb.append(name.split("@")[0]);
}
/**
* MAC+PID 的hashcode 獲取16個低位
*/
int id = sb.toString().hashCode() & 0xffff;
return id % (maxMachineId + 1);
}
public Snowflake() {
dataCenterId = getDataCenterId(MAX_DATA_CENTER_ID);
machineId = getMachineId(dataCenterId, MAX_MACHINE_ID);
}
//public static void main(String[] args) {
// //指定數據中心和機器識別id
// Snowflake snowflake = new Snowflake(2, 3);
// System.out.println("指定數據中心和機器識別ID來生成ID");
// for (int i = 0; i < 10; i++) {
// System.out.println(snowflake.nextId());
// }
//
// //默認快速使用方式
// snowflake = new Snowflake();
// System.out.println("快速使用方式來生成ID");
// for (int i = 0; i < 10; i++) {
// System.out.println(snowflake.nextId());
// }
//}
}