一、前言背景
前幾天我部門一個和銀行對接的項目中出現了業務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時開始出現瓶頸。