如何将 UUID 转换为少于 40 位的数字?

How to convert a UUID to a number with less than 40 digits?

我需要将 UUID(如 ffffffff-ffff-ffff-ffff-ffffffffffff)转换为尽可能少的数字。这些 UUID 由 com.fasterxml.uuid.impl.TimeBasedGenerator.java.

生成
package com.fasterxml.uuid.impl;

import java.util.UUID;

import com.fasterxml.uuid.*;

/**
 * Implementation of UUID generator that uses time/location based generation
 * method (variant 1).
 *<p>
 * As all JUG provided implementations, this generator is fully thread-safe.
 * Additionally it can also be made externally synchronized with other
 * instances (even ones running on other JVMs); to do this,
 * use {@link com.fasterxml.uuid.ext.FileBasedTimestampSynchronizer}
 * (or equivalent).
 *
 * @since 3.0
 */
public class TimeBasedGenerator extends NoArgGenerator
{
    /*
    /**********************************************************************
    /* Configuration
    /**********************************************************************
     */

    protected final EthernetAddress _ethernetAddress;

    /**
     * Object used for synchronizing access to timestamps, to guarantee
     * that timestamps produced by this generator are unique and monotonically increasings.
     * Some implementations offer even stronger guarantees, for example that
     * same guarantee holds between instances running on different JVMs (or
     * with native code).
     */
    protected final UUIDTimer _timer;

    /**
     * Base values for the second long (last 8 bytes) of UUID to construct
     */
    protected final long _uuidL2;
    
    /*
    /**********************************************************************
    /* Construction
    /**********************************************************************
     */

    /**
     * @param ethAddr Hardware address (802.1) to use for generating
     *   spatially unique part of UUID. If system has more than one NIC,
     */
    
    public TimeBasedGenerator(EthernetAddress ethAddr, UUIDTimer timer)
    {
        byte[] uuidBytes = new byte[16];
        if (ethAddr == null) {
            ethAddr = EthernetAddress.constructMulticastAddress();
        }
        // initialize baseline with MAC address info
        _ethernetAddress = ethAddr;
        _ethernetAddress.toByteArray(uuidBytes, 10);
        // and add clock sequence
        int clockSeq = timer.getClockSequence();
        uuidBytes[UUIDUtil.BYTE_OFFSET_CLOCK_SEQUENCE] = (byte) (clockSeq >> 8);
        uuidBytes[UUIDUtil.BYTE_OFFSET_CLOCK_SEQUENCE+1] = (byte) clockSeq;
        long l2 = UUIDUtil.gatherLong(uuidBytes, 8);
        _uuidL2 = UUIDUtil.initUUIDSecondLong(l2);
        _timer = timer;
    }
    
    /*
    /**********************************************************************
    /* Access to config
    /**********************************************************************
     */

    @Override
    public UUIDType getType() { return UUIDType.TIME_BASED; }

    public EthernetAddress getEthernetAddress() { return _ethernetAddress; }
    
    /*
    /**********************************************************************
    /* UUID generation
    /**********************************************************************
     */
    
    /* As timer is not synchronized (nor _uuidBytes), need to sync; but most
     * importantly, synchronize on timer which may also be shared between
     * multiple instances
     */
    @Override
    public UUID generate()
    {
        final long rawTimestamp = _timer.getTimestamp();
        // Time field components are kind of shuffled, need to slice:
        int clockHi = (int) (rawTimestamp >>> 32);
        int clockLo = (int) rawTimestamp;
        // and dice
        int midhi = (clockHi << 16) | (clockHi >>> 16);
        // need to squeeze in type (4 MSBs in byte 6, clock hi)
        midhi &= ~0xF000; // remove high nibble of 6th byte
        midhi |= 0x1000; // type 1
        long midhiL = (long) midhi;
        midhiL = ((midhiL << 32) >>> 32); // to get rid of sign extension
        // and reconstruct
        long l1 = (((long) clockLo) << 32) | midhiL;
        // last detail: must force 2 MSB to be '10'
        return new UUID(l1, _uuidL2);
    }
}

我预计每天至少会在特定机器上生成 1200 万个 UUID。我假设 TimeBasedGenerator 将在程序启动时实例化一次,因此将具有恒定的网络地址和恒定的 运行dom 生成器种子时间(下面代码片段中的值 now)。

final long now = 1644575806478L;

final TimeBasedGenerator sut = new TimeBasedGenerator(EthernetAddress.fromInterface(),
  new UUIDTimer(new Random(now), null));

如何使用

将UUID转换为数字

a) 少于 40 位

b) 以便最早在 6 个月内出现一次重复的 UUID?

尝试失败 1

将 UUID 转换为数字的最简单方法如下所示:

import java.math.BigInteger;
import java.util.function.Function;

public class OldUUIDConverter implements Function<String, BigInteger> {
    @Override
    public BigInteger apply(final String uuid) {
        final String uuidWithoutDashes = uuid.replaceAll("-", "");
        return new BigInteger(uuidWithoutDashes, 16);
    }
}

对于最大可能的 UUID (ffffffff-ffff-ffff-ffff-ffffffffffff),它会生成数字

340282366920938463463374607431768211455
^         ^         ^         ^         ^

因为我们是直接转换UUID,所以如果UUID生成器不是太频繁地产生重复的UUID,就不会重复。

满足上述要求的条件b)。但是条件 a) -- 转换后的数字的长度 -- 不是。

尝试失败 2

有人提议只取UUID的前8位。因此标准 a) 将得到满足。

但是,UUID的前8位重复太频繁了。

我写了一个简单的测试程序:

import com.fasterxml.uuid.EthernetAddress;
import com.fasterxml.uuid.UUIDTimer;
import com.fasterxml.uuid.impl.TimeBasedGenerator;
import org.junit.Test;

import java.io.IOException;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

public class UUIDShortenerTest {
    @Test
    public void test() throws IOException {
        final Set<String> first8DigitsAsLong = new HashSet<>(73000000);

        final long now = 1644575806478L;

        final TimeBasedGenerator sut = new TimeBasedGenerator(EthernetAddress.fromInterface(), new UUIDTimer(new Random(now), null));

        for (int i=0; i < Integer.MAX_VALUE; i++) {
            final String curUuid = sut.generate().toString();
            final String first8Digits = curUuid.substring(0, 8);
            if ((i % 1000000L) == 0) {
                System.out.println(String.format("Counter: %d", i));
            }
            if (!first8DigitsAsLong.add(first8Digits)) {
                System.out.println("Duplicate!");
                System.out.println(i);
                break;
            }
        }
    }
}

我运行它10次。每次我记下UUID代数后前8位重复。

结果如下:

  1. 74499376 代 UUID 之后找到重复的前 8 位数字。
  2. 44259478
  3. 45365652
  4. 45031094
  5. 45445463
  6. 46250299
  7. 45403800
  8. 44658612
  9. 46098250
  10. 43748051

假设在我生成了 7500 万个 UUID 后重复前 8 位数字。

如果我每天生成1200万个UUID,这意味着第一个前8位重复的UUID将在62-63天(75000000/1200000=62.5天)后生成。

这62.5天(约两个月)低于UUID重复前6个月的要求。

我知道的选项

我可以做很多事情来解决这个问题。

一个是增加我使用的第一个数字的数量,直到 UUID 重复的频率达到所需的水平。也就是说,我可以尝试使用 9 个第一位数字而不是 8 个,看看它是否足够好。如果没有,我可以使用前 10 个数字等

另一种方法是使用失败尝试 #1 中的代码,并在具有更大基础的数字系统中对结果数字 340282366920938463463374607431768211455 进行编码。

假设我使用数字 0-9、拉丁文小写和大写字母以及 14 个特殊字符。这导致每个数字可以有 10+26+26+14=76 个数字。我假设上述数字在 base 76 系统中将比十进制或十六进制系统中的相同数字短。

是否还有其他更好的选择(例如具有足够多样性的哈希函数)?

tl;博士

new BigInteger( 
    "ffffffff-ffff-ffff-ffff-ffffffffffff" ,
    16 
)               // Parse the input as hexadecimal numbers. Returns a `BigInteger` object.
.toString()     // Generate text containing the digits of a decimal integer representation of this number.
.codePoints()   // Generate a stream of the Unicode code point number assigned to each of the characters in this string.
.toArray()      // Collect the streamed code point numbers into an array.
.length         // Get size of array.

39

340282366920938463463374607431768211455确实不足40位

您的声明:

But criterion a) -- the length of the converted number -- is not.

…不正确。

生成的整数有 39 位。 Thirty-nine 满足您小于 40 位数的门槛:39 < 40。

这是计算数字的代码。

String input = "ffffffff-ffff-ffff-ffff-ffffffffffff" ;
String uuidWithoutDashes = input.replaceAll( "-" , "" );
BigInteger bi = new BigInteger( uuidWithoutDashes , 16 );
String output = bi.toString() ;
int digitCount = output.codePoints().toArray().length ;

System.out.println( bi ) ;
System.out.println( digitCount ) ;

看到这个code run live at IdeOne.com

340282366920938463463374607431768211455

39

或者,手动计算数字。为了便于计数,我添加了脱字符来标记 1、11、21、31、41。

340282366920938463463374607431768211455
^         ^         ^         ^         ^

至于更大的图景,我无法想象存储从 UUID 的十进制表示中提取的 39 位数字的有价值的目的。研究建议您坚持使用 128 位二进制表示的评论。

我也想知道为什么你提供一个固定的数字作为你的 Random 的种子。这样做会产生与输出相同的数字。

这是生日问题。理论上只要生成两个UUID就可以发生UUID冲突,不过有方便的probability tables。您正在尝试生成 12*10^6*30*6 ~= 2*10^9 个 ID。您愿意承担多少风险?