.NET短唯一标识符

133

我需要在.NET中使用一个唯一标识符(由于太长,无法使用GUID)。

人们认为这里使用的算法是一个好的选择,还是您有其他建议?


8
有多短?有多独一无二?如果基于以太网适配器的硬件地址,GUID就能保证是独一无二的;纯数学计算生成的任何东西都不能被证明是独一无二的——只是极有可能独一无二(概率极高)。 - Jon
以下是有关编程的内容,请将其从英语翻译成中文。仅返回翻译后的文本:长度为15,并尽可能独特。 - Noel
6
长度是15个什么?15个字节吗?如果是的话,为什么不从GUID中去掉一个字节呢? - KristoferA
如果您不需要超过63位密钥,可以使用长整型获得相同的结果 https://github.com/joshclark/Flakey - Chris Marisic
4
在GUID中删除一个字节会极大地增加密钥冲突的可能性。如果您删除了错误顺序的字节,那么它可能会使密钥冲突变成必然发生的情况。 - Chris Marisic
请在此处查看我的答案:https://dev59.com/w3M_5IYBdhLWcg3wQQpd#56291295 - Vinod Srivastav
22个回答

3
如果您的应用程序没有数百万人在同一毫秒内使用该生成短唯一字符串的功能,您可以考虑使用以下函数。
private static readonly Object obj = new Object();
private static readonly Random random = new Random();
private string CreateShortUniqueString()
{
    string strDate = DateTime.Now.ToString("yyyyMMddhhmmssfff");
    string randomString ;
    lock (obj)
    {
        randomString = RandomString(3);
    }
    return strDate + randomString; // 16 charater
}
private string RandomString(int length)
{

    const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxy";
    var random = new Random();
    return new string(Enumerable.Repeat(chars, length)
      .Select(s => s[random.Next(s.Length)]).ToArray());
}

如果您只需要在未来的99年内使用应用程序,将yyyy更改为yy。
更新20160511:更正随机函数
- 添加锁定对象
- 将随机变量从RandomString函数中移出
参考资料


3
这很好,尽管你不应该每次都初始化一个新的Random - 加锁的原因是允许您重复使用同一个Random实例。我认为你忘记删除那行代码了! - NibblyPig

3

这是我的解决方案,它不安全且不支持并发,每秒最多只能生成1000个GUID,并且支持线程安全。

public static class Extensors
{

    private static object _lockGuidObject;

    public static string GetGuid()
    {

        if (_lockGuidObject == null)
            _lockGuidObject = new object();


        lock (_lockGuidObject)
        {

            Thread.Sleep(1);
            var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            var epochLong = Convert.ToInt64((DateTime.UtcNow - epoch).TotalMilliseconds);

            return epochLong.DecimalToArbitrarySystem(36);

        }

    }

    /// <summary>
    /// Converts the given decimal number to the numeral system with the
    /// specified radix (in the range [2, 36]).
    /// </summary>
    /// <param name="decimalNumber">The number to convert.</param>
    /// <param name="radix">The radix of the destination numeral system (in the range [2, 36]).</param>
    /// <returns></returns>
    public static string DecimalToArbitrarySystem(this long decimalNumber, int radix)
    {
        const int BitsInLong = 64;
        const string Digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";

        if (radix < 2 || radix > Digits.Length)
            throw new ArgumentException("The radix must be >= 2 and <= " + Digits.Length.ToString());

        if (decimalNumber == 0)
            return "0";

        int index = BitsInLong - 1;
        long currentNumber = Math.Abs(decimalNumber);
        char[] charArray = new char[BitsInLong];

        while (currentNumber != 0)
        {
            int remainder = (int)(currentNumber % radix);
            charArray[index--] = Digits[remainder];
            currentNumber = currentNumber / radix;
        }

        string result = new String(charArray, index + 1, BitsInLong - index - 1);
        if (decimalNumber < 0)
        {
            result = "-" + result;
        }

        return result;
    }

此代码没有经过优化,仅为范例!


虽然这是一个有趣的解决方案,但无法保证每毫秒UtcNow都会返回唯一的滴答值:根据备注,其分辨率取决于系统计时器。另外,最好确保系统时钟不会向后更改!(由于用户13971889的答案将此问题推到了我的提问列表顶部,我对该答案进行了评论,因此我认为我应该在这里重复那个评论。) - Joe Sewell

3
    public static string ToTinyUuid(this Guid guid)
    {
        return Convert.ToBase64String(guid.ToByteArray())[0..^2]  // remove trailing == padding 
            .Replace('+', '-')                          // escape (for filepath)
            .Replace('/', '_');                         // escape (for filepath)
    }

用法

Guid.NewGuid().ToTinyUuid()

将其转换回来并不是什么高深学问,所以我会留给你这个任务。


3

这是我生成随机且短小唯一ID的方法。使用加密随机数生成器进行安全的随机数生成。在chars字符串中添加所需的任何字符。

using System;
using System.Security.Cryptography;

// ...

private string GenerateRandomId(int length)
{
    string charset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    char[] outputChars = new char[length];
    
    using RandomNumberGenerator rng = RandomNumberGenerator.Create();
    int minIndex = 0;
    int maxIndexExclusive = charset.Length;
    int diff = maxIndexExclusive - minIndex;

    long upperBound = uint.MaxValue / diff * diff;

    byte[] randomBuffer = new byte[sizeof(int)];

    for (int i = 0; i < outputChars.Length; i++)
    {
        // Generate a fair, random number between minIndex and maxIndex
        uint randomUInt;
        do
        {
            rng.GetBytes(randomBuffer);
            randomUInt = BitConverter.ToUInt32(randomBuffer, 0);
        }
        while (randomUInt >= upperBound);
        int charIndex = (int)(randomUInt % diff);

        // Set output character based on random index
        outputChars[i] = charset[charIndex];
    }

    return new string(outputChars);
}

这是通过将随机整数缩小到字符集索引范围内来实现的,并考虑了随机数是绝对上限的特殊情况,因此重新投掷新的整数。
该解决方案产生公正和均匀分布的输出,测试了1,000,000个字符长度的输出,没有明显的偏差:
string output = GenerateRandomId(1_000_000);
var tally = output.GroupBy(c => c).OrderBy(g => g.Key).Select(g => (g.Key, g.Count())).ToArray();

int average = (int)(tally.Aggregate(new BigInteger(0), (b, t) => {b += t.Item2; return b;}, b => b) / tally.Count());
int max = tally.Max(g => g.Item2);
int min = tally.Min(g => g.Item2);

Console.WriteLine($"Avg: {average}");
Console.WriteLine($"Max: {max}");
Console.WriteLine($"Min: {min}");


foreach((char key, int count) in tally) {
    Console.WriteLine($"{key}: {count}");
}

输出:

Avg: 27777
Max: 28163
Min: 27341
0: 28081
1: 27773
...
Z: 27725

我发现这对于小样本非常有用,但我很好奇它是如何创建唯一值的。我测试了一下,发现它们并不是唯一的。例如,当长度为5时,每10万个字符串中大约有100个重复。但是当长度为7时,重复的字符串很少,所以这必须是概率问题。我找不到一个可以生成唯一字符串而无需进行重复检查的函数,但我很好奇是否存在这样的函数。 - pghcpa
@pghcpa 这是可能的,您需要一个1:1转换函数,它接受整数输入并产生唯一的整数输出。也就是说,在整数范围内的每个输入都会产生单个唯一的输出,但使输出看起来是均匀随机的,并且具有线性增加的输入。然后,将大输出整数转换为基数36,这成为随机字符串。缺点是很容易推导出使用的转换函数,然后轻松预测未来的rng值,因此不太安全。 - Ryan

2

我知道这篇文章的发布日期已经很久了... :)

我有一个生成器,它只能产生9个十六进制字符,例如:C9D6F7FF3,C9D6FB52C。

public class SlimHexIdGenerator : IIdGenerator
{
    private readonly DateTime _baseDate = new DateTime(2016, 1, 1);
    private readonly IDictionary<long, IList<long>> _cache = new Dictionary<long, IList<long>>();

    public string NewId()
    {
        var now = DateTime.Now.ToString("HHmmssfff");
        var daysDiff = (DateTime.Today - _baseDate).Days;
        var current = long.Parse(string.Format("{0}{1}", daysDiff, now));
        return IdGeneratorHelper.NewId(_cache, current);
    }
}


static class IdGeneratorHelper
{
    public static string NewId(IDictionary<long, IList<long>> cache, long current)
    {
        if (cache.Any() && cache.Keys.Max() < current)
        {
            cache.Clear();
        }

        if (!cache.Any())
        {
            cache.Add(current, new List<long>());
        }

        string secondPart;
        if (cache[current].Any())
        {
            var maxValue = cache[current].Max();
            cache[current].Add(maxValue + 1);
            secondPart = maxValue.ToString(CultureInfo.InvariantCulture);
        }
        else
        {
            cache[current].Add(0);
            secondPart = string.Empty;
        }

        var nextValueFormatted = string.Format("{0}{1}", current, secondPart);
        return UInt64.Parse(nextValueFormatted).ToString("X");
    }
}

1
基于@dorcohen的回答和@pootzko的评论。你可以使用这个。它在传输过程中是安全的。
var errorId = System.Web.HttpServerUtility.UrlTokenEncode(Guid.NewGuid().ToByteArray());

如果有人想知道结果:Jzhw2oVozkSNa2IkyK4ilA2,或者您可以在 https://dotnetfiddle.net/VIrZ8j 上自行尝试。 - chriszo111
很不幸,这个功能与最近的dotnet不兼容:https://dev59.com/bVUL5IYBdhLWcg3wK1cU - manuc66

1
在C#中,一个long值有64位,如果使用Base64编码,则会有12个字符,包括1个填充字符=。如果我们去掉填充字符=,则会有11个字符。
一个疯狂的想法是,我们可以使用Unix纪元和一个计数器来形成一个long值。在C#中,Unix纪元DateTimeOffset.ToUnixEpochMillisecondslong格式表示,但是8字节中的前2个字节始终为0,否则日期时间值将大于最大日期时间值。因此,这给了我们2个字节来放置一个ushort计数器。
因此,总的来说,只要ID生成的数量不超过每毫秒65536个,我们就可以拥有一个唯一的ID:
// This is the counter for current epoch. Counter should reset in next millisecond
ushort currentCounter = 123;

var epoch = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
// Because epoch is 64bit long, so we should have 8 bytes
var epochBytes = BitConverter.GetBytes(epoch);
if (BitConverter.IsLittleEndian)
{
    // Use big endian
    epochBytes = epochBytes.Reverse().ToArray();
}

// The first two bytes are always 0, because if not, the DateTime.UtcNow is greater 
// than DateTime.Max, which is not possible
var counterBytes = BitConverter.GetBytes(currentCounter);
if (BitConverter.IsLittleEndian)
{
    // Use big endian
    counterBytes = counterBytes.Reverse().ToArray();
}

// Copy counter bytes to the first 2 bytes of the epoch bytes
Array.Copy(counterBytes, 0, epochBytes, 0, 2);

// Encode the byte array and trim padding '='
// e.g. AAsBcTCCVlg
var shortUid = Convert.ToBase64String(epochBytes).TrimEnd('=');

请不要使用时间来保证唯一性。我已经两次被在多处理器系统中使用此方法的人所坑过。 - Jay

0

如果您不需要输入字符串,可以使用以下方法:

static class GuidConverter
{
    public static string GuidToString(Guid g)
    {
        var bytes = g.ToByteArray();
        var sb = new StringBuilder();
        for (var j = 0; j < bytes.Length; j++)
        {
            var c = BitConverter.ToChar(bytes, j);
            sb.Append(c);
            j++;
        }
        return sb.ToString();
    }

    public static Guid StringToGuid(string s) 
        => new Guid(s.SelectMany(BitConverter.GetBytes).ToArray());
}

这将把Guid转换为8个字符的字符串,如下所示:

{b77a49a5-182b-42fa-83a9-824ebd6ab58d} --> "䦥띺ᠫ䋺ꦃ亂檽趵"

{c5f8f7f5-8a7c-4511-b667-8ad36b446617} --> "엸詼䔑架펊䑫ᝦ"


0

为了不丢失字符(+ / -),如果您想在 URL 中使用您的 GUID,则必须将其转换为 base32

对于 10,000,000,没有重复的键

    public static List<string> guids = new List<string>();
    static void Main(string[] args)
    {
        for (int i = 0; i < 10000000; i++)
        {
            var guid = Guid.NewGuid();
            string encoded = BytesToBase32(guid.ToByteArray());
            guids.Add(encoded);
            Console.Write(".");
        }
        var result = guids.GroupBy(x => x)
                    .Where(group => group.Count() > 1)
                    .Select(group => group.Key);

        foreach (var res in result)
            Console.WriteLine($"Duplicate {res}");

        Console.WriteLine($"*********** end **************");
        Console.ReadLine();
    }

    public static string BytesToBase32(byte[] bytes)
    {
        const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        string output = "";
        for (int bitIndex = 0; bitIndex < bytes.Length * 8; bitIndex += 5)
        {
            int dualbyte = bytes[bitIndex / 8] << 8;
            if (bitIndex / 8 + 1 < bytes.Length)
                dualbyte |= bytes[bitIndex / 8 + 1];
            dualbyte = 0x1f & (dualbyte >> (16 - bitIndex % 8 - 5));
            output += alphabet[dualbyte];
        }

        return output;
    }

-2
private static readonly object _getUniqueIdLock = new object();
public static string GetUniqueId()
{       
    lock(_getUniqueIdLock)
    {
        System.Threading.Thread.Sleep(1);
        return DateTime.UtcNow.Ticks.ToString("X");
    }
}

1
尽管这是一个有趣的解决方案,但不能保证 UtcNow 每毫秒都返回唯一的滴答值:根据 备注,其分辨率取决于系统计时器。此外,您最好确保系统时钟不会向后更改!(Ur3an0 的答案也存在这些问题。) - Joe Sewell
同意。这是一种贫穷的方法,不应该在你自己的良好控制的环境之外使用。 - user13971889

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接