常見的全局ID生成方案

在分佈式系統架構中,常常都須要一個全局的ID生成器,來保證系統中某些業務場景中對於主鍵的要求,當前實現ID生成的方式仍是挺多的。本文咱們來談談常見的ID生成方式。 全局ID該有的特性html

  1. 惟一性: 確保生成的ID爲全網惟一
  2. 有序性: 確保ID對於某項業務來講是有序性遞增的
  3. 時間戳: 可清楚知道ID生成的時間點
  4. 高可用性: 確保任什麼時候間都能生成有效ID

##常見的ID生成方式java

一. UUID

UUID是由一組32位數的16進制數字所構成,是故UUID理論上的總數爲16^32=2^128,約等於3.4 x 10^38。也就是說若每納秒產生1兆個UUID,要花100億年纔會將全部UUID用完。mysql

UUID的標準型式包含32個16進制數字,以連字號分爲五段,形式爲8-4-4-4-12的32個字符。示例:550e8400-e29b-41d4-a716-446655440000git

JAVAgithub

package com.mytest;
import java.util.UUID;
 
public class UTest {
     
    public static void main(String[] args) {
        UUID uuid = UUID.randomUUID();
        System.out.println(uuid);
     }
}
複製代碼

優勢:

  • 簡單, 通常開發語言中都會自帶生成方式。
  • 本地生成, 不須要額外的網絡消耗
  • 性能高
  • 水平擴展能力強

缺點:

  • 不易於存儲, ID有128bit,佔用空間大
  • 無序性, 若是做爲數據庫主鍵會影響性能
  • 無心義, UUID是一串無規則字符串, 無任何業務含義, 無時間戳。

二. 數據庫自增

mysql數據庫使用auto_increment 字段來輔助爲ID自增。 oracle數據庫經過建立sequence 來生成ID自增。redis

--ORACLE
-- 建立sequence
create sequence seq_test
minvalue 1
maxvalue 9999999
start with 1
increment by 1
cache 50;

--建表 
drop table test; 
create table test( 
ID integer 
,stu_name nvarchar2(4) 
,stu_age number 
); 

--插入數據 
insert into test values(seq_test.nextval,'Mary',15); 
insert into test values(seq_test.nextval,'Tom',16); 
複製代碼

優勢:

  • 簡單,無需任何額外程序操做

缺點:

  • 高併發狀況下性能不佳
  • 水平擴展困難,分佈式環境下沒法確保ID全局惟一

三. snowflake

snowflake是Twitter開源的分佈式ID生成算法,結果是一個long型的ID。其核心思想是:使用41bit做爲毫秒數,10bit做爲機器的ID(5個bit是數據中心,5個bit的機器ID),12bit做爲毫秒內的流水號(意味着每一個節點在每毫秒能夠產生 4096 個 ID),最後還有一個符號位,永遠是0。算法

snowflake算法的組成部分:spring

  1. 41位的時間序列(精確到毫秒,41位的長度可使用69年)
  2. 10位的機器標識(10位的長度最多支持部署1024個節點)
  3. 12位的計數順序號(12位的計數順序號支持每一個節點每毫秒產生4096個ID序號) 最高位是符號位,始終爲0。

java 實現方式sql

/** * Twitter_Snowflake<br> * SnowFlake的結構以下(每部分用-分開):<br> * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br> * 1位標識,因爲long基本類型在Java中是帶符號的,最高位是符號位,正數是0,負數是1,因此id通常是正數,最高位是0<br> * 41位時間截(毫秒級),注意,41位時間截不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截) * 獲得的值),這裏的的開始時間截,通常是咱們的id生成器開始使用的時間,由咱們程序來指定的(以下下面程序IdWorker類的startTime屬性)。41位的時間截,可使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br> * 10位的數據機器位,能夠部署在1024個節點,包括5位datacenterId和5位workerId<br> * 12位序列,毫秒內的計數,12位的計數順序號支持每一個節點每毫秒(同一機器,同一時間截)產生4096個ID序號<br> * 加起來恰好64位,爲一個Long型。<br> * SnowFlake的優勢是,總體上按照時間自增排序,而且整個分佈式系統內不會產生ID碰撞(由數據中心ID和機器ID做區分),而且效率較高,經測試,SnowFlake每秒可以產生26萬ID左右。 */
public class SnowflakeIdWorker {

    // ==============================Fields===========================================
    /** 開始時間截 (2015-01-01) */
    private final long twepoch = 1420041600000L;

    /** 機器id所佔的位數 */
    private final long workerIdBits = 5L;

    /** 數據標識id所佔的位數 */
    private final long datacenterIdBits = 5L;

    /** 支持的最大機器id,結果是31 (這個移位算法能夠很快的計算出幾位二進制數所能表示的最大十進制數) */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    /** 支持的最大數據標識id,結果是31 */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);

    /** 序列在id中佔的位數 */
    private final long sequenceBits = 12L;

    /** 機器ID向左移12位 */
    private final long workerIdShift = sequenceBits;

    /** 數據標識id向左移17位(12+5) */
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    /** 時間截向左移22位(5+5+12) */
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    /** 生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095) */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

    /** 工做機器ID(0~31) */
    private long workerId;

    /** 數據中心ID(0~31) */
    private long datacenterId;

    /** 毫秒內序列(0~4095) */
    private long sequence = 0L;

    /** 上次生成ID的時間截 */
    private long lastTimestamp = -1L;

    //==============================Constructors=====================================
    /** * 構造函數 * @param workerId 工做ID (0~31) * @param datacenterId 數據中心ID (0~31) */
    public SnowflakeIdWorker(long workerId, long datacenterId) {
        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));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

    // ==============================Methods==========================================
    /** * 得到下一個ID (該方法是線程安全的) * @return SnowflakeId */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //若是當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                    String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        //若是是同一時間生成的,則進行毫秒內序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒內序列溢出
            if (sequence == 0) {
                //阻塞到下一個毫秒,得到新的時間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //時間戳改變,毫秒內序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的時間截
        lastTimestamp = timestamp;

        //移位並經過或運算拼到一塊兒組成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
                | (datacenterId << datacenterIdShift) //
                | (workerId << workerIdShift) //
                | sequence;
    }

    /** * 阻塞到下一個毫秒,直到得到新的時間戳 * @param lastTimestamp 上次生成ID的時間截 * @return 當前時間戳 */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /** * 返回以毫秒爲單位的當前時間 * @return 當前時間(毫秒) */
    protected long timeGen() {
        return System.currentTimeMillis();
    }

    //==============================Test=============================================
    /** 測試 */
    public static void main(String[] args) {
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0);
        for (int i = 0; i < 1000; i++) {
            long id = idWorker.nextId();
            System.out.println(Long.toBinaryString(id));
            System.out.println(id);
        }
    }
}

複製代碼

優勢:

  • 性能好,低延時, 能知足Twitter每秒上萬條請求
  • 獨立部署, 穩定性高
  • 可根據自身業務分配bit位, 靈活性好

缺點:

  • 強依賴機器時間, 若是機器上時間回撥,會致使發號重複或者服務會處於不可用狀態。

四. Redis實現全局ID生成器

定義一個通用的key,該key的規則是時間格式,精確到秒,保證每秒都是不一樣的key(固然key的規則能夠根據自身業務需求進行定製),value的值是一個long型的整數,前半部分是當前時間精確到秒,後面是自增的值,設計成5位,不夠的補0,這樣基本就是每秒最多能生成99999個ID,基本能知足大部分的需求,若是須要更多,能夠多保留幾位就行。 java 實現方式數據庫

/** * 使用redis生成分佈式ID */
public interface IdGeneratorService {

    /** * @param biz 業務名稱 */
    long generatorId(String biz);

    /** * * @return */
    long generatorId();
}
複製代碼
package com.test;

import com.google.common.base.Strings;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.Calendar;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import lombok.extern.slf4j.Slf4j;

@Service
@Slf4j
public class RedisIdGeneratorService implements IdGeneratorService {

    private static final String keyPrefix = "smart";

    /** * JedisClient對象 */
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;


    /** * @Description * @author butterfly */
    private String getIDPrefix() {
        Date date = new Date();
        Calendar c = Calendar.getInstance();
        c.setTime(date);
        int year = c.get(Calendar.YEAR);
        int day = c.get(Calendar.DAY_OF_YEAR); // 今天是第多少天
        int hour = c.get(Calendar.HOUR_OF_DAY);
        int minute = c.get(Calendar.MINUTE);
        int second = c.get(Calendar.SECOND);
        String dayFmt = String.format("%1$03d", day); // 0補位操做 必須知足三位
        String hourFmt = String.format("%1$02d", hour);  // 0補位操做 必須知足2位
        String minuteFmt = String.format("%1$02d", minute);  // 0補位操做 必須知足2位
        String secondFmt = String.format("%1$02d", second);  // 0補位操做 必須知足2位
        StringBuffer prefix = new StringBuffer();
        prefix.append((year - 2000)).append(dayFmt).append(hourFmt).append(minuteFmt).append(secondFmt);
        return prefix.toString();
    }


    /** * @author butterfly */
    private long incrDistrId(String biz) {
        String prefix = getIDPrefix();
        String orderId = null;
        String key = "#{biz}:id:".replace("#{biz}", biz).concat(prefix); // 00001
        try {
            ValueOperations<String, Object> valueOper = redisTemplate.opsForValue();
            Long index = valueOper.increment(key, 1);
            orderId = prefix.concat(String.format("%1$05d", index)); // 補位操做 保證知足5位
        } catch (Exception ex) {
            log.error("分佈式訂單號生成失敗異常。。。。。", ex);
        } finally {
            redisTemplate.expire(key, 600, TimeUnit.SECONDS);//保留10分鐘內的key
        }
        if (Strings.isNullOrEmpty(orderId)) return 0;
        return Long.parseLong(orderId);
    }

    /** * @Description 生成分佈式ID * @author butterfly */
    @Override
    public long generatorId(String biz) {
        // 轉成數字類型,可排序
        return incrDistrId(biz);
    }

    @Override
    public long generatorId() {
        return incrDistrId(keyPrefix);
    }
}
複製代碼

優勢:

  • 可以必定程度上保持Id具備必定的增加規律,有利於索引和排序
  • 比較靈活,能夠根據本身的須要,定製不一樣的ID的格式

缺點:

  • 依賴於Redis,須要引入Redis中間件的配置
  • 增長運維成本

五. 美團Leaf

Leaf是美團基礎研發平臺推出的一個分佈式ID生成服務,名字取自德國哲學家、數學家萊布尼茨的一句話:「There are no two identical leaves in the world.」Leaf具有高可靠、低延遲、全局惟一等特色。

Leaf能保證全局惟一性、趨勢遞增、單調遞增、信息安全,裏面也提到了幾種分佈式方案的對比,但也須要依賴關係數據庫、Zookeeper等中間件,這裏就再也不詳細展開,有興趣可參考此前美團技術博客的一篇文章:《Leaf美團分佈式ID生成服務》 Leaf項目Github地址: github.com/Meituan-Dia…

總結

這篇文章列舉了幾種常見的全局ID生成方案, 每種方式有各自的優缺點和適用的場景。在實際的工做中,須要結合自身的工做環境和業務需求, 進行合理選型。 沒有最好的方案,只有最適合的方案。


歡迎長按下圖關注公衆號: 終身幼稚園

相關文章
相關標籤/搜索