这个UUID生成代码存在问题吗?

4

我有一些代码需要使用UUID作为数据库ID。出于简单起见,我选择了v4(随机数),我没有看到使用任何其他不太随机的UUID版本的真正原因。我的UUID类大致定义如下(简化):

class uuid {
public:
    static uuid create_v4();
public:
    // cut out for simplification...
public:
    uint8_t bytes[16];
};

实际的生成代码如下:

namespace {

uint32_t rand32() {
    // we need to do this, because there is no
    // gaurantee that RAND_MAX is >= 0xffffffff
    // in fact, it is LIKELY to be 0x7fffffff
    const uint32_t r1 = rand() & 0x0ff;
    const uint32_t r2 = rand() & 0xfff;
    const uint32_t r3 = rand() & 0xfff;
    return (r3 << 20) | (r2 << 8) | r1;

}

}

uuid uuid::create_v4() {

    static const uint16_t c[] = {
        0x8000,
        0x9000,
        0xa000,
        0xb000,
    };

    uuid uuid;

    const uint32_t rand_1 = (rand32() & 0xffffffff);
    const uint32_t rand_2 = (rand32() & 0xffff0fff) | 0x4000;
    const uint32_t rand_3 = (rand32() & 0xffff0fff) | c[rand() & 0x03];
    const uint32_t rand_4 = (rand32() & 0xffffffff);

    uuid.bytes[0x00] = (rand_1 >> 24) & 0xff;
    uuid.bytes[0x01] = (rand_1 >> 16) & 0xff;
    uuid.bytes[0x02] = (rand_1 >> 8 ) & 0xff;
    uuid.bytes[0x03] = (rand_1      ) & 0xff;

    uuid.bytes[0x04] = (rand_2 >> 24) & 0xff;
    uuid.bytes[0x05] = (rand_2 >> 16) & 0xff;
    uuid.bytes[0x06] = (rand_2 >> 8 ) & 0xff;
    uuid.bytes[0x07] = (rand_2      ) & 0xff;

    uuid.bytes[0x08] = (rand_3 >> 24) & 0xff;
    uuid.bytes[0x09] = (rand_3 >> 16) & 0xff;
    uuid.bytes[0x0a] = (rand_3 >> 8 ) & 0xff;
    uuid.bytes[0x0b] = (rand_3      ) & 0xff;

    uuid.bytes[0x0c] = (rand_4 >> 24) & 0xff;
    uuid.bytes[0x0d] = (rand_4 >> 16) & 0xff;
    uuid.bytes[0x0e] = (rand_4 >> 8 ) & 0xff;
    uuid.bytes[0x0f] = (rand_4      ) & 0xff;

    return uuid;
}

我认为这看起来是正确的,但是最近从数据库中收到错误消息,说我试图插入的 UUID 是重复的。由于这应该是极不可能的,我必须假设我的代码可能存在问题。所以有人发现了什么问题吗?我的随机 UUID 生成器不够随机吗?

注意:我无法使用 boost 的随机数生成或其 UUID 库。我希望我可以,但我被绑定在一个特定的系统上,安装了特定版本的库,并且获取足够新的 boost 版本以具备这些功能基本上是不可能的。


1
你如何为 rand() 进行种子初始化? - Keith Randall
5
对于这种情况,我认为使用普通的 rand() 函数并不是一个好主意,特别是由 LCG 生成的数字的低位比较不随机(它们通常具有更短的周期)。你至少应该考虑一些高位,并且如果我是你,我会选择包括时间戳的算法版本,这样你产生重复数字的可能性就会小很多。 - Matteo Italia
3
你能让数据库自己生成吗?这是最安全的选择。 - pstrjds
1
@Matteo:就我所知,GNU libc 实现的 rand() 采取措施避免 LCG 具有较低的随机性,尽管我可能是错的。 - Evan Teran
1
使用 time() 进行种子初始化的分辨率为 1 秒。你是否可能在同一秒内多次运行种子代码? - Keith Randall
显示剩余12条评论
1个回答

3

我认为代码是合理的。正如评论中提到的,使用rand()是否是一个好的选择还存在一些问题,但是你对它的使用似乎是产生32位数据的合理方式,假设使用了较新版本的库,保证低位和高位一样随机(也是由你在评论中提到的)。

因此,只要rand()函数表现得足够好,很不可能会出现重复的情况。所以我的猜测是有不同类型的错误发生了。以下是一些可能性:

  1. time(0)失败。这似乎非常不可能。如果它在两个不同的运行中都返回-1来表示错误,那么就可能导致问题。然而,它唯一可能失败的方式是如果给它一个无效的地址(这绝对不是这种情况)。
  2. 多线程使用。我不认为rand()是线程安全的。如果该代码在多线程环境中使用,可能会导致意外的行为。
  3. Cron引起困难。如果工作站上的时钟不准确,并且通过某个服务器自动设置(例如通过rdate),则可能会导致cron工作在某个时间重复执行。我能够模拟这种行为,只需创建一个cron工作,每分钟将当前日期转储到文件中,然后重复设置日期...它最终会将相同的日期/时间(到秒)多次写入文件。由于时间函数的一秒分辨率,这很容易导致重复的种子。
  4. 编写UUID到数据库的代码不正确。即使UUID生成器完美运行,也可能存在不同的错误将相同的UUID两次写入数据库中。

以上只是猜测。其中,第三个是我最喜欢的,但如果我正在审查自己的代码,我会首先怀疑第四个。


第三点非常有趣,我一定要深入研究一下。我已经更新了代码,使用/dev/urandom作为种子(如果可以成功读取),如果失败则退回到time(0)。目前看起来效果不错。 - Evan Teran
1
@Evan:上周五下午,当我等待一些代码扫描完成(它们正在耗尽我的生命和灵魂)时,我在Windows和Linux中测试了你的UUID生成程序。我只是生成了1000万个UUID,将它们转储到文件中,排序,然后通过awk或ruby(现在记不清了)查找重复项,并没有产生重复项。虽然这个测试并不能真正证明什么,但我未能发现你的代码逻辑有任何问题。 - Mark Wilkins
感谢您对此的关注。代码已经运行了几周,我只遇到了一次重复键错误。所以这可能只是真的很倒霉吧。 - Evan Teran
我接受这个答案,因为我认为问题出在第三点。我修改了代码,使用/dev/random来检查,如果失败则只使用time(0)。自那以后就没有发生过冲突了。再次感谢。 - Evan Teran

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