记一次随机字符串生成算法的随机概率与性能的提升

一、前言背景

前几天我部门一个和银行对接的项目中出现了业务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时开始出现瓶颈。

 

posted @ 2015-03-31 08:22  陌城&心灵捕手  阅读(3795)  评论(21编辑  收藏  举报