第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。

  1. 引入distributedid-client依賴:在項目pom.xml添加坐標

    <dependencies>
         <dependency>
              <groupId>com.itheima.distributedid</groupId>
              <artifactId>distributedid-client</artifactId>
              <version>1.0-SNAPSHOT</version>
         </dependency>
    </dependencies>
    
  2. 分散式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
    
  3. 獲取ID時,直接調用即可

    Long id = 0L;
    //從服務端獲取自增型ID
    id = DistributedId.autoincrementId("your service name");
                    
    //本地生成雪花演算法ID
    id = DistributedId.snowflake();
    
    //從服務端獲取雪花演算法ID
    id = DistributedId.snowflakeFromServer();
    
    //使用號段模式獲取單個ID
    id = DistributedId.segment();
    
  4. 資料庫腳本

    /*
     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());
    //    }
    //}

}