1. 程式人生 > >高並發之 - 全局有序唯一id Snowflake 應用實戰

高並發之 - 全局有序唯一id Snowflake 應用實戰

collect idt 例如 es2015 pub isp 限制 cas don

原文:高並發之 - 全局有序唯一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 應用實戰