使用Nhibernate检查使用过的密钥的最佳方法是什么?

时间:2012-07-19 16:38:45

标签: c# .net asp.net-mvc nhibernate

在我的网站上我允许人们批量购买我网站的订阅(我称之为优惠券)。一旦他们拥有这些优惠券,他们就会将这些优惠券交给他们,并将他们的代码输入他们的账户进行升级。

现在我正在考虑做4个字母数字代码(大写,小写和数字)并且会有类似的东西

var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var stringChars = new char[4];
var random = new Random();

for (int i = 0; i < stringChars.Length; i++)
{
    stringChars[i] = chars[random.Next(chars.Length)];
}

var finalString = new String(stringChars);

现在我认为这会给我足够多的组合,如果我用完了,我总是能够超越代码的长度。我想保持简短,因为我不希望用户输入数字很大。

我也没有时间制作更优雅的解决方案,也许他们点击了电子邮件中的链接或其他内容并激活了他们的帐户,当然这会减少试图随机猜出凭证编号的人。

如果网站每个都变得更受欢迎,我会处理这些事情。

我想知道如何处理同一凭证的可能重复生成。我的第一个想法是每次创建凭证时都要检查数据库,如果存在凭证,则创建一个新凭证。

然而,这似乎可能很慢。所以我想也许首先得到所有密钥并将它们存储在内存中并检查那里但是如果列表不断增长,我可能会遇到内存异常以及所有那些很棒的东西。

所有人都有任何想法吗?还是我坚持做上面列出的两种方法之一?

我正在使用nhibernate,asp.net mvc和C#。

修改

 static void Main(string[] args)
        {
            List<string> hold = new List<string>();
            for (int i = 0; i < 10000; i++)
            {
                HashAlgorithm sha = new SHA1CryptoServiceProvider();
                byte[] result = sha.ComputeHash(BitConverter.GetBytes(i));
                string hex = null;

                foreach (byte x in result)
                {
                    hex += String.Format("{0:x2}", x);
                }

                hold.Add(hex.Substring(0,3));

                Console.WriteLine(hex.Substring(0, 4));
            }


             Console.WriteLine("Number of Distinct values {0}", hold.Distinct().Count());
        }

以上是我尝试使用散列的尝试。但是我认为我错过了一些东西,因为它似乎有更多的重复,然后预期。

修改2

我想我添加了我所遗漏的内容,但不确定这是否正是他的意思。我也不确定在我移动它的情况下该怎么做(我似乎给了我40个地方的长度,我可以移动它)。

  static void Main(string[] args)
        {
            int subStringLength = 4;
            List<string> hold = new List<string>();
            for (int i = 0; i < 10000; i++)
            {
                SHA1CryptoServiceProvider sha = new SHA1CryptoServiceProvider();
                byte[] result = sha.ComputeHash(BitConverter.GetBytes(i));
                string hex = null;

                foreach (byte x in result)
                {
                    hex += String.Format("{0:x2}", x);
                }

                int startingPositon = 0;
                string possibleVoucherCode = hex.Substring(startingPositon,subStringLength);

                string voucherCode = Move(subStringLength, hold, hex, startingPositon, possibleVoucherCode);
                hold.Add(voucherCode);
            }


             Console.WriteLine("Number of Distinct values {0}", hold.Distinct().Count());
        }

    private static string Move(int subStringLength, List<string> hold, string hex, int startingPositon, string possibleVoucherCode)
    {
        if (hold.Contains(possibleVoucherCode))
        {
            int newPosition = startingPositon + 1;
            if (newPosition <= hex.Length)
            {
                if ((newPosition + subStringLength) > hex.Length)
                {
                    possibleVoucherCode = hex.Substring(newPosition, subStringLength);
                    return Move(subStringLength, hold, hex, newPosition, possibleVoucherCode);
                }
                // return something
                return "0";
            }
            else
            {
                // return something
                return "0";
            }
        }
        else
        {
           return possibleVoucherCode;
        }

    }
}

5 个答案:

答案 0 :(得分:1)

它会很慢,因为您想要随机生成凭证,然后检查数据库中每个生成的代码。

我会创建一个包含id,代码和is_used列的表vouchers。我会用足够的随机代码填充该表一次。由于这可以在单独的过程中完成,因此性能不会是一个大问题。让它在晚上运行,第二天你会得到一张完全填好的代金券桌。

如果您想防止生成重复的凭证,那不会有问题。无论如何都可以生成它们并将它们放在System.Collections.Generic.HashSet中(这可以防止在不抛出异常的情况下添加重复项)或者在将它们添加到vouchers表之前调用Linq方法Distinct()。

答案 1 :(得分:1)

如果您坚持简短代码:

使用GUID作为主键,生成一个随机数。你可能想把它翻译成alpha-num取决于你。

使用guid和随机数的最后一个或两个字节。 1234-684687这应该会使稍微不那么容易使用优惠券。并处理任何(罕见)碰撞和异常。

缩短int的简单方法,改变它的基础(从10到62)。 (在VB中,这是旧代码) 在给定"2lkCB1"

时,这会产生Int32.MaxValue
''//given intValue as your random integer
Dim result As String = String.Empty
Dim digits as String = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
Dim x As Integer
While (intValue > 0)
   x = intValue Mod digits.Length
   result = digits(x) & result 
   intValue = intValue - x
   intValue = intValue \ digits.Length
End While
Return result

但现在我们已经回答了不止一个问题。

答案 2 :(得分:1)

对于像这样的批量数据操作,我建议不要使用NHibernate而只是直接使用ADO.NET。

批量检查

由于您预计会同时生成大批代码,因此您应该将多个代码检查批量处理到数据库的单个往返中。如果您使用的是SQL Server 2008或更高版本,则可以使用表值参数,一次检查整个代码列表。

SELECT DISTINCT b.Code
FROM @batch b
WHERE NOT EXISTS (
    SELECT v.Code
    FROM dbo.Voucher v
    WHERE v.Code = b.Code
);

<强>并发

现在,并发问题呢?如果两个用户几乎同时生成相同的代码怎么办?或者只是在我们检查代码的唯一性和我们将其插入凭证表的时间之间?

我们可以通过修改查询来解决这个问题:

DECLARE @batchid uniqueidentifier;
SET @batchid = NEWID();

INSERT INTO dbo.Voucher (Code, BatchId)
SELECT DISTINCT b.Code, @batchid
FROM @batch b
WHERE NOT EXISTS (
    SELECT Code
    FROM dbo.Voucher v
    WHERE b.Code = v.Code
);

SELECT Code
FROM dbo.Voucher
WHERE BatchId = @batchid;

通过.NET执行

假设您已定义以下表值用户类型...

CREATE TYPE dbo.VoucherCodeList AS TABLE (
    Code nvarchar(8) COLLATE SQL_Latin1_General_CP1_CS_AS NOT NULL
    /* !!! Remember to specify the collation on your Voucher.Code column too, since you want upper and lower-case codes. */
);

...您可以通过.NET代码执行此查询,如下所示:

public ICollection<string> GenerateCodes(int numberOfCodes)
{
    var result = new List<string>(numberOfCodes);

    while (result.Count < numberOfCodes)
    {
        var batchSize = Math.Min(_batchSize, numberOfCodes - result.Count);
        var batch = Enumerable.Range(0, batchSize)
            .Select(x => GenerateRandomCode());
        var oldResultCount = result.Count;

        result.AddRange(FilterAndSecureBatch(batch));

        var filteredBatchSize = result.Count - oldResultCount;
        var collisionRatio = ((double)batchSize - filteredBatchSize) / batchSize;

        // Automatically increment length of random codes if collisions begin happening too frequently
        if (collisionRatio > _collisionThreshold)
            CodeLength++;
    }

    return result;
}

private IEnumerable<string> FilterAndSecureBatch(IEnumerable<string> batch)
{
    using (var command = _connection.CreateCommand())
    {
        command.CommandText = _sqlQuery; // the concurrency-safe query listed above

        var metaData = new[] { new SqlMetaData("Code", SqlDbType.NVarChar, 8) };
        var param = command.Parameters.Add("@batch", SqlDbType.Structured);
        param.TypeName = "dbo.VoucherCodeList";
        param.Value = batch.Select(x =>
        {
            var record = new SqlDataRecord(metaData);
            record.SetString(0, x);
            return record;
        });

        using (var reader = command.ExecuteReader())
            while (reader.Read())
                yield return reader.GetString(0);
    }
}

<强>性能

在实现所有这些之后(并将命令和参数创建移出循环,以便在批次之间重复使用),我能够使用批量大小500一致地插入10,000个代码。 0.5至2秒,或每毫秒5至20个代码。

代码密度/碰撞/可猜测性

_collisionThreshold字段限制了代码的密度。它是介于0和1之间的值。实际上,必须小于1,否则当4位数代码用完时你会陷入无限循环(可能应该在代码中为此添加一个断言) )。出于性能原因,我建议永远不要将其置于0.5之上。超过50%的冲突意味着它花费更多时间测试已经使用过的代码,而不是实际生成新代码。

保持较低的碰撞阈值是您控制代码难以猜测的方式。将_collisionThreshold设置为0.01会生成代码,以致有人猜测代码的几率为1%。

如果碰撞过于频繁,CodeLength(由GenerateRandomCode()方法使用)将会递增。这个值需要在某个地方保留。执行GenerateCodes()后,请检查CodeLength以查看其是否已更改,然后保存新值。

源代码

完整代码可在此处获取:https://gist.github.com/3217856。我是此代码的作者,并在MIT license下发布。我很开心这个小小的挑战,还学习了如何将表值参数传递给内联参数化查询。我以前从未这样做过。我只是将它们传递给了成熟的存储过程。

答案 3 :(得分:0)

可能的解决方案是这样的:
查找凭证的最大ID(整数)。然后,对其运行任何散列函数,取前32位并转换为要向用户显示的字符串(或使用32位散列函数,如Jenkins hash function)。这可能会起作用,哈希冲突非常罕见。但这种解决方案与你的解决方案非常相似,在随机性方面。

您可以运行发现前10或100次碰撞的测试(这应该足够了)并强制算法“跳过”它们并使用不同的起始值。然后,您根本不需要检查数据库(好吧,至少在您达到约4294967296凭证之前......)

答案 4 :(得分:0)

如何使用nHibernate的HiLo算法? Here是关于如何获取下一个值(没有DB访问权限)的示例。