創建一個基於redis的id生成器

參考文章:

  • https://blog.csdn.net/hengyunabc/article/details/44244951
  • https://www.jianshu.com/p/955909e1bd71
  • https://tech.meituan.com/2017/04/21/mt-leaf.html

參考項目:https://github.com/hengyunabc/redis-id-generator值。
evalsha教程:https://www.runoob.com/redis/scripting-evalsha.html
eval教程:https://www.runoob.com/redis/scripting-eval.html



一、分佈式id生成器需要滿足的要求

1.全局唯一

2.儘可能保證id的遞增

因爲在查詢的時候經常會有例如分頁以及排序之類的需求,這個時候如果主鍵的id本身能夠體現出時許效率會更加好。而對於常見的排序還有分頁,我們解決辦法有兩種:

  1. 在數據表中添加一個時間字段,對其創建一個普通索引。
  2. id本生就是按照時間大致有序的。

因爲常見的普通索引的訪問效率是比聚集索引要慢的,所以我們儘可能使用第二種解決方案

3.其他的一些要求

  1. id要儘可能的短,這樣可以減少存儲的空間以及增加查詢的效率。
  2. 要有足夠數量的id可以使用,不然當數據量非常大時,id耗盡就不行了
  3. 要考慮不同機器之間的時間不一致問題
  4. QPS儘量要高,這樣就可以,否則例如像類SNOWFLAKE算法會在64位ID中利用部分位數(如12)表示單位時間內生成的ID序號,這部分序號用完了,這個單位時間就不能再生成序號了


二、常見的id生成器方案:

1.利用mysql數據庫的自增主鍵特性

優點:
  • 簡單,代碼方便,性能還行
  • 數字的id是遞增的,方便進行分頁和排序
缺點:
  • 不同的數據庫語法和實現不同,實現數據遷移或者多數據庫版本的時候可能會出現一些問題
  • 我們常見的是一主多從數據庫,這會產生單點故障,以及性能瓶頸
  • 數據量大時需要考慮分庫分表
優化方案:
  • 使用多個master,對每個master設置的初始id不同,步長不同,例如有四個master,我們可以讓master1生成(1,5,9),master2生成(2,6,10),master3生成(3,7,11),master4生成(4,8,12),這樣可以降低單個數據庫的壓力

2.UUID

常見的一種分佈式id生成器,可以利用數據庫也可以利用代碼。

優點:
  • 簡單,方便
  • 生成id的性能好,基本上不會有性能問題
  • 全球唯一,對於數據庫合併,遷移等問題不會存在衝突
缺點
  • 不是有序的
  • UUID的字符串長度較長,查詢效率不高,且消耗存儲空間比較大,如果是海量數據庫就需要考慮存儲量的問題了
  • 可讀性差

3.redis生成id

redis的大致原理和普通數據庫的生成原理是大致相同的,只不過redis不是使用自增組件,而是使用原子操作 INCR和INCRBY來實現。

優點:
  • 不依賴於數據庫,靈活方便,且性能優於數據庫。
    數字ID天然排序,對分頁或者需要排序的結果很有幫助。
缺點:
  • 如果系統中沒有Redis,還需要引入新的組件,增加系統複雜度。
  • 需要編碼和配置的工作量比較大。

4.snowflake算法

  1. 一個ID由64位生成
  2. 41bit作爲時間戳,記錄當前時間到標記的起始時間(如到2018.1.1)差,精確到毫秒,那麼服務可用時長爲(1<<41)/(1000* 60 * 60 * 24 * 365) = 69.73年
  3. 10bit作爲機器ID,也就是可以有1024臺機器
  4. 12bit作爲序列號,代表單位時間(這裏是毫秒)內允許生成的ID總數,也就是1ms內允許生成4096個ID
優點:
  • 不依賴於數據庫,靈活方便,且性能優於數據庫。
  • ID按照時間在單機上是遞增的。
缺點:
  • 在單機上是遞增的,但是由於涉及到分佈式環境,每臺機器上的時鐘不可能完全同步,也許有時候也會出現不是全局遞增的情況。

5.類SNOWFLAKE算法

SNOWFLAKE給出的主要是一個思想,把ID劃分爲多個段,有不同的含義,可以結合自己的要求進行重新劃分。按照個人理解,時間戳位數少了,機器位數多了,序列號位數多了。借鑑snowflake的思想,結合各公司的業務邏輯和併發量,可以實現自己的分佈式ID生成算法。

舉例,假設某公司ID生成器服務的需求如下:
  • 單機高峯併發量小於1W,預計未來5年單機高峯併發量小於10W
  • 有2個機房,預計未來5年機房數量小於4個
  • 每個機房機器數小於100臺
  • 目前有5個業務線有ID生成需求,預計未來業務線數量小於10個
分析過程如下:
  • 高位取從2017年1月1日到現在的毫秒數(假設系統ID生成器服務在這個時間之後上線),假設系統至少運行10年,那至少需要10年 * 365天 * 24小時 * 3600秒 * 1000毫秒 = 320 * 10 ^ 9,差不多預留39bit給毫秒數
  • 每秒的單機高峯併發量小於10W,即平均每毫秒的單機高峯併發量小於100,差不多預留7bit給每毫秒內序列號
  • 5年內機房數小於4個,預留2bit給機房標識
  • 每個機房小於100臺機器,預留7bit給每個機房內的服務器標識
  • 業務線小於10個,預留4bit給業務線標識
這樣設計的64bit標識,可以保證:
  • 每個業務線、每個機房、每個機器生成的ID都是不同的
  • 同一個機器,每個毫秒內生成的ID都是不同的
  • 同一個機器,同一個毫秒內,以序列號區區分保證生成的ID是不同的
  • 將毫秒數放在最高位,保證生成的ID是趨勢遞增的
缺點:
  • 由於“沒有一個全局時鐘”,每臺服務器分配的ID是絕對遞增的,但從全局看,生成的ID只是趨勢遞增的(有些服務器的時間早,有些服務器的時間晚)


三、實現一個簡易的redis的id生成器

利用redis的lua腳本執行功能,在每個節點上通過lua腳本生成唯一id,其中使用的是雪花算法。
生成的ID是64位的:

  • 使用41 bit來存放時間,精確到毫秒,可以使用41年。
  • 使用12 bit來存放邏輯分片ID,最大分片ID是4095
  • 使用10 bit來存放自增長ID,意味着每個節點,每毫秒最多可以生成1024個ID

比如GTM時間 Fri Mar 13 10:00:00 CST 2015 ,它的距1970年的毫秒數是 1426212000000,假定分片ID是53,自增長序列是4,則生成的ID是:

// 左移22位,指代最前面14bit的存儲信息,再左移10位表示中間存儲分片信息的12bit
5981966696448054276 = 1426212000000 << 22 + 53 << 10 + 4

redis提供了TIME命令,可以取得redis服務器上的秒數和微秒數。因些lua腳本返回的是一個四元組。

second, microSecond, partition, seq

客戶端要自己處理,生成最終ID。

((second * 1000 + microSecond / 1000) << (12 + 10)) + (shardId << 10) + seq;

seq對應的是集羣中的節點值
如集羣裏有3個節點,則節點1返回的seq是:

0, 3, 6, 9, 12 ...

節點2返回的seq是

1, 4, 7, 10, 13 ...

節點3返回的seq是

2, 5, 8, 11, 14 ...

我們可以將lua腳本轉換成sha1值,然後通過EVALSHA指令傳遞這個

下面我們直接看代碼

項目主程序

public class Main {

	public static void main(String[] args) {
		String tab = "order";
		long userId = 123456789;

		IdGenerator idGenerator = IdGenerator.builder()
				.addHost("127.0.0.1", 6379, "c5809078fa6d652e0b0232d552a9d06d37fe819c")
//				.addHost("127.0.0.1", 7379, "accb7a987d4fb0fd85c57dc5a609529f80ec3722")
//				.addHost("127.0.0.1", 8379, "f55f781ca4a00a133728488e15a554c070b17255")
				.build();

		long id = idGenerator.next(tab, userId);

		System.out.println("id:" + id);
		List<Long> result = IdGenerator.parseId(id);

		System.out.println("miliSeconds:" + result.get(0) + ", partition:"
				+ result.get(1) + ", seq:" + result.get(2));
	}
}
id生成器相關代碼
public class IdGenerator {

	static final Logger logger = LoggerFactory.getLogger(IdGenerator.class);
	/**
	 * JedisPool, luaSha
	 */
	List<Pair<JedisPool, String>> jedisPoolList;
	int retryTimes;

	int index = 0;

	private IdGenerator() {

	}

	private IdGenerator(List<Pair<JedisPool, String>> jedisPoolList,
			int retryTimes) {
		this.jedisPoolList = jedisPoolList;
		this.retryTimes = retryTimes;
	}

	static public IdGeneratorBuilder builder() {
		return new IdGeneratorBuilder();
	}

	static class IdGeneratorBuilder {
		List<Pair<JedisPool, String>> jedisPoolList = new ArrayList();
		int retryTimes = 5;

		public IdGeneratorBuilder addHost(String host, int port, String luaSha) {
			jedisPoolList.add(Pair.of(new JedisPool(host, port), luaSha));
			return this;
		}

		public IdGeneratorBuilder retryTimes(int retryTimes) {
			this.retryTimes = retryTimes;
			return this;
		}

		public IdGenerator build() {
			return new IdGenerator(jedisPoolList, retryTimes);
		}
	}

	public long next(String tab) {
		return next(tab, 0);
	}

	public long next(String tab, long shardId) {
		for (int i = 0; i < retryTimes; ++i) {
			Long id = innerNext(tab, shardId);
			if (id != null) {
				return id;
			}
		}
		throw new RuntimeException("Can not generate id!");
	}

	Long innerNext(String tab, long shardId) {
		index++;
		Pair<JedisPool, String> pair = jedisPoolList.get(index
				% jedisPoolList.size());
		JedisPool jedisPool = pair.getLeft();

		String luaSha = pair.getRight();
		Jedis jedis = null;
		try {
			jedis = jedisPool.getResource();
			List<Long> result = (List<Long>) jedis.evalsha(luaSha, 2, tab, "" + shardId);
			long id = buildId(result.get(0), result.get(1), result.get(2),
					result.get(3));
			return id;
		} catch (JedisConnectionException e) {
			if (jedis != null) {
				jedisPool.returnBrokenResource(jedis);
			}
			logger.error("generate id error!", e);
		} finally {
			if (jedis != null) {
				jedisPool.returnResource(jedis);
			}
		}
		return null;
	}

	public static long buildId(long second, long microSecond, long shardId,
			long seq) {
		long miliSecond = (second * 1000 + microSecond / 1000);
		return (miliSecond << (12 + 10)) + (shardId << 10) + seq;
	}

	public static List<Long> parseId(long id) {
		long miliSecond = id >>> 22;
		// 2 ^ 12 = 0xFFF
		long shardId = (id & (0xFFF << 10)) >> 10;
		long seq = id & 0x3FF;

		List<Long> re = new ArrayList<Long>(4);
		re.add(miliSecond);
		re.add(shardId);
		re.add(seq);
		return re;
	}
}

至此我們的基於redis的id生成器就完成了

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