分佈式ID生成策略(1)_snowflake算法

  最近在研究分佈式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.0 分佈式時間發生器

1.1 設計考慮

(1) System.currentTimeMillis()方法每次執行都要進行一次系統內核調用,系統開銷較大。對於當前的這個序列號生成器來講,只要保證遞增序列從4095歸0時獲取的時間 比 上次歸0時獲取的時間大就不會產生重複值,所以使用一個long變量緩存了最近一次時間。性能

(2) 機房ID 和 機器ID正常狀況下不會發生改變,所以每次從系統更新時間後當即進行或運算並保存,避免頻繁的更新操做。

(3) 配置類AbstractRMConfig 設計成抽象類,用戶可自由實現並註冊到時間發生器便可。

(4) 爲避免業務平靜期遞增序列長時間沒法到達4096,致使緩存時間過舊引起其它問題,所以使用定時線程TimeUpdater每1000毫秒更新一次時間,間隔時間能夠自由設置。

 

1.2 代碼

/**
 * 分佈式時間發生器
 * @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);
			}
		}
	}

}

 

2.0 分佈式自增加主鍵發生器

2.1 設計考慮

(1) 多表共用一個實例,避免連鎖更新時間和代碼複雜化。

(2) 每次增加到4096就歸0並更新到最新時間,其它取緩存時間。

(3) 有文章說每次歸0會致使0過多,Hash取模分表後0表的數據會偏多。但彷佛並不會,所以沒有采用隨機數發生器。

 

2.2 代碼

/**
 * <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();
		}
	}
	
}

 

3.0 使用示例

3.1 使用步驟

(1) 實現具體的配置類,譬如從配置文件獲取配置信息,從zookeeper在線獲取配置信息。

(2) 匿名靜態代碼塊註冊配置信息到時間發生器,而後就能夠正常獲取主鍵。

(3) 若是使用Spring容器,可使用@Postconstruct初始化註冊信息。

(4) 配置類的fail()方法:如發生異常狀況,譬如與zookeeper失去鏈接,意味着節點可能被清理,其它機器上線後可能使用了相同的機器ID致使主鍵重複。所以能夠在配置實現類中跟蹤異常信息,並在異常出現時馬上調用fail()方法中止產生正確主鍵。

(5) 配置類的init()方法:如須要使用動態註冊方式,能夠將獲取配置的代碼在這裏實現。

(6) 配置類的refresh()方法:如想動態擴容方便,運行期動態更新機器ID和機房ID,那麼能夠將實現放在這裏。

  注意事項:若是機房內的機器時間有快有慢,那麼當一臺機器意外下線,另一臺機器上線搶佔了相同ID,那麼很大可能會產生重複主鍵。編程實現時必定要注意:

  ① 機器時間必定要儘量一致。

  ② 新上線機器一段時間內不會搶佔其它機器ID,哪怕其已經下線。

 

3.2 示例代碼

/**
 * 使用示例
 * @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;
		}

	}
	
}

 

4.0 其它事項

4.1 測試結果

(1) 單線程循環取4096000個主鍵,恰好1004毫秒,說明沒有性能問題。

(2) 多線程分別循環取4096000個主鍵,用時2248毫秒,未發現重複值。

 

4.2 源碼地址

https://github.com/tonylau08/dcafe

如測試使用過程當中發現任何錯誤,請告知。如以爲不錯,給我顆小星星。謝謝!

相關文章
相關標籤/搜索