2

在我的网站上,我允许人们批量购买我网站的订阅(我称它们为优惠券)。一旦他们拥有这些代金券,他们就会将其提供给任何人,然后将代码输入他们的帐户以进行升级。

现在我正在考虑做 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;
        }

    }
}
4

5 回答 5

1

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

我会创建一个vouchers带有 id、代码和 is_used 列的表。我会用足够的随机代码填充该表一次。由于这可以在单独的过程中完成,因此性能不会成为大问题。让它在晚上运行,第二天你会得到一个填满的凭证表。

如果您想防止生成重复的凭证,那将不是问题。无论如何,您都可以生成它们并将它们放在 System.Collections.Generic.HashSet 中(这可以防止添加重复项而不引发异常)或调用 Linq 方法 Distinct(),然后再将它们添加到该vouchers表中。

于 2012-07-25T12:12:59.397 回答
1

如果您坚持使用代码:

使用 GUID 作为主键,生成一个随机数。您可能希望如何将其转换为 alpha-num 取决于您。

使用 guid 和随机数的最后一个或两个字节。1234-684687 这应该会使暴力破解优惠券变得不太容易。并处理任何(罕见的)碰撞异常。

缩短 int 的简单方法,更改它的基数(从 10 到 62)。"2lkCB1"(在 VB中,这是旧代码)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

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

于 2012-07-25T18:15:39.917 回答
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
);

并发

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

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

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% 的冲突意味着它花费更多时间测试已使用的代码,而不是实际生成新代码。

保持低碰撞阈值是您控制代码难以猜测的方式。设置_collisionThreshold0.01将生成代码,这样有人猜测代码的可能性约为 1%。

如果冲突发生得太频繁,CodeLength(该GenerateRandomCode()方法使用的)将被递增。这个值需要保存在某个地方。执行后GenerateCodes(),检查CodeLength是否有变化,然后保存新值。

源代码

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

于 2012-07-26T01:05:46.657 回答
0

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

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

于 2012-07-19T16:53:58.547 回答
0

使用 nHibernate 的HiLo算法怎么样?
是一个关于如何获得下一个值的示例(没有数据库访问权限)。

于 2012-07-19T19:00:32.023 回答