程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA編程入門知識 >> 全局唯一ID設計

全局唯一ID設計

編輯:JAVA編程入門知識

在分布式系統中,經常需要使用全局唯一ID查找對應的數據。產生這種ID需要保證系統全局唯一,而且要高性能以及占用相對較少的空間。

全局唯一ID在數據庫中一般會被設成主鍵,這樣為了保證數據插入時索引的快速建立,還需要保持一個有序的趨勢。

這樣全局唯一ID就需要保證這兩個需求:

  • 全局唯一
  • 趨勢有序

全局ID產生的幾種方式

數據庫自增

當服務使用的數據庫只有單庫單表時,可以利用數據庫的auto_increment來生成全局唯一遞增ID.

優勢:

  • 簡單,無需程序任何附加操作
  • 保持定長的增量
  • 在單表中能保持唯一性

劣勢:

  • 高並發下性能不佳,主鍵產生的性能上限是數據庫服務器單機的上限。
  • 水平擴展困難,在分布式數據庫環境下,無法保證唯一性。

UUID

一般的語言中會自帶UUID的實現,比如Java中UUID方式UUID.randomUUID().toString(),可以通過服務程序本地產生,ID的生成不依賴數據庫的實現。

優勢:

  • 本地生成ID,不需要進行遠程調用。
  • 全局唯一不重復。
  • 水平擴展能力非常好。

劣勢:

  • ID有128 bits,占用的空間較大,需要存成字符串類型,索引效率極低。
  • 生成的ID中沒有帶Timestamp,無法保證趨勢遞增

Twitter Snowflake

snowflake是twitter開源的分布式ID生成算法,其核心思想是:產生一個long型的ID,使用其中41bit作為毫秒數,10bit作為機器編號,12bit作為毫秒內序列號。這個算法單機每秒內理論上最多可以生成1000*(2^12)個,也就是大約400W的ID,完全能滿足業務的需求。

根據snowflake算法的思想,我們可以根據自己的業務場景,產生自己的全局唯一ID。因為Java中long類型的長度是64bits,所以我們設計的ID需要控制在64bits。

比如我們設計的ID包含以下信息:

| 41 bits: Timestamp | 3 bits: 區域 | 10 bits: 機器編號 | 10 bits: 序列號 |

產生唯一ID的Java代碼:

import java.security.SecureRandom;

/**
 * 自定義 ID 生成器
 * ID 生成規則: ID長達 64 bits
 * 
 * | 41 bits: Timestamp (毫秒) | 3 bits: 區域(機房) | 10 bits: 機器編號 | 10 bits: 序列號 |
 */
public class CustomUUID {
    // 基准時間
    private long twepoch = 1288834974657L; //Thu, 04 Nov 2010 01:42:54 GMT
    // 區域標志位數
    private final static long regionIdBits = 3L;
    // 機器標識位數
    private final static long workerIdBits = 10L;
    // 序列號識位數
    private final static long sequenceBits = 10L;

    // 區域標志ID最大值
    private final static long maxRegionId = -1L ^ (-1L << regionIdBits);
    // 機器ID最大值
    private final static long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 序列號ID最大值
    private final static long sequenceMask = -1L ^ (-1L << sequenceBits);

    // 機器ID偏左移10位
    private final static long workerIdShift = sequenceBits;
    // 業務ID偏左移20位
    private final static long regionIdShift = sequenceBits + workerIdBits;
    // 時間毫秒左移23位
    private final static long timestampLeftShift = sequenceBits + workerIdBits + regionIdBits;

    private static long lastTimestamp = -1L;

    private long sequence = 0L;
    private final long workerId;
    private final long regionId;

    public CustomUUID(long workerId, long regionId) {

        // 如果超出范圍就拋出異常
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
        }
        if (regionId > maxRegionId || regionId < 0) {
            throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0");
        }

        this.workerId = workerId;
        this.regionId = regionId;
    }

    public CustomUUID(long workerId) {
        // 如果超出范圍就拋出異常
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0");
        }
        this.workerId = workerId;
        this.regionId = 0;
    }

    public long generate() {
        return this.nextId(false, 0);
    }

    /**
     * 實際產生代碼的
     *
     * @param isPadding
     * @param busId
     * @return
     */
    private synchronized long nextId(boolean isPadding, long busId) {

        long timestamp = timeGen();
        long paddingnum = regionId;

        if (isPadding) {
            paddingnum = busId;
        }

        if (timestamp < lastTimestamp) {
            try {
                throw new Exception("Clock moved backwards.  Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

        //如果上次生成時間和當前時間相同,在同一毫秒內
        if (lastTimestamp == timestamp) {
            //sequence自增,因為sequence只有10bit,所以和sequenceMask相與一下,去掉高位
            sequence = (sequence + 1) & sequenceMask;
            //判斷是否溢出,也就是每毫秒內超過1024,當為1024時,與sequenceMask相與,sequence就等於0
            if (sequence == 0) {
                //自旋等待到下一毫秒
                timestamp = tailNextMillis(lastTimestamp);
            }
        } else {
            // 如果和上次生成時間不同,重置sequence,就是下一毫秒開始,sequence計數重新從0開始累加,
            // 為了保證尾數隨機性更大一些,最後一位設置一個隨機數
            sequence = new SecureRandom().nextInt(10);
        }

        lastTimestamp = timestamp;

        return ((timestamp - twepoch) << timestampLeftShift) | (paddingnum << regionIdShift) | (workerId << workerIdShift) | sequence;
    }

    // 防止產生的時間比之前的時間還要小(由於NTP回撥等問題),保持增量的趨勢.
    private long tailNextMillis(final long lastTimestamp) {
        long timestamp = this.timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = this.timeGen();
        }
        return timestamp;
    }

    // 獲取當前的時間戳
    protected long timeGen() {
        return System.currentTimeMillis();
    }
}

使用自定義的這種方法需要注意的幾點:

  • 為了保持增長的趨勢,要避免有些服務器的時間早,有些服務器的時間晚,需要控制好所有服務器的時間,而且要避免NTP時間服務器回撥服務器的時間。
  • 在跨毫秒時,序列號總是歸0,會使得序列號為0的ID比較多,導致生成的ID取模後不均勻,所以序列號不是每次都歸0,而是歸一個0到9的隨機數。
  • 使用這個CustomUUID類,最好在一個系統中能保持單例模式運行。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved