9種分佈式ID生成方式,總有一款適合你

在這裏插入圖片描述

分佈式ID必要性。

業務量小於500W或數據容量小於2G的時候單獨一個mysql便可提供服務,再大點的時候就進行讀寫分離也能夠應付過來。但當主從同步也扛不住的是就須要分表分庫了,但分庫分表後須要有一個惟一ID來標識一條數據,數據庫的自增ID顯然不能知足需求;特別一點的如訂單、優惠券也都須要有惟一ID作標識。此時一個可以生成全局惟一ID的系統是很是必要的。那麼這個全局惟一ID就叫分佈式ID。java

分佈式ID需知足那些條件node

  • 全局惟一:基本要求就是必須保證ID是全局性惟一的。
  • 高性能:高可用低延時,ID生成響應要快。
  • 高可用:無限接近於100%的可用性
  • 好接入:遵循拿來主義原則,在系統設計和實現上要儘量的簡單
  • 趨勢遞增:最好趨勢遞增,這個要求就得看具體業務場景了,通常不嚴格要求

1. UUID

UUID 是指Universally Unique Identifier,翻譯爲中文是通用惟一識別碼,UUID 的目的是讓分佈式系統中的全部元素都能有惟一的識別信息。形式爲 8-4-4-4-12,總共有 36個字符。用起來很是簡單mysql

import java.util.UUID;
 public static void main(String[] args) {
  String uuid = UUID.randomUUID().toString().replaceAll("-","");
  System.out.println(uuid);
 }

輸出結果 99a7d0925b294a53b2f4db9d5a3fb798,但UUID卻並不適用於實際的業務需求。訂單號用UUID這樣的字符串沒有絲毫的意義,看不出和訂單相關的有用信息;而對於數據庫來講用做業務主鍵ID,它不只是太長仍是字符串,存儲性能差查詢也很耗時,因此不推薦用做分佈式ID。git

優勢:生成足夠簡單,本地生成無網絡消耗,具備惟一性缺點:無序的字符串,不具有趨勢自增特性,沒有具體的業務含義。如此長的字符串當MySQL主鍵並不是明智選擇。github

2. 基於數據庫自增ID

基於數據庫的auto_increment自增ID徹底能夠充當分佈式ID,具體實現:須要一個單獨的MySQL實例用來生成ID,建表結構以下:web

CREATE DATABASE `SoWhat_ID`;
CREATE TABLE SoWhat_ID.SEQUENCE_ID (
    `id` bigint(20) unsigned NOT NULL auto_increment, 
    `value` char(10) NOT NULL default '',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
) ENGINE
=MyISAM;
insert into SEQUENCE_ID(value) VALUES ('values');

當咱們須要一個ID的時候,向表中插入一條記錄返回主鍵ID,但這種方式有一個比較致命的缺點,訪問量激增時MySQL自己就是系統的瓶頸,用它來實現分佈式服務風險比較大,不推薦!redis

優勢:實現簡單,ID單調自增,數值類型查詢速度快算法

缺點:DB單點存在宕機風險,沒法扛住高併發場景sql

3. 基於數據庫集羣模式

前邊說了單點數據庫方式不可取,那對上邊的方式作一些高可用優化,換成主從模式集羣。懼怕一個主節點掛掉無法用,那就作雙主模式集羣,也就是兩個Mysql實例都能單獨的生產自增ID。那這樣還會有個問題,兩個MySQL實例的自增ID都從1開始,會生成重複的ID怎麼辦?解決方案:設置起始值和自增步長數據庫

MySQL_1 配置:

set @@auto_increment_offset = 1;     -- 起始值
set @@auto_increment_increment = 2;  -- 步長

MySQL_2 配置:

set @@auto_increment_offset = 2;     -- 起始值
set @@auto_increment_increment = 2;  -- 步長

這樣兩個MySQL實例的自增ID分別就是:

一、三、五、七、9 
二、四、六、八、10

可是若是兩個仍是沒法知足咋辦呢?增長第三臺MySQL實例須要人工修改1、二兩臺MySQL實例的起始值和步長,把第三臺機器的ID起始生成位置設定在比現有最大自增ID的位置遠一些,但必須在1、二兩臺MySQL實例ID尚未增加到第三臺MySQL實例的起始ID值的時候,不然自增ID就要出現重複了,必要時可能還須要停機修改。

優勢:解決DB單點問題

缺點:不利於後續擴容,並且實際上單個數據庫自身壓力仍是大,依舊沒法知足高併發場景。

4. 基於數據庫的號段模式

號段模式是當下分佈式ID生成器的主流實現方式之一,號段模式能夠理解爲從數據庫批量的獲取自增ID,每次從數據庫取出一個號段範圍,例如 (1,1000] 表明1000個ID,具體的業務服務將本號段,生成1~1000的自增ID並加載到內存。表結構以下:

CREATE TABLE id_generator (
  `id` int(10NOT NULL,
  `max_id` bigint(20NOT NULL COMMENT '當前最大id',
  `step` int(20NOT NULL COMMENT '號段的步長',
  `biz_type`    int(20NOT NULL COMMENT '業務類型',
  `version` int(20NOT NULL COMMENT '版本號',
  PRIMARY KEY (`id`)
)
  • max_id :當前最大的可用id
  • step :表明號段的長度
  • biz_type :表明不一樣業務類型
  • version :是一個樂觀鎖,每次都更新version,保證併發時數據的正確性
id biz_type max_id step version
1 101 1000 2000 0

等這批號段ID用完,再次向數據庫申請新號段,對max_id字段作一次update操做,update max_id= max_id + step,update成功則說明新號段獲取成功,新的號段範圍是(max_id ,max_id +step]。

update id_generator set max_id = {max_id+step}, version = version + 1
 where version =  {versionand biz_type = XX

因爲多業務端可能同時操做,因此採用版本號 version 樂觀鎖方式更新,這種分佈式ID生成方式不強依賴於數據庫,不會頻繁的訪問數據庫,對數據庫的壓力小不少。可是若是遇到了雙十一或者秒殺相似的活動仍是會對數據庫有比較高的訪問。

5. 基於Redis模式

Redis 也一樣能夠實現,原理就是Redis 是單線程的,所以咱們能夠利用redis的incr命令實現ID的原子性自增。

127.0.0.1:6379> set seq_id 1     // 初始化自增ID爲1
OK
127.0.0.1:6379> incr seq_id      // 增長1,並返回遞增後的數值
(integer) 2

用redis實現須要注意一點,要考慮到redis持久化的問題。redis有兩種持久化方式RDB和AOF。

6. 基於雪花算法(Snowflake)模式

SnowFlake 算法,是 Twitter 開源的分佈式 id 生成算法。其核心思想就是:使用一個 64 bit 的 long 型的數字做爲全局惟一 id。在分佈式系統中的應用十分普遍,且ID 引入了時間戳,爲何叫雪花算法呢?私覺得衆所周知世界上沒有一對相同的雪花。雪花算法基本上保持自增的,後面的代碼中有詳細的註解。這 64 個 bit 中,其中 1 個 bit 是不用的,而後用其中的 41 bit 做爲毫秒數,用 10 bit 做爲工做機器 id,12 bit 做爲序列號。舉例如上圖:

  1. 第一個部分是 1 個 bit:0, 這個是無心義的。由於二進制裏第一個 bit 位若是是 1,那麼都是負數,可是咱們生成的 id 都是正數,因此第一個 bit 統一都是 0。
  2. 第二個部分是 41 個 bit:表示的是時間戳。單位是毫秒。41 bit 能夠表示的數字多達 2^41 - 1,也就是能夠標識 2 ^ 41 - 1 個毫秒值,換算成年就是表示 69 年的時間。
  3. 第三個部分是 5  個 bit:表示的是機房 id 5 個 bit 表明機器 id。意思就是最多表明 2 ^ 5 個機房(32 個機房)
  4. 第四個部分是 5  個 bit:表示的是機器 id。每一個機房裏能夠表明 2 ^ 5 個機器(32 臺機器),也能夠根據本身公司的實際狀況肯定。
  5. 第五個部分是 12 個 bit:表示的序號,就是某個機房某臺機器上這 一毫秒內同時生成的 id 的序號。12 bit 能夠表明的最大正整數是 2 ^ 12 - 1 = 4096,也就是說能夠用這個 12 bit 表明的數字來區分同一個毫秒內的 4096 個不一樣的 id。

總結:簡單來講,你的某個服務假設要生成一個全局惟一 id,那麼就能夠發送一個請求給部署了 SnowFlake 算法的系統,由這個 SnowFlake 算法系統來生成惟一 id。

這個 SnowFlake 算法系統首先確定是知道本身所在的機房和機器的,好比機房 id = 17,機器 id = 12。

接着 SnowFlake 算法系統接收到這個請求以後,首先就會用二進制位運算的方式生成一個 64 bit 的 long 型 id,64 個 bit 中的第一個 bit 是無心義的。

接着 41 個 bit,就能夠用當前時間戳(單位到毫秒),而後接着 5 個 bit 設置上這個機房 id,還有 5 個 bit 設置上機器 id。

最後再判斷一下,當前這臺機房的這臺機器上這一毫秒內,這是第幾個請求,給此次生成 id 的請求累加一個序號,做爲最後的 12 個 bit。最終一個 64 個 bit 的 id 就出來了,相似於:這個算法能夠保證一個機房的一臺機器在同一毫秒內,生成了一個惟一的 id。可能一個毫秒內會生成多個 id,可是有最後 12 個 bit 的序號來區分開來。

總結:就是用一個 64 bit 的數字中各個 bit 位來設置不一樣的標誌位,區分每個 id。

SnowFlake 算法的實現代碼以下:

/**
 * 雪花算法相對來講若是思緒捋順了實現起來比較簡單,前提熟悉位運算。
 */

public class SnowFlake
{
 /**
  * 開始時間截 (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 << workerIdBits);

 /**
  * 支持的最大機房標識id,結果是31
  */

 private final long maxDataCenterId = ~(-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 << sequenceBits);

 /**
  * 工做機器ID(0~31)
  */

 private volatile long workerId;

 /**
  * 機房中心ID(0~31)
  */

 private volatile long dataCenterId;

 /**
  * 毫秒內序列(0~4095)
  */

 private volatile long sequence = 0L;

 /**
  * 上次生成ID的時間截
  */

 private volatile long lastTimestamp = -1L;

 //==============================Constructors=====================================

 /**
  * 構造函數
  *
  * @param workerId     工做ID (0~31)
  * @param dataCenterId 機房中心ID (0~31)
  */


 public SnowFlake(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 (該方法是線程安全的)
  * 若是一個線程反覆獲取Synchronized鎖,那麼synchronized鎖將變成偏向鎖。
  *
  * @return SnowflakeId
  */

 public synchronized long nextId() throws RuntimeException
 
{
  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;
   //毫秒內序列溢出,一毫秒內超過了4095個
   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 當前時間戳
  */

 private long tilNextMillis(long lastTimestamp)
 
{
  long timestamp = timeGen();
  while (timestamp <= lastTimestamp)
  {
   timestamp = timeGen();
  }
  return timestamp;
 }

 /**
  * 返回以毫秒爲單位的當前時間
  * @return 當前時間(毫秒)
  */

 private long timeGen()
 
{
  return System.currentTimeMillis();
 }
}

SnowFlake算法的優勢

  • 高性能高可用:生成時不依賴於數據庫,徹底在內存中生成。
  • 容量大:每秒中能生成數百萬的自增ID。
  • ID自增:存入數據庫中,索引效率高。

SnowFlake算法的缺點

  • 依賴與系統時間的一致性,若是系統時間被回調,或者改變,可能會形成id衝突或者重複。

實際中咱們的機房並無那麼多,咱們能夠改進改算法,將10bit的機器id優化成業務表或者和咱們系統相關的業務。

7. 百度uid-generator

項目GitHub地址:https://github.com/baidu/uid-generator,uid-generator是由百度技術部開發,基於Snowflake算法實現的,與原始的snowflake算法不一樣在於,uid-generator支持自定義時間戳、工做機器ID和 序列號等各部分的位數,並且uid-generator中採用用戶自定義workId的生成策略。

uid-generator須要與數據庫配合使用,須要新增一個WORKER_NODE表。當應用啓動時會向數據庫表中去插入一條數據,插入成功後返回的自增ID就是該機器的workId數據由host,port組成。由上圖可知,UidGenerator的時間部分只有28位,這就意味着UidGenerator默認只能承受8.5年(2^28-1/86400/365)。固然,根據你業務的需求,UidGenerator能夠適當調整delta seconds、worker node id和sequence佔用位數。

接下來分析百度UidGenerator的實現。須要說明的是UidGenerator有兩種方式提供:和DefaultUidGeneratorCachedUidGenerator。咱們先分析比較容易理解的DefaultUidGenerator

DefaultUidGenerator

delta seconds這個值是指當前時間與epoch時間的時間差,且單位爲。epoch時間就是指集成UidGenerator生成分佈式ID服務第一次上線的時間,可配置,也必定要根據你的上線時間進行配置,由於默認的epoch時間但是2016-09-20,不配置的話,會浪費好幾年的可用時間。

worker id接下來講一下UidGenerator是如何給worker id賦值的,搭建UidGenerator的話,須要建立一個表:UidGenerator會在集成用它生成分佈式ID的實例啓動的時候,往這個表中插入一行數據,獲得的id值就是準備賦給workerId的值。因爲workerId默認22位,那麼,集成UidGenerator生成分佈式ID的全部實例重啓次數是不容許超過4194303次(即2^22-1),不然會拋出異常。

這段邏輯的核心代碼來自DisposableWorkerIdAssigner.java中,固然,你也能夠實現WorkerIdAssigner.java接口,自定義生成workerId。sequence核心代碼以下,幾個實現的關鍵點:

  • synchronized保證線程安全。
  • 若是時間有任何的回撥,那麼直接拋出異常。
  • 若是當前時間和上一次是同一秒時間,那麼sequence自增。若是同一秒內自增值超過2^13-1,那麼就-- 會自旋等待下一秒(getNextSecond)。
  • 若是是新的一秒,那麼sequence從新從0開始。
/**
     * Get UID
     *
     * @return UID
     * @throws UidGenerateException in the case: Clock moved backwards; Exceeds the max timestamp
     */

    protected synchronized long nextId() {
        long currentSecond = getCurrentSecond();
        // Clock moved backwards, refuse to generate uid
        if (currentSecond < lastSecond) {
            long refusedSeconds = lastSecond - currentSecond;
            throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
        }
        // At the same second, increase sequence
        if (currentSecond == lastSecond) {
            sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
            // Exceed the max sequence, we wait the next second to generate uid
            if (sequence == 0) {
                currentSecond = getNextSecond(lastSecond);
            }
        // At the different second, sequence restart from zero
        } else {
            sequence = 0L;
        }
        lastSecond = currentSecond;
        // Allocate bits for UID
        return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
    }

總結經過DefaultUidGenerator的實現可知,它對時鐘回撥的處理比較簡單粗暴。另外若是使用UidGenerator的DefaultUidGenerator方式生成分佈式ID,必定要根據你的業務的狀況和特色,調整各個字段佔用的位數:

<property name="timeBits" value="28"/>
<property name="workerBits" value="22"/>
<property name="seqBits" value="13"/>
<property name="epochStr" value="2016-09-20"/>

CachedUidGenerator

CachedUidGeneratorUidGenerator的重要改進實現。它的核心利用了RingBuffer,以下圖所示,它本質上是一個數組,數組中每一個項被稱爲slot。UidGenerator設計了兩個RingBuffer,一個保存惟一ID,一個保存flag。RingBuffer的尺寸是2^n,n必須是正整數:具體細節閱讀Git源碼便可,能夠直接經過 SpringBoot 集成開發使用。

8. 美團(Leaf)

Leaf由美團開發,github地址:https://github.com/Meituan-Dianping/Leaf,Leaf同時支持號段模式和snowflake算法模式,能夠 切換使用。

號段模式

先導入源碼 https://github.com/Meituan-Dianping/Leaf ,在建一張表leaf_alloc

DROP TABLE IF EXISTS `leaf_alloc`;
CREATE TABLE `leaf_alloc` (
  `biz_tag` varchar(128)  NOT NULL DEFAULT '' COMMENT '業務key',
  `max_id` bigint(20NOT NULL DEFAULT '1' COMMENT '當前已經分配了的最大id',
  `step` int(11NOT NULL COMMENT '初始步長,也是動態調整的最小步長',
  `description` varchar(256)  DEFAULT NULL COMMENT '業務key的描述',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '數據庫維護的更新時間',
  PRIMARY KEY (`biz_tag`)
ENGINE=InnoDB;

而後在項目中開啓號段模式,配置對應的數據庫信息,並關閉snowflake模式

leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://localhost:3306/leaf_test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
leaf.jdbc.username=root
leaf.jdbc.password=root

leaf.snowflake.enable=false
#leaf.snowflake.zk.address=
#leaf.snowflake.port=

啓動leaf-server 模塊的 LeafServerApplication項目就跑起來了 號段模式獲取分佈式自增ID的測試url :http://localhost:8080/api/segment/get/leaf-segment-test 監控號段模式:http://localhost:8080/cache

snowflake模式

Leaf的snowflake模式依賴於ZooKeeper,不一樣於原始snowflake算法也主要是在workId的生成上,Leaf中workId是基於ZooKeeper的順序Id來生成的,每一個應用在使用Leaf-snowflake時,啓動時都會都在Zookeeper中生成一個順序Id,至關於一臺機器對應一個順序節點,也就是一個workId。

leaf.snowflake.enable=true
leaf.snowflake.zk.address=127.0.0.1
leaf.snowflake.port=2181

snowflake模式獲取分佈式自增ID的測試url:http://localhost:8080/api/snowflake/get/test

9. 滴滴(Tinyid)

Tinyid 由滴滴開發,Github地址:https://github.com/didi/tinyid

Tinyid是一個ID生成器服務,它提供了REST API和Java客戶端兩種獲取方式,若是使用Java客戶端獲取方式的話,官方宣稱能單實例能達到1kw QPS(Over10 million QPSper single instance when using the java client.)

Tinyid教程 的原理很是簡單,經過數據庫表中的數據基本是就能猜出個八九不離十,就是經典的segment模式,和美團的leaf原理幾乎一致。原理圖以下所示,以同一個bizType爲例,每一個tinyid-server會分配到不一樣的segment,例如第一個tinyid-server分配到(1000, 2000],第二個tinyid-server分配到(2000, 3000],第3個tinyid-server分配到(3000, 4000]:再以第一個tinyid-server爲例,當它的segment用了20%(核心源碼:segmentId.setLoadingId(segmentId.getCurrentId().get() + idInfo.getStep() * Constants.LOADING_PERCENT / 100);,LOADING_PERCENT的值就是20),即設定loadingId爲20%的閾值,例如當前id是10000,步長爲10000,那麼loadingId=12000。那麼當請求分佈式ID分配到12001時(或者重啓後),即超過loadingId,就會返回一個特殊code:new Result(ResultCode.LOADING, id);tinyid-server根據ResultCode.LOADING這個響應碼就會異步分配下一個segment(4000, 5000],以此類推。具體使用參考


本文分享自微信公衆號 - sowhat1412(sowhat9094)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索