[TOC]java
上了微服務以後,不少本來很簡單的問題如今都變複雜了,例如全局 ID 這事!算法
鬆哥最近工做中恰好用到這塊內容,因而調研了市面上幾種常見的全局 ID 生成策略,稍微作了一下對比,供小夥伴們參考。sql
當數據庫分庫分表以後,本來的主鍵自增就不方便繼續使用了,須要找到一個新的合適的方案,鬆哥的需求就是在這樣的狀況下提出的。數據庫
接下來咱們一塊兒來捋一捋。api
總體上來講,這個問題有兩種不一樣的思路:緩存
這兩種思路又對應了不一樣的方案,咱們一個一個來看。安全
數據庫本身搞定,就是說我在數據插入的時候,依然不考慮主鍵的問題,但願繼續使用數據庫的主鍵自增,可是很明顯,本來默認的主鍵自增如今無法用了,咱們必須有新的方案。服務器
數據庫分庫分表以後的結構以下圖(假設數據庫中間件用的 MyCat):markdown
此時若是本來的 db一、db二、db3 繼續各自主鍵自增,那麼對於 MyCat 而言,主鍵就不是自增了,主鍵就會重複,用戶從 MyCat 中查詢到的數據主鍵就有問題。網絡
找到問題的緣由,那麼剩下的就好解決了。
咱們能夠直接修改 MySQL 數據庫主鍵自增的起始值和步長。
首先咱們能夠經過以下 SQL 查看與此相關的兩個變量的取值:
SHOW VARIABLES LIKE 'auto_increment%'
能夠看到,主鍵自增的起始值和步長都是 1。
起始值好改,在定義表的時候就能夠設置,步長咱們能夠經過修改這個配置實現:
set @@auto_increment_increment=9;
修改後,再去查看對應的變量值,發現已經變了:
此時咱們再去插入數據,主鍵自增就不是每次自增 1,而是每次自增 9 了。
至於自增起始值其實很好設置,建立表的時候就能夠設置了。
create table test01(id integer PRIMARY KEY auto_increment,username varchar(255)) auto_increment=8;
既然 MySQL 能夠修改自增的起始值和每次增加的步長,如今假設我有 db一、db2 和 db3,我就能夠分別設置這三個庫中表的自增起始值爲 一、二、3,而後自增步長都是 3,這樣就能夠實現自增了。
可是很明顯這種方式不夠優雅,並且處理起來很麻煩,未來擴展也不方便,所以不推薦。
若是你們分庫分表工具剛好使用的是 MyCat,那麼結合 Zookeeper 也能很好的實現主鍵全局自增。
MyCat 做爲一個分佈式數據庫中間,屏蔽了數據庫集羣的操做,讓咱們操做數據庫集羣就像操做單機版數據庫同樣,對於主鍵自增,它有本身的方案:
這裏咱們主要來看方案 4。
配置步驟以下:
server.xml
schema.xml
設置主鍵自增,而且設置主鍵爲 id 。
在 myid.properties 中配置 zookeeper 信息:
sequence_conf.properties
注意,這裏表名字要大寫。
最後重啓 MyCat ,刪掉以前建立的表,而後建立新表進行測試便可。
這種方式就比較省事一些,並且可擴展性也比較強,若是選擇了 MyCat 做爲分庫分表工具,那麼這種不失爲一種最佳方案。
前面介紹這兩種都是在數據庫或者數據庫中間件層面來處理主鍵自增,咱們 Java 代碼並不須要額外工做。
接下來咱們再來看幾種須要在 Java 代碼中進行處理的方案。
最容易想到的就是 UUID (Universally Unique Identifier) 了,
UUID 的標準型式包含 32 個 16 進制數字,以連字號分爲五段,形式爲 8-4-4-4-12 的 36 個字符,這個是 Java 自帶的,用着也簡單,最大的優點就是本地生成,沒有網絡消耗,可是但凡在公司作開發的小夥伴都知道這個東西在公司項目中使用並很少。緣由以下:
所以,UUID 並不是最佳方案。
雪花算法是由 Twitter 公佈的分佈式主鍵生成算法,它可以保證不一樣進程主鍵的不重複性,以及相同進程主鍵的有序性。在同一個進程中,它首先是經過時間位保證不重複,若是時間相同則是經過序列位保證。
同時因爲時間位是單調遞增的,且各個服務器若是大致作了時間同步,那麼生成的主鍵在分佈式環境能夠認爲是整體有序的,這就保證了對索引字段的插入的高效性。
例如 MySQL 的 Innodb 存儲引擎的主鍵。使用雪花算法生成的主鍵,二進制表示形式包含 4 部分,從高位到低位分表爲:1bit 符號位、41bit 時間戳位、10bit 工做進程位以及 12bit 序列號位。
預留的符號位,恆爲零。
41 位的時間戳能夠容納的毫秒數是 2 的 41 次冪,一年所使用的毫秒數是:365 24 60 60 1000。經過計算可知:Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L);
結果約等於 69.73 年。
ShardingSphere 的雪花算法的時間紀元從 2016 年 11 月 1 日零點開始,可使用到 2086 年,相信能知足絕大部分系統的要求。
該標誌在 Java 進程內是惟一的,若是是分佈式應用部署應保證每一個工做進程的 id 是不一樣的。該值默認爲 0,可經過屬性設置。
該序列是用來在同一個毫秒內生成不一樣的 ID。若是在這個毫秒內生成的數量超過 4096 (2 的 12 次冪),那麼生成器會等待到下個毫秒繼續生成。
注意: 該算法存在 時鐘回撥 問題,服務器時鐘回撥會致使產生重複序列,所以默認分佈式主鍵生成器提供了一個最大容忍的時鐘回撥毫秒數。 若是時鐘回撥的時間超過最大容忍的毫秒數閾值,則程序報錯;若是在可容忍的範圍內,默認分佈式主鍵生成器會等待時鐘同步到最後一次主鍵生成的時間後再繼續工做。 最大容忍的時鐘回撥毫秒數的默認值爲 0,可經過屬性設置。
下面鬆哥給出一個雪花算法的工具類,你們能夠參考:
public class IdWorker { // 時間起始標記點,做爲基準,通常取系統的最近時間(一旦肯定不能變更) private final static long twepoch = 1288834974657L; // 機器標識位數 private final static long workerIdBits = 5L; // 數據中心標識位數 private final static long datacenterIdBits = 5L; // 機器ID最大值 private final static long maxWorkerId = -1L ^ (-1L << workerIdBits); // 數據中心ID最大值 private final static long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 毫秒內自增位 private final static long sequenceBits = 12L; // 機器ID偏左移12位 private final static long workerIdShift = sequenceBits; // 數據中心ID左移17位 private final static long datacenterIdShift = sequenceBits + workerIdBits; // 時間毫秒左移22位 private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; private final static long sequenceMask = -1L ^ (-1L << sequenceBits); /* 上次生產id時間戳 */ private static long lastTimestamp = -1L; // 0,併發控制 private long sequence = 0L; private final long workerId; // 數據標識id部分 private final long datacenterId; public IdWorker(){ this.datacenterId = getDatacenterId(maxDatacenterId); this.workerId = getMaxWorkerId(datacenterId, maxWorkerId); } /** * @param workerId * 工做機器ID * @param datacenterId * 序列號 */ public IdWorker(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; } /** * 獲取下一個ID * * @return */ public synchronized long nextId() { long timestamp = timeGen(); if (timestamp < lastTimestamp) { throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } if (lastTimestamp == timestamp) { // 當前毫秒內,則+1 sequence = (sequence + 1) & sequenceMask; if (sequence == 0) { // 當前毫秒內計數滿了,則等待下一秒 timestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = timestamp; // ID偏移組合生成最終的ID,並返回ID long nextId = ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift) | (workerId << workerIdShift) | sequence; return nextId; } private long tilNextMillis(final long lastTimestamp) { long timestamp = this.timeGen(); while (timestamp <= lastTimestamp) { timestamp = this.timeGen(); } return timestamp; } private long timeGen() { return System.currentTimeMillis(); } /** * <p> * 獲取 maxWorkerId * </p> */ protected static long getMaxWorkerId(long datacenterId, long maxWorkerId) { StringBuffer mpid = new StringBuffer(); mpid.append(datacenterId); String name = ManagementFactory.getRuntimeMXBean().getName(); if (!name.isEmpty()) { /* * GET jvmPid */ mpid.append(name.split("@")[0]); } /* * MAC + PID 的 hashcode 獲取16個低位 */ return (mpid.toString().hashCode() & 0xffff) % (maxWorkerId + 1); } /** * <p> * 數據標識id部分 * </p> */ protected 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(); id = ((0x000000FF & (long) mac[mac.length - 1]) | (0x0000FF00 & (((long) mac[mac.length - 2]) << 8))) >> 6; id = id % (maxDatacenterId + 1); } } catch (Exception e) { System.out.println(" getDatacenterId: " + e.getMessage()); } return id; } }
用法以下:
IdWorker idWorker = new IdWorker(0, 0); for (int i = 0; i < 1000; i++) { System.out.println(idWorker.nextId()); }
Leaf 是美團開源的分佈式 ID 生成系統,最先期需求是各個業務線的訂單 ID 生成需求。在美團早期,有的業務直接經過 DB 自增的方式生成 ID,有的業務經過 Redis 緩存來生成 ID,也有的業務直接用 UUID 這種方式來生成 ID。以上的方式各自有各自的問題,所以美團決定實現一套分佈式 ID 生成服務來知足需求目前 Leaf 覆蓋了美團點評公司內部金融、餐飲、外賣、酒店旅遊、貓眼電影等衆多業務線。在4C8G VM 基礎上,經過公司 RPC 方式調用,QPS 壓測結果近 5w/s,TP999 1ms(TP=Top Percentile,Top 百分數,是一個統計學裏的術語,與平均數、中位數都是一類。TP50、TP90 和 TP99 等指標經常使用於系統性能監控場景,指高於 50%、90%、99% 等百分線的狀況)。
目前 LEAF 的使用有兩種不一樣的思路,號段模式和 SNOWFLAKE 模式,你能夠同時開啓兩種方式,也能夠指定開啓某種方式(默認兩種方式爲關閉狀態)。
咱們從 GitHub 上 Clone LEAF 以後,它的配置文件在 leaf-server/src/main/resources/leaf.properties
中,各項配置的含義以下:
。
能夠看到,若是使用號段模式,須要數據庫支持;若是使用 SNOWFLAKE 模式,須要 Zookeeper 支持。
號段模式仍是基於數據庫,可是思路有些變化,以下:
若是使用號段模式,咱們首先須要建立一張數據表,腳本以下:
CREATE DATABASE leaf CREATE TABLE `leaf_alloc` ( `biz_tag` varchar(128) NOT NULL DEFAULT '', `max_id` bigint(20) NOT NULL DEFAULT '1', `step` int(11) NOT NULL, `description` varchar(256) DEFAULT NULL, `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`biz_tag`) ) ENGINE=InnoDB; insert into leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')
這張表中各項字段的含義以下:
配置完成後,啓動項目,訪問 http://localhost:8080/api/segment/get/leaf-segment-test
路徑(路徑最後面的 leaf-segment-test 是業務標記),便可拿到 ID。
能夠經過以下地址訪問到號段模式的監控頁面 http://localhost:8080/cache
。
號段模式優缺點:
優勢
缺點
SNOWFLAKE 模式須要配合 Zookeeper 一塊兒,不過 SNOWFLAKE 對 Zookeeper 的依賴是弱依賴,把 Zookeeper 啓動以後,咱們能夠在 SNOWFLAKE 中配置 Zookeeper 信息,以下:
leaf.snowflake.enable=true leaf.snowflake.zk.address=192.168.91.130 leaf.snowflake.port=2183
而後從新啓動項目,啓動成功後,經過以下地址能夠訪問到 ID:
http://localhost:8080/api/snowflake/get/test
這個主要是利用 Redis 的 incrby 來實現,這個我以爲沒啥好說的。
zookeeper 也能作,可是比較麻煩,不推薦。
綜上,若是項目中剛好使用了 MyCat,那麼可使用 MyCat+Zookeeper,不然建議使用 LEAF,兩種模式皆可。