自定義分佈式ID算法

自定義分佈式ID算法

算法原理

參考雪花算法,將當前時間減去基準時間(8位36進制) + 自增序列(4位36進制) + 服務器ID(4位10進制:worker+datacenter) + 隨機數(4位10進制)。

例如:當前時間和20100101的差值(8位36進制可使用89年,9位可以使用3220年) + 自增序列(4位36進制,每毫秒支持160萬個自增序列值) +服務器ID(4位10進制:worker+datacenter) + 隨機數(4位10進制,可拆分8192個數據庫),理論上不同服務器每秒支持生成至少16億個不重複ID(實際筆記本測試可達400萬個ID/秒)

使用36進制的原因:

主要是解決雪花算法時間回撥導致ID重複問題,增加ID的實際精度實現唯一性。之所以不採用62進制是因爲ID有可能用於文件名,部分數據庫可能設置爲不區分大小寫的場景,而且62進制對ID的整體長度影響不大。

優點

  1. 毫秒數在高位,自增序列在低位,整個 ID 都是趨勢遞增的。(同一毫秒內排序可能不正確,因爲計數器沒做清零)
  2. 不依賴數據庫等第三方系統,以服務的方式部署,穩定性更高,生成 ID 的性能也是非常高的。可以達到400萬個ID/秒。
  3. 可以根據自身業務特性分配 bit 位,非常靈活。
  4. 支持類似mycat的分庫分表機制,可以根據策略進行分庫的框架,最大支持拆分8192個數據庫。
  5. ID長度在89年內保持20位,大於89年爲21位,有隨機數和自增序列可以保證數據的安全。
  6. 理論不存在時鐘回撥問題,計數器每毫秒不清零,並且每毫秒有9999個隨機數。
  7. 可支持本地批量生成,緩存ID。

缺點

  1. ID是20位字符串,相對自增序列佔用更大的存儲空間。
  2. 不滿足具備業務規則的id生成方式,例如希望以年月日開頭的id。
  3. 分庫分表只支持隨機或者根據worker+datacenter方式拆分。不支持類似自增序列的方式進行拆分。
  4. VARCHAR似乎比NUMBER類型性能差一些(oracle自己測試感覺差的不多),對索引的支持可能不同的數據庫有一些差異,例如對id有order by 或者是大於,小余等需求。
  5. 其他未知問題。

源碼

StringIdGenerator

import java.security.SecureRandom;

/**
 * 20位字符串Id生成類
 *
 * @author huangsq
 * @version 1.0, 2018-02-19
 */
public class StringIdGenerator implements IdGenerator<String> {

	/**
	 * 開始時間截 (20100101)
	 */
	public static final long START_TIME = 1262275200000L;

	public static final int MIN_SEQUENCE = 50000;
	public static final int MAX_SEQUENCE = 1679615;
	/**
	 * 支持的最大服務器ID標識
	 */
	public static final long MAX_SERVER_ID = 9999;
	/**
	 * 默認進制
	 */
	private static final int RADIX = 36;

	/**
	 * 機器id所佔的位數
	 */
	private static final long workerIdBits = 12L;
	/**
	 * 數據標識id所佔的位數
	 */
	private static final long datacenterIdBits = 12L;
	/**
	 * 支持的最大機器id,結果是4095
	 */
	private static final long maxWorkerId = -1L ^ (-1L << workerIdBits);
	/**
	 * 支持的最大數據標識id,結果是4095
	 */
	private static final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
	/**
	 * 機器ID向左移12位
	 */
	private final long workerIdShift = 0;
	/**
	 * 數據標識id向左移17位(12+5)
	 */
	private final long datacenterIdShift = workerIdBits;

	private SecureRandom random = new SecureRandom();

	private int sequence = MIN_SEQUENCE;

	public static void main(String[] args) {
		System.out.println(maxWorkerId);
	}

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

	/**
	 * 服務器ID(0~9999)
	 */
	private int serverId;

	/**
	 * 構造函數
	 *
	 * @param workerId     工作ID (0~31)
	 * @param datacenterId 數據中心ID (0~31)
	 */
	public StringIdGenerator(int workerId, int 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.serverId = (datacenterId << datacenterIdShift) | (workerId << workerIdShift);
	}

	/**
	 * 生成20位字符串ID:當前時間和20100101的差值(8位36進制可使用89年,9位3220年) + 自增序列(4位36進制,160萬個自增序列值) +<br/>
	 * 服務器ID(4位10進制:worker+datacenter) + 隨機數(4位10進制,可拆分8192個數據庫),理論上不同服務器每秒支持生成16億個不重複ID(實際400萬個ID/秒)。
	 *
	 * @return
	 * @author huangsq
	 */
	public String nextId() {
		return nextId(serverId);
	}

	/**
	 * 生成20位字符串ID:當前時間和20100101的差值(8位36進制可使用89年,9位3220年) + 自增序列(4位36進制,160萬個自增序列值) +<br/>
	 * 服務器ID(4位10進制:worker+datacenter) + 隨機數(4位10進制,可拆分8192個數據庫),理論上不同服務器每秒支持生成16億個不重複ID(實際400萬個ID/秒)。
	 *
	 * @param serverId 服務器ID(worker+datacenter)
	 * @return
	 * @author huangsq
	 */
	public String nextId(int serverId) {
		if (serverId > MAX_SERVER_ID || serverId < 0) {
			throw new IllegalArgumentException(String.format("serverId can't be &gt %d or &lt 0", MAX_SERVER_ID));
		}
		StringBuilder idString = timeIdBuilder(RADIX);
		appendId(idString, serverId);
		appendId(idString, random.nextInt(10000));

		return idString.toString();
	}

	/**
	 * 生成16位字符串ID:當前時間和20100101的差值(8位36進制可使用89年,9位3220年) + 自增序列(4位36進制,160萬個自增序列值) +<br/>
	 * 隨機數(4位10進制),理論上每秒支持生成16億個不重複ID。多用於臨時文件名。
	 *
	 * @return
	 * @author huangsq
	 */
	public String timeId() {
		StringBuilder idString = timeIdBuilder(RADIX);
		appendId(idString, random.nextInt(10000));

		return idString.toString();
	}

	/**
	 * 生成12位字符串ID:當前時間和20100101的差值(8位36進制可使用89年,9位3220年) + 自增序列(4位36進制,160萬個自增序列值) +<br/>
	 *
	 * @return
	 * @author huangsq
	 */
	private synchronized StringBuilder timeIdBuilder(int radix) {
		long timestamp = timeGen();
		// 如果是同一時間生成的,則進行毫秒內序列
		++sequence;
		if (sequence > MAX_SEQUENCE) {
			sequence = MIN_SEQUENCE;
			if (lastTimestamp == timestamp) { // 毫秒內序列溢出
				// 阻塞到下一個毫秒,獲得新的時間戳
				timestamp = tilNextMillis(lastTimestamp);
			}
		}
		lastTimestamp = timestamp;

		StringBuilder idString = new StringBuilder();
		idString.append(NumberUtil.toString(timestamp - START_TIME, radix));
		idString.append(NumberUtil.toString(sequence, radix));
		return idString;
	}

	private void appendId(StringBuilder idString, int number) {
		if (number < 1) {
			idString.append("0000");
		} else if (number < 10) {
			idString.append("000").append(number);
		} else if (number < 100) {
			idString.append("00").append(number);
		} else if (number < 1000) {
			idString.append('0').append(number);
		} else {
			idString.append(number);
		}
	}

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

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

}

NumberUtil

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

/**
 * 進制轉換工具類
 * @author huangsq
 * @version 1.0, 2018-02-19
 */
public final class NumberUtil {

	static final String CHAR62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
	static final char[] digits = CHAR62.toCharArray();

	static final Map<Character, Integer> digitMap = new HashMap<Character, Integer>();

	/**
	 * 支持的最大進制數
	 */
	public static final int MAX_RADIX = digits.length;

	/**
	 * 支持的最小進制數
	 */
	public static final int MIN_RADIX = 2;
	/**
	 * 32進制
	 */
	public static final int RADIX32 = 32;
	/**
	 * 36進制
	 */
	public static final int RADIX36 = 36;

	/**
	 * 默認除法運算精度
	 */
	public static final int DEF_DIV_SCALE = 17;

	/**
	 * 隨機數
	 */
	private static final Random random = new Random();

	static {
		for (int i = 0; i < digits.length; i++) {
			digitMap.put(digits[i], (int) i);
		}
	}

	private NumberUtil() {
	}

	/**
	 * 將長整型數值轉換爲62進制數
	 * @param num
	 * @return
	 */
	public static String toString(long num) {
		return toString(num, MAX_RADIX);
	}

	/**
	 * 將長整型數值轉換爲32進制數
	 * @param num
	 * @return
	 */
	public static String toString32(long num) {
		return toString(num, RADIX32);
	}

	/**
	 * 將長整型數值轉換爲36進制數
	 * @param num
	 * @return
	 */
	public static String toString36(long num) {
		return toString(num, RADIX36);
	}

	/**
	 * 將長整型數值轉換爲指定的進制數(最大支持62進制,字母數字已經用盡)
	 * @param num
	 * @param radix
	 * @return
	 */
	public static String toString(long num, int radix) {
		if (radix < MIN_RADIX || radix > MAX_RADIX)
			radix = 10;
		if (radix == 10)
			return Long.toString(num);

		final int size = 65;
		int charPos = 64;

		char[] buf = new char[size];
		boolean negative = (num < 0);

		if (!negative) {
			num = -num;
		}

		while (num <= -radix) {
			buf[charPos--] = digits[(int) (-(num % radix))];
			num = num / radix;
		}
		buf[charPos] = digits[(int) (-num)];

		if (negative) {
			buf[--charPos] = '-';
		}

		return new String(buf, charPos, (size - charPos));
	}

	/**
	 * 將62進制字符串轉換爲長整型數字
	 * @param str 數字字符串
	 * @param radix 進制數
	 * @return
	 */
	public static long toNumber(String str) {
		return toNumber(str, MAX_RADIX);
	}

	/**
	 * 將32進制字符串轉換爲長整型數字
	 * @param str 數字字符串
	 * @param radix 進制數
	 * @return
	 */
	public static long toNumber32(String str) {
		return toNumber(str, RADIX32);
	}

	/**
	 * 將36進制字符串轉換爲長整型數字
	 * @param str 數字字符串
	 * @param radix 進制數
	 * @return
	 */
	public static long toNumber36(String str) {
		return toNumber(str, RADIX36);
	}

	/**
	 * 將字符串轉換爲長整型數字
	 * @param str 數字字符串
	 * @param radix 進制數
	 * @return
	 */
	public static long toNumber(String str, int radix) {
		if (str == null) {
			throw new NumberFormatException("null");
		}

		if (radix < MIN_RADIX) {
			throw new NumberFormatException("radix " + radix + " less than " + MIN_RADIX);
		}
		if (radix > MAX_RADIX) {
			throw new NumberFormatException("radix " + radix + " greater than " + MAX_RADIX);
		}

		long result = 0;
		boolean negative = false;
		int len = str.length();
		int i = 0;
		long limit = -Long.MAX_VALUE;
		long multmin;
		Integer digit;

		if (len > 0) {
			char firstChar = str.charAt(0);
			if (firstChar < '0') {
				if (firstChar == '-') {
					negative = true;
					limit = Long.MIN_VALUE;
				} else if (firstChar != '+') {
					throw forInputString(str);
				}

				if (len == 1) {
					throw forInputString(str);
				}
				i++;
			}
			multmin = limit / radix;
			while (i < len) {
				digit = digitMap.get(str.charAt(i++));
				if (digit == null) {
					throw forInputString(str);
				}
				if (digit < 0) {
					throw forInputString(str);
				}
				if (result < multmin) {
					throw forInputString(str);
				}
				result *= radix;
				if (result < limit + digit) {
					throw forInputString(str);
				}
				result -= digit;
			}
		} else {
			throw forInputString(str);
		}
		return negative ? result : -result;
	}

	/**
	 * 隨機生成指定長度的字符串,由數字、小寫字母和大寫字母組成
	 * @param length 字符串長度
	 * @return
	 */
	public static String random(int length) {
		return random(length, RADIX36);
	}

	/**
	 * 隨機生成指定長度的字符串,由數字、小寫字母和大寫字母組成
	 * @param length 字符串長度
	 * @param radix 進制數
	 * @return
	 */
	public static String random(int length, int radix) {
		char[] cs = new char[length];
		for (int i = 0; i < length; i++) {
			cs[i] = digits[random.nextInt(radix)];
		}
		return new String(cs);
	}

	private static NumberFormatException forInputString(String s) {
		return new NumberFormatException("For input string: \"" + s + "\"");
	}

}

歡迎留言分享您認爲更優秀的分佈式ID算法!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章