如何使用UUID生成唯一的正长整型数

50
我需要为数据库主键列生成唯一的长整型id。
我想可以使用 UUID.randomUUID().getMostSignificantBits(),但有时会生成一些负数的长整型,这对我来说是个问题。
是否可能从UUID中仅生成正数的长整型?由于将有数十亿条目,因此我希望每个生成的密钥都必须是唯一的。

为什么你不使用序列?你能够使用这样的东西吗?或者UUID,这是你必须使用的解决方案吗? - Michał Ziober
你能否更详细地解释一下序列? - Saurabh Kumar
你使用哪个数据库?你使用哪个数据库框架(JDBC、Hibernate、myBatis)? - Taky
请阅读关于PostgreSQL序列的内容:http://www.neilconway.org/docs/sequences/。这只是一个例子。在您的数据库中,您应该会找到类似的东西。 - Michał Ziober
我正在使用mysql。我想在应用程序端完成它,因为如果我在数据库端完成它,我必须再次触发一个查询来获取行的ID..而我想避免这种情况。 - Saurabh Kumar
显示剩余4条评论
8个回答

65
UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE
这能起作用的原因是,当你使用&运算符与1进行操作时,它允许相同的数字通过,而当你使用&运算符与0进行操作时,它将其阻止并且结果为0。现在,Long.MAX_Value在二进制中表示为:
0111111111111111111111111111111111111111111111111111111111111111 

这是一个0后面跟着63个1的二进制数(总共64位,在Java中被称为长整型)

当您将一个数字X与上述数字进行按位&运算时,您将得到相同的数字X,只不过最左边的位现在变成了零。这意味着您只更改了该数字的符号而没有更改其值。


3
有没有办法从上述逻辑生成的长字符串中获取UUID(或可能的UUID)? - Francesco
12
碰撞发生的可能性有多大? - user1870400
1
这意味着结果总是为正,正确吗? - frostman
yaa!结果将始终为正数! - atish shimpi
1
如果在 JavaScript 中使用此随机值,则其不起作用。它对于 JS 来说太长了,会四舍五入该值。此问题在这里有描述:https://dev59.com/_nM_5IYBdhLWcg3wcCnc - Ashim

12

就像其他人所写的一样,long类型的空间不足以存储唯一的数字。但在许多情况下,一个数字可能已经足够特定用途的唯一性要求。 例如,具有纳秒精度的时间戳通常已经足够好了。 要获得它,请将当前毫秒数左移20位以为纳秒分配空间,然后将其覆盖在纳秒上:

(System.currentTimeMillis() << 20) | (System.nanoTime() & ~9223372036854251520L);

nano & ~9223372036854251520L部分获取当前纳秒数并将前44个字节设置为0,只保留右侧20位,表示多达一毫秒的纳秒数(999999纳秒)。

它与以下内容相同:

nanoseconds & ~1111111111111111111111111111111111111111111100000000000000000000

旁注:毫微秒不应用于表示当前时间,因为它们的起始点在时间上不固定,并且当它们达到最大值时会被重置。

你可以使用任何其他位操作。通常最好考虑当前时间和其他一些信息,例如当前线程ID、进程ID、IP地址等。


nano-time不应该与9223372036853727232L相与吗?数字9223372036854251520L只会取nano-time的最右19位。 - wcmatthysen
2
发生冲突的可能性有多大? - user1870400
2
为什么不使用 0xFFFFFL 而是 ~9223372036854251520L(System.currentTimeMillis() << 20) | (System.nanoTime() & 0xFFFFFL); - codeisee
你可以轻松地在1毫秒内创建一千个对象。 - ggb667
你可以轻松地在1毫秒内创建一千个对象。 - ggb667

8

LongGenerator会创建一个顺序ID,这意味着它可能会发生冲突。而GUID是普遍唯一的。它可能足以满足Saurabh的要求,但我认为这不是完全正确的答案。 - Sap
Apache commons-id无法下载?还有其他替代品吗? - JavaTechnical

5
这段代码受@Daniel Nuriyev答案的启发。但是,与其使用纳秒时间,当在同一毫秒内发生冲突时,会使用计数器(或我所见过的称为鉴别器):
private static long previousTimeMillis = System.currentTimeMillis();
private static long counter = 0L;

public static synchronized long nextID() {
    long currentTimeMillis = System.currentTimeMillis();
    counter = (currentTimeMillis == previousTimeMillis) ? (counter + 1L) & 1048575L : 0L;
    previousTimeMillis = currentTimeMillis;
    long timeComponent = (currentTimeMillis & 8796093022207L) << 20;
    return timeComponent | counter;
}

此方法通过将毫秒时间戳组件与计数器组件打包在一起生成半唯一ID。该算法允许在发生冲突之前在同一毫秒内生成大约100万个(确切地说是1048575)唯一ID。直到2248年,唯一ID都会被生成,此时它将绕回并从0开始。
ID生成的过程如下: 自纪元以来的毫秒数: | 0 | 000000000000000000000010110111101111100110001001111100101011111 |
与(8796093022207L)按位与: | 0 | 000000000000000000001111111111111111111111111111111111111111111 |
以给出43个最不重要的位作为时间组件。
然后将其向左移动20位,以获得: | 0 | 0010110111101111100110001001111100101011111 | 00000000000000000000 |
按位或计数器的20位(例如,如果计数器为3),则可得: | 0 | 0010110111101111100110001001111100101011111 | 00000000000000000101 |
只使用43位(而不是44位)作为时间组件,因为我们不希望更改最高有效位(即数字的符号)。这导致仅生成正数ID。

1
它在 Kotlin 中运行得非常好。非常感谢!真是一件艺术品。如果您使用 AtomicLong 存储结果,它将变得线程安全吗? - Beezer
@Beezer,nextID方法由于标记了synchronized关键字而是线程安全的(因此不需要额外的工作来使其线程安全)。将方法本身同步比使用AtomicLong更简单。如果您对previousTimeMilliscounter变量都使用AtomicLong,则仍然需要一个锁定对象来确保这两个变量被原子更新。 - wcmatthysen

3

我刚刚发现了这个解决方案。目前我正在努力理解这个解决方案。它是基于Twitter雪花ID生成算法的Java实现,用于生成64位顺序ID。

https://github.com/Predictor/javasnowflake

欢迎提出任何建议。


但我又看到了public synchronized String generateLongId()。长期来看,同步块会降低性能。 - Saurabh Kumar
1
长期或短期对于同步造成的性能降级有何影响? - user93353
2
@SaurabhKumar 我非常怀疑你会在小的同步块中遇到性能问题。虽然同步通常比CAS慢,但它并不总是比CAS慢(例如Java的NIO,Google也有一个很棒的讲座),而且它肯定比任何数据库串行ID生成器更快(假设你需要先获取ID,数据库往返等)。你应该知道,在Java中还有大量其他被同步的事情(大多数Servlet容器都在某个地方这样做)。 - Adam Gent
嗯,@user93353 如果你运行两次,你不会注意到性能下降。如果你运行200万次,你就错过午餐了:D - Erk

2

您还可以使用时间排序标识符(TSID),它是从Twitter的Snowflake ID派生而来。

TSID与其他在此处展示的想法非常相似。没有必要解释别人很久以前就解释得很好的内容。

示例代码:

public class TSID {

    private static final int RANDOM_BITS = 22;
    private static final int RANDOM_MASK = 0x003fffff;
    private static final long TSID_EPOCH = Instant.parse("2020-01-01T00:00:00.000Z").toEpochMilli();
    private static final AtomicInteger counter = new AtomicInteger((new SecureRandom()).nextInt());

    public static long next() {
        final long time = (System.currentTimeMillis() - TSID_EPOCH) << RANDOM_BITS;
        final long tail = counter.incrementAndGet() & RANDOM_MASK;
        return (time | tail);
    }
}

以上代码是从Tsid复制并改编而来的一个小例子。它可以每毫秒生成多达4194304个ID,假设只有一个ID生成器且不会发生冲突。

如果您担心安全问题,例如生成时间泄漏和序列猜测,请查看Francesco的ID Encryptor库。

请注意,如果您打算在Javascript中使用64位标识符,该语言只支持52位精度,您需要将其转换为某些字符串格式,例如十六进制、基32、基62等。

有关更多信息,请阅读Vlad撰写的这些优秀文章:


1
我希望在应用程序端完成,因为如果我在数据库端完成,我还需要再次触发一个查询来获取行的id,而我想避免这种情况。 不行! 您可以使用AUTOINCREMENT主键,并在JDBC中检索使用INSERT生成的密钥。
String insertSQL = "INSERT INTO table... (name, ...)"
        + " VALUES(?, ..., ?)";
try (Connection connection = getConnection();
        PreparedStatement stmt = connection.prepareStatement(insertSQL,
                Statement.RETURN_GENERATED_KEYS)) {
    stmt.setString(1, ...);
    stmt.setInt(2, ...);
    stmt.setBigDecimal(3, ...);
    ...
    stmt.executeUpdate();

    ResultSet keysRS = stmt.getGeneratedKeys();
    if (keysRS.next()) {
        long id = keysRS.getInt(1);
    }
}

这样更高效、更简单、更安全。UUID有128位,只用64位会降低其唯一性。所以至少主观上不是100%完美。至少对两个长部分执行异或(^)。

0
有点晚回复,但是现在看到这个的人,你也可以实现LUHN算法来为你的主键生成唯一的ID。我们在我们的产品中使用它已经超过5年了,它能够完成工作。

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