談談雪花算法的使用


背景

618來臨之際,為了應對一些突發流量,購買了兩台一個月的ECS用來臨時對部分項目擴容。其中一個項目有用到雪花算法來生成Id,這個還是挺OK的。

不過發現要在配置文件中手動配置機器碼!!配置的時候還要先知道目前配置了那些,這樣才可以避免重復。

經過了解,除了會有單機單實例的情況,還會有單機多實例的情況。

這個要人工配置,是徒增工作量的,有點讓人難以接受。

針對這個,老黃就做了一點調整,讓這個機器碼自動生成。

雪花算法基礎

關於雪花算法,大部分文章都可以看到這個圖。這個圖很好的詮釋了雪花算法生成Id的幾個重要組成部分,這里也不展開具體介紹了。

時間戳,工作機器Id,序列號這些位數是可以根據自己的業務場景來進來調整的。

10bit工作機器Id,其實就是上面說到的機器碼,雪花算法內部並沒有做任何處理,而是交由業務方自己定義,所以業務方需要自己保證這個的唯一性。

大部分情況,會把它分為5bit數據中心標識和5bit機器Id。這樣的話可以支持32個數據中心和32個機器Id。

換句話說就是,一個業務可以在一個數據中心部署32個實例,最多部署的32個數據中心。正常來說,大部分項目,都不會需要部署這么多實例。。。

考慮到內網的IP段基本上是固定的,同一個應用基本上也會在連續的IP上面部署。

所以這里老黃最后采用的是本地IP地址取余做為機器Id,機器的HostName取余做為默認的數據中心Id。

下面來看看具體的實現。

簡單實現

自動獲取機器Id和數據中心Id。

/// <summary>
/// 獲取機器Id
/// </summary>
/// <returns></returns>
private int GetWorkerId()
{
    var workerId = 0;

    try
    {
        IPAddress ipaddress = IPAddress.Parse("0.0.0.0");
        NetworkInterface[] interfaces = NetworkInterface.GetAllNetworkInterfaces();
        foreach (NetworkInterface ni in interfaces)
        {
            if (ni.NetworkInterfaceType == NetworkInterfaceType.Ethernet)
            {
                foreach (UnicastIPAddressInformation ip in
                    ni.GetIPProperties().UnicastAddresses)
                {
                    if (ip.Address.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
                    {
                        ipaddress = ip.Address;
                        break;
                    }
                }
            }
        }
        Console.WriteLine($"ip = {ipaddress.ToString()}");
        var val = ipaddress.GetAddressBytes().Sum(x => x);
        // 取余
        workerId = val % (int)MaxWorkerId;
    }
    catch
    {
        // 異常的話,生成一個隨機數
        workerId = new Random().Next((int)MaxWorkerId - 1);
    }

    return workerId;
}

/// <summary>
/// 獲取數據中心Id
/// </summary>
/// <returns></returns>
private int GetDatacenterId()
{
    var hostName = Dns.GetHostName();
    Console.WriteLine($"hostname = {hostName}");
    var val = System.Text.Encoding.UTF8.GetBytes(hostName).Sum(x => x);
    // 取余
    return val % (int)MaxDatacenterId;
}

生成器的構造函數

public IdGenerator(long datacenterId = -1)
{
    if (datacenterId == -1)
    {
        // default
        datacenterId = GetDatacenterId();
    }

    if (datacenterId > MaxDatacenterId || datacenterId < 0)
    {
        throw new ArgumentException("非法數據標志ID", nameof(datacenterId));
    }

    // 先檢驗再賦值
    WorkerId = GetWorkerId();
    DatacenterId = datacenterId;

    Console.WriteLine($"w = {WorkerId}");
    Console.WriteLine($"d = {DatacenterId}");
}

這里的數據中心可以讓使用方自己定義,默認-1的話,會根據HostName去生成一個。

這里給一個自定義的可選標識,主要還是考慮到了單機多實例,即一個IP上面部署多個實例。

雖然這個時候還是要考慮人工配置,不過已經從多機變成單機了,也算是一點簡化。畢竟大部分情況下也不會建議在同一個機器部署多個一樣的項目。

默認情況下的使用,IdGenerator對象要全局唯一,做成單例即可。

IdGenerator generator = new IdGenerator();

Parallel.For(0, 20, x =>
{
    Console.WriteLine(generator.NextId()); 
});

Console.WriteLine("Hello World!");
System.Threading.Thread.Sleep(1000 * 60);

下面運行多個容器來模擬。

可以看到機器Id和數據中心Id都是沒有重復的。

在運行一次。

也是同樣的。

不足與展望

目前這種做法在應用實例少,機器數量少的情況下是基本可以滿足使用要求的了。老黃公司目前也就不到30台服務器,所以怎么都是夠用的。

但是依靠IP和HostName,隨着實例或機器的數量增多,沒有辦法保證它們取余算出來的一定是唯一的。

在這種情況下就需要考慮引用第三方存儲(Redis或數據庫)來保證這個的唯一性了。

下面是本文的示例代碼:

SnowflakeDemo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM