高並發之 - 全局有序唯一id Snowflake 應用實戰
前言
本篇主要介紹高並發算法Snowflake是怎麽應用到實戰項目中的。
對於怎麽理解Snowflake算法,大家可以從網上搜索‘Snowflake’,大量資源可供查看,這裏就不一一詳訴,這裏主要介紹怎麽實戰應用。
對於不理解的,可以看看這篇文章 Twitter-Snowflake,64位自增ID算法詳解
為什麽有Snowflake算法的出現呢?
首先它是Twitter提出來的。
前世今生
以前我們可以用UUID作為唯一標識,但是UUID是無序的,又是英文、數字、橫桿的結合。當我們要生成有序的id並且按時間排序時,UUID必然不是最好的選擇。
當我們需要有序的id時,可以用數據庫的自增長id,但是在當今高並發系統時代下,自增長id速度太慢,滿足不了需求。然而,對於有‘有序的id按時間排序’這一需求時,Twitter提出了它的算法,並且用於Twitter中。
需要註意的地方
可達並發量根據不同的配置不同,每秒上萬並發量不成問題。
id可用時間:69年
使用限制
使用Snowflake其實有個限制,就是必須知道運行中是哪臺機器。比如我們用Azure雲,配置了10個實例(機器),要知道這10個機器是哪一臺。
開始用Snowflake
首先,直接貼Snowflake算法代碼,算法怎麽實現就不具體說:(C#版,java版的代碼也一樣實現)
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace ConsoleApp6 { /// <summary> /// From: https://github.com/twitter/snowflake /// An object that generates IDs. /// This is broken into a separate class in case/// we ever want to support multiple worker threads /// per process /// </summary> public class IdWorker { private long workerId; private long datacenterId; private long sequence = 0L; private static long twepoch = 1288834974657L; /// <summary> /// 機器標識位數 /// </summary> private static long workerIdBits = 5L; /// <summary> /// //數據中心標識位數 /// </summary> private static long datacenterIdBits = 5L; /// <summary> /// //機器ID最大值 /// </summary> private static long maxWorkerId = -1L ^ (-1L << (int)workerIdBits); /// <summary> /// //數據中心ID最大值 /// </summary> private static long maxDatacenterId = -1L ^ (-1L << (int)datacenterIdBits); /// <summary> /// //毫秒內自增位 /// </summary> private static long sequenceBits = 12L; /// <summary> /// //機器ID偏左移12位 /// </summary> private long workerIdShift = sequenceBits; /// <summary> /// //數據中心ID左移17位 /// </summary> private long datacenterIdShift = sequenceBits + workerIdBits; /// <summary> /// //時間毫秒左移22位 /// </summary> private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; private long sequenceMask = -1L ^ (-1L << (int)sequenceBits); private long lastTimestamp = -1L; private static object syncRoot = new object(); /// <summary> /// /// </summary> /// <param name="workerId">機器id,哪臺機器。最大31</param> /// <param name="datacenterId">數據中心id,哪個數據庫,最大31</param> public IdWorker(long workerId, long datacenterId) { // sanity check for workerId if (workerId > maxWorkerId || workerId < 0) { throw new ArgumentException(string.Format("worker Id can‘t be greater than %d or less than 0", maxWorkerId)); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new ArgumentException(string.Format("datacenter Id can‘t be greater than %d or less than 0", maxDatacenterId)); } this.workerId = workerId; this.datacenterId = datacenterId; } public long nextId() { lock (syncRoot) { long timestamp = timeGen(); if (timestamp < lastTimestamp) { throw new ApplicationException(string.Format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0L; } lastTimestamp = timestamp; return ((timestamp - twepoch) << (int)timestampLeftShift) | (datacenterId << (int)datacenterIdShift) | (workerId << (int)workerIdShift) | sequence; } } protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } protected long timeGen() { return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds; } } }
怎麽用呢?
直接用
IdWorker idWorker = new IdWorker(1, 2); long id = idWorker.nextId();
說明
workerId是機器id,表示分布式環境下的那臺機器。datacenterId是數據庫中心,表示哪個數據庫中心。這裏的機器id與數據庫中心id最大是31。
我們看到nextId方法裏面是用鎖來生成id的。
然而我們怎麽真正地應用到我們實際的項目中呢?
Snowflake運用到項目中
例如,我們分布式有三臺機器,1個數據庫。
那麽workerId分別在機器A/B/C中的值為1/2/3,datacenterId都為0。
這個配置好了之後,那麽我們怎麽在代碼裏面編寫呢?
比如,對於一個web應用,我們都知道,在客戶端請求時,服務器都會生成一個Controller,那麽怎麽保證IdWorker實例只能在一臺服務器中存在一個呢?
答案大家都知道,是靜態屬性(當然也可以單例)。下面我們用控制臺程序來模仿一下controller的請求,當10個線程請求時會發生什麽情況。
模仿的Controller如下:
class TestIdWorkerController { private static readonly IdWorker _idWorker = new IdWorker(1, 2); public void GenerateId(HashSet<long> set) { int i = 0; while (true) { if (i++ == 1000000) break; long id = _idWorker.nextId(); lock (set) { if (!set.Add(id)) Console.WriteLine($"same id={id}"); } } } }
我們看到,id會生成1000000個,並且如果有相同的時候打印出來相同的id。(這裏為什麽用鎖來鎖住HashSet,因為HashSet線程不是安全的,所以要用鎖)
下面我在主程序中,開啟10個線程,分別來new一次TestIdWorkerController,new一次Thread。
static void Main(string[] args) { //存放id的集合 HashSet<long> set = new HashSet<long>(); //啟動10個線程 for (int i = 0; i < 10; i++) { TestIdWorkerController testIdWorker = new TestIdWorkerController(); Thread thread = new Thread(() => testIdWorker.GenerateId(set)); thread.Start(); } //每秒鐘打印當前生成的狀態 while (true) { Console.WriteLine($"set.count={set.Count}"); Thread.Sleep(1000 * 1); } }
我們看到,每秒打印輸出的集合,如何輸出的集合數量=1000000(id數)*10(線程數),也側面驗證了沒有重復。
從上圖看出,執行完畢,並且沒打印same,結果也為1000000(id數)*10(線程數)。所以盡情的所用吧。
可以關註本人的公眾號,多年經驗的原創文章共享給大家。
高並發之 - 全局有序唯一id Snowflake 應用實戰