記一次隨機字符串生成算法的隨機概率與性能的提升


一、前言背景

前幾天我部門一個和銀行對接的項目中出現了業務Id重復的現象,導致了很多之前不可預見的bug。由於該項目有資金流動,涉及到金錢交易,故不敢有任何閃失。於是leader把同事寫的Handler.ashx.cs發給我瞧了瞧,其中的一處流水號生成代碼引起了我的注意。代碼如下:

string[] str1 = new string[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z" };
string[] str2 = new string[] { "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z" };
string[] str3 = new string[] { "0", "1", "2", "3", "4", "5", "6", "7", "8", "9" };
Random r = new Random();
int s1 = r.Next(0, str1.Length - 1);
string a1 = str1[s1];

r = new Random();
int s2 = r.Next(0, str2.Length - 1);
string a2 = str2[s2];

r = new Random();
int s3 = r.Next(0, str3.Length - 1);
string a3 = str3[s3];

string lsh = a1 + a2 + a3;

由於交易有可能中途失敗,所以每次重新交易的時候,訂單號是不變的,但是會重新生成流水號。也就是說在交易開始的時候生成固定訂單號,到最終交易結束,這一個業務流程有可能一次性成功,有可能重復多次才能成功,畢竟交易有可能第一次就失敗,也有可能連續失敗好幾次。

業務Id=交易訂單號+流水號,其中訂單號預留9位,流水號規定為3位由大小寫英文字母和數字組成的隨機字符串。由於訂單號是固定且唯一的,那么只要保證生成的流水號是唯一的就能夠保證業務Id是唯一的。不過流水號的唯一性的適應范圍是依賴訂單號的,類似於根據訂單號分組,然后每一組里面的流水號不能重復。

於是我又急急忙忙問了維護該項目的同事,按照以往慣例,一筆交易最多失敗多少次才能成功,同事說最多不會超過10-12次吧。那么也就是說同一個訂單號最多也就生成12個流水號,而由大小寫英文字母和阿拉伯數字組成的三位隨機字符串最多有238328種。但是再看前面同事的代碼可以看出,同事生成的隨機字符串組成方式是[ 大寫英文字母+小寫英文字母+阿拉伯數字 ],而按照這種固定的組合方式,最多只能生成26*26*10=6760種三位隨機字符串,顯然重復概率偏大。

然后再瞧一瞧上面的代碼,可以看出同事是使用與時間相關的默認種子值,初始化 Random 類的新實例。並且實例化了3個Random對象,很顯然3個Random對象極有可能會產生一模一樣的隨機數。默認情況下,Random 類的無參數構造函數使用系統時鍾生成其種子值,而參數化構造函數可根據當前時間的計時周期數采用Int32值。 但是,因為時鍾的分辨率有限,所以,如果使用無參數構造函數連續創建不同的 Random 對象,就會創建生成相同隨機數序列的隨機數生成器。

經過了這么一分析,顯然這種生成三位隨機字符串的方式存在極大的重復隱患。由於博主一貫主張在公司干活的首要目標是快速解決問題,於是博主決定先去網上找一找,看看有沒有比較通用靠譜的代碼。但是幾近波折,發現大多不如意,好吧,掄起袖子自己造輪子吧!

 

二、技術實現

1、Random 類是偽隨機數生成器,偽隨機數是以相同的概率從一組有限的數字中選取的。 所選數字並不具有完全的隨機性,因為它們是用一種確定的數學算法選擇的,但是從實用的角度而言,其隨機程度已足夠了。但是針對上述場景用起來總覺得隨機性偏弱,於是博主在MSDN有了新的發現,發現了一個可以生成強隨機數的類:RNGCryptoServiceProvider。該類可以使用加密服務提供程序 (CSP) 提供的實現來實現加密隨機數生成器 (RNG),顯然隨機性要大於Random類。

private int GetRandomInt(int maxValue)
{
    if (maxValue < 0)
    {
        throw new ArgumentOutOfRangeException("maxValue", "maxValue 小於零。");
    }
    S_rng.GetBytes(S_buffer);
    int value = BitConverter.ToInt32(S_buffer, 0);
    value = value % (maxValue + 1);
    if (value < 0) value = -value;
    return value;
}

 

2、同事既然把生成隨機字符串的方式固定為[ 大寫英文字母+小寫英文字母+阿拉伯數字 ],顯然這樣子不對,還可以有諸如[ 大寫英文字母+大寫英文字母+大寫英文字母 ]、[ 大寫英文字母+阿拉伯數字+大寫英文字母 ]等等其他組合方式的。也就是數學里面的組合,從n個不同元素中任意取出m個元素進行組合,允許組合內有重復元素,比如生成4位隨機字符串就是[ 大寫英文字母+小寫英文字母+阿拉伯數字+阿拉伯數字 ]。

為了便於支持多種數據類型的元素進行可重復組合,采用泛型。

private void GetAllCombination<T>(List<T[]> values, T[] array, T[] buffer, int index)
{
    if (index == 0)
    {
        foreach (T value in array)
        {
            buffer[0] = value;
            T[] tmp = new T[buffer.Length];
            buffer.CopyTo(tmp, 0);
            int l = tmp.Length;
            for (int i = 0; i < l / 2; i++)
            {
                T t = tmp[i];
                tmp[i] = tmp[l - i - 1];
                tmp[l - i - 1] = t;
            }
            values.Add(tmp);
        }
    }
    else
    {
        foreach (T value in array)
        {
            buffer[index] = value;
            GetAllCombination(values, array, buffer, index - 1);
        }
    }
}

private List<T[]> GetAllCombination<T>(T[] array, int m)
{
    List<T[]> values = new List<T[]>();
    T[] buffer = new T[m];
    GetAllCombination(values, array, buffer, m - 1);
    return values;
}

 

3、為了保證RandomString類的同一個實例對象生成的隨機字符串都是唯一的,博主特意在內部弄了一個容器,並且加鎖以支持多線程訪問。

/// <summary>
/// 生成一個由大小寫英文字母和數字組成的隨機字符串
/// </summary>
/// <returns></returns>
public string Next()
{
    while (true)
    {
        string indexType = CombinationType[GetRandomInt(CombinationType.Count - 1)].Item2;
        string randomString = string.Empty;
        foreach (var item in indexType)
        {
            randomString += GetRandomChar(item.ToString());
        }

        lock (this._lockObj)
        {
            if (!this._listRandomString.Contains(randomString))
            {
                this._listRandomString.Add(randomString);
                return randomString;
            }
        }
    }
}

 

4、擴展屬性

/// <summary>
/// 隨機字符串的位置組合信息
/// </summary>
public List<Tuple<int, string, int>> CombinationType { get; private set; }
/// <summary>
/// RandomString對象實例最多可以產生的隨機字符串
/// </summary>
public int Count { get; private set; }

 

 5、測試效果

 

 

 

 

 

 

 

三、結語思考

以上關鍵代碼均以貼出,博主也只是闡述自己的思考方式,借助此文拋磚引玉,希望得到大家指點。由於組合用的是遞歸算法,則必然導致性能低下,可否有大神還有其他方式進行優化?

通過性能測試可以看出,當m為13時開始出現瓶頸。

 


免責聲明!

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



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