最近在研究分佈式ID的生成方法,發現Twitter的snowflake算法挺有意思,所以親自動手用Java進行了實現。java
snowflake算法用64位整數來表示主鍵,其結構以下圖:git
1 bit符號位:設計者不喜歡負數主鍵?方便使用負數標識不正確的ID?github
41 bit毫秒時間:2^41 / (365 * 24 * 3600 * 1000) ≈ 69年算法
10 bit機房ID + 機器ID:最大值爲1023編程
12 bit遞增序列:最大值爲4095緩存
由於使用機房ID + 機器ID來標識機器,所以能夠分散到每臺業務機器運行而不會產生重複,不須要集中產生主鍵,這是這個算法最大的優勢。多線程
每秒最多能夠生成主鍵數:4096 * 1000毫秒 = 4096000。以當前機器的配置狀況和業務狀況,單機每秒400萬不重複ID不管如何都已經足夠。分佈式
雖然算法自己很簡單,但分佈式集羣面臨的狀況很複雜,編碼過程當中要考慮的因素有不少。廢話很少說,「翠花!上代碼!」ide
(1) System.currentTimeMillis()方法每次執行都要進行一次系統內核調用,系統開銷較大。對於當前的這個序列號生成器來講,只要保證遞增序列從4095歸0時獲取的時間 比 上次歸0時獲取的時間大就不會產生重複值,所以使用一個long變量緩存了最近一次時間。性能
(2) 機房ID 和 機器ID正常狀況下不會發生改變,所以每次從系統更新時間後當即進行或運算並保存,避免頻繁的更新操做。
(3) 配置類AbstractRMConfig 設計成抽象類,用戶可自由實現並註冊到時間發生器便可。
(4) 爲避免業務平靜期遞增序列長時間沒法到達4096,致使緩存時間過舊引起其它問題,所以使用定時線程TimeUpdater每1000毫秒更新一次時間,間隔時間能夠自由設置。
/** * 分佈式時間發生器 * @author Tony.Lau */ public enum TimeGenerator { INSTANCE; private Logger logger = LoggerFactory.getLogger(TimeGenerator.class); private AbstractRMConfig config; private long lastTimeMills; private volatile boolean isFail = true; private int rmid = -1; private final Lock rmidLock = new ReentrantLock(); private ScheduledExecutorService es = Executors.newScheduledThreadPool(1); private boolean isRun = false; /** 獲取緩存時間 */ long getTime() { try { rmidLock.lock(); if (isFail) { return -1l; } return lastTimeMills; } finally { rmidLock.unlock(); } } /** 獲取最新時間 */ long updateTime() { try { rmidLock.lock(); if (isFail) { return -1l; } long temp = (System.currentTimeMillis() << 23 >>> 1) ^ rmid; while (temp <= lastTimeMills) { temp = (System.currentTimeMillis() << 23 >>> 1) ^ rmid; } return lastTimeMills = temp; } finally { rmidLock.unlock(); } } /** 註冊配置信息 */ public RegisterState registerRoomMachine(AbstractRMConfig config) { isFail = true; if (config == null) { return RegisterState.ERROR; } if (config instanceof FailRMConfig) { return RegisterState.FAIL; } try { rmidLock.lock(); this.config = config; if (!updateRmid().equals(RegisterState.OK)) { logger.error("registerRoomMachine error"); return RegisterState.ERROR; } if (!isRun) { int timePeriod = config.getTimeUpdatePeriod(); if(timePeriod < 1){ logger.error("getTimeUpdatePeriod error:" + timePeriod + "<1"); return RegisterState.ERROR; } es.scheduleAtFixedRate(new TimeUpdater(), 0, timePeriod, TimeUnit.MILLISECONDS); isRun = true; } isFail = false; } finally { rmidLock.unlock(); } logger.info("registerRoomMachine success"); return RegisterState.OK; } /** 更新機房ID 和 機器ID */ private RegisterState updateRmid() { logger.debug("updateRmid()"); int roomId = config.getRoomId(); int roomBitNum = config.getRoomBitNum(); int machineId = config.getMachineId(); int machineBitNum = config.getMachineBitNum(); if (roomId < 0 || machineId < 0) { isFail = true; logger.error("房間ID 或 機器ID不能小於0:roomId=" + roomId + "--machineId=" + machineId); return RegisterState.ERROR; } if (roomBitNum < 1 || machineBitNum < 1) { isFail = true; logger.error("房間ID位數 或 機器ID位數不能小於1:roomBitNum=" + roomBitNum + "--machineBitNum=" + machineBitNum); return RegisterState.ERROR; } if (roomBitNum + machineBitNum > 10) { isFail = true; logger.error("房間ID+機器ID組合後位數不能超過10位:roomBitNum=" + roomBitNum + "--machineBitNum=" + machineBitNum); return RegisterState.ERROR; } if (roomId >= (1 << roomBitNum)) { isFail = true; logger.error("機房ID超過設定數值:" + roomId + ">=" + (1 << roomBitNum)); return RegisterState.ERROR; } if (machineId >= (1 << machineBitNum)) { isFail = true; logger.error("機器ID超過設定數值" + machineId + ">=" + (1 << machineBitNum)); return RegisterState.ERROR; } rmid = ((roomId << machineBitNum) ^ machineId) << 12; lastTimeMills = (System.currentTimeMillis() << 23 >>> 1) ^ rmid; return RegisterState.OK; } /** * <b>註冊狀態</b><br> * OK:註冊機房ID和機器ID成功,能夠開始獲取主鍵。<br> * FAIL:註冊Fail對象成功,系統中止產生正確主鍵,所有返回-1。<br> * ERROR:註冊機房ID和機器ID失敗,空對象或者參數錯誤,系統沒法產生正確主鍵,所有返回-1。<br> * * @create 2016-12-22 21:06:35 */ public enum RegisterState { OK, FAIL, ERROR; } /** * <b>時間定時更新器</b><br> * @create 2016-12-22 22:09:45 */ private class TimeUpdater implements Runnable { @Override public void run() { try { updateTime(); } catch (Exception e) { logger.error("定時更新時間發生錯誤", e); } } } }
(1) 多表共用一個實例,避免連鎖更新時間和代碼複雜化。
(2) 每次增加到4096就歸0並更新到最新時間,其它取緩存時間。
(3) 有文章說每次歸0會致使0過多,Hash取模分表後0表的數據會偏多。但彷佛並不會,所以沒有采用隨機數發生器。
/** * <b>分佈式自增加主鍵發生器</b><br> * 枚舉單例,只容許公用一個實例。 * @author Tony.Lau * @create 2016-12-23 09:50:41 */ public enum PrimaryKeyGen { INSTANCE; private final Lock INCR_LOCK = new ReentrantLock(); private int increment = 0; /** * <b>1bit符號位 + 41bit時間 + 機房ID + 機器ID + 12bit自增加ID</b><br> * @return 若是返回值小於等於0,則表示系統環境錯誤;大於0爲正常值。 */ public long getIncrKey() { try { INCR_LOCK.lock(); long time = 0l; if (increment >= 4096) { increment = 0; if((time = TimeGenerator.INSTANCE.updateTime()) < 0){ return -1l; }else{ return time ^ (increment++); } }else{ if((time = TimeGenerator.INSTANCE.getTime()) < 0){ return -1l; }else{ return time ^ (increment++); } } } finally { INCR_LOCK.unlock(); } } }
(1) 實現具體的配置類,譬如從配置文件獲取配置信息,從zookeeper在線獲取配置信息。
(2) 匿名靜態代碼塊註冊配置信息到時間發生器,而後就能夠正常獲取主鍵。
(3) 若是使用Spring容器,可使用@Postconstruct初始化註冊信息。
(4) 配置類的fail()方法:如發生異常狀況,譬如與zookeeper失去鏈接,意味着節點可能被清理,其它機器上線後可能使用了相同的機器ID致使主鍵重複。所以能夠在配置實現類中跟蹤異常信息,並在異常出現時馬上調用fail()方法中止產生正確主鍵。
(5) 配置類的init()方法:如須要使用動態註冊方式,能夠將獲取配置的代碼在這裏實現。
(6) 配置類的refresh()方法:如想動態擴容方便,運行期動態更新機器ID和機房ID,那麼能夠將實現放在這裏。
注意事項:若是機房內的機器時間有快有慢,那麼當一臺機器意外下線,另一臺機器上線搶佔了相同ID,那麼很大可能會產生重複主鍵。編程實現時必定要注意:
① 機器時間必定要儘量一致。
② 新上線機器一段時間內不會搶佔其它機器ID,哪怕其已經下線。
/** * 使用示例 * @author Tony.Lau */ public class Example{ static { RoomMachineConfig config = new RoomMachineConfig(0, 1, 0, 1, 1000); RegisterState state = TimeGenerator.INSTANCE.registerRoomMachine(config); } private static PrimaryKeyGen keyGen = PrimaryKeyGen.INSTANCE; public long getKey(){ return keyGen.getIncrKey(); } private static class RoomMachineConfig extends AbstractRMConfig{ public RoomMachineConfig(){ this.init(); /* if(config.change()){ refresh(); } */ } public RoomMachineConfig(int roomId, int roomBitNum, int machineId, int machineBitNum, int timeUpdatePeriod) { super(roomId, roomBitNum, machineId, machineBitNum, timeUpdatePeriod); /* if(config.change()){ refresh(); } */ } @Override protected RegisterState init() { // 獲取配置並設置參數 //this.roomId = //this.roomBitNum = //this.machineId = //this.machineBitNum = return TimeGenerator.INSTANCE.registerRoomMachine(this); } @Override protected RegisterState refresh() { // 獲取配置並更新參數 //this.roomId = //this.roomBitNum = //this.machineId = //this.machineBitNum = return TimeGenerator.INSTANCE.registerRoomMachine(this); } @Override public int getRoomId() { return roomId; } @Override public int getRoomBitNum() { return roomBitNum; } @Override public int getMachineId() { return machineId; } @Override public int getMachineBitNum() { return machineBitNum; } @Override public int getTimeUpdatePeriod(){ return timeUpdatePeriod; } } }
(1) 單線程循環取4096000個主鍵,恰好1004毫秒,說明沒有性能問題。
(2) 多線程分別循環取4096000個主鍵,用時2248毫秒,未發現重複值。
https://github.com/tonylau08/dcafe
如測試使用過程當中發現任何錯誤,請告知。如以爲不錯,給我顆小星星。謝謝!