将UUID作为Base64字符串存储

94

我一直在尝试将UUID作为数据库键使用。我希望尽可能少地占用字节,同时保持UUID表示可读性。

我使用了base64将其减少到22个字节,并删除了一些似乎对于我的用途没有必要存储的尾随“==”。这种方法是否有任何缺陷?

基本上,我的测试代码进行了一系列的转换,将UUID缩小到22个字节的字符串,然后将其转换回UUID。

import java.io.IOException;
import java.util.UUID;

public class UUIDTest {

    public static void main(String[] args){
        UUID uuid = UUID.randomUUID();
        System.out.println("UUID String: " + uuid.toString());
        System.out.println("Number of Bytes: " + uuid.toString().getBytes().length);
        System.out.println();

        byte[] uuidArr = asByteArray(uuid);
        System.out.print("UUID Byte Array: ");
        for(byte b: uuidArr){
            System.out.print(b +" ");
        }
        System.out.println();
        System.out.println("Number of Bytes: " + uuidArr.length);
        System.out.println();


        try {
            // Convert a byte array to base64 string
            String s = new sun.misc.BASE64Encoder().encode(uuidArr);
            System.out.println("UUID Base64 String: " +s);
            System.out.println("Number of Bytes: " + s.getBytes().length);
            System.out.println();


            String trimmed = s.split("=")[0];
            System.out.println("UUID Base64 String Trimmed: " +trimmed);
            System.out.println("Number of Bytes: " + trimmed.getBytes().length);
            System.out.println();

            // Convert base64 string to a byte array
            byte[] backArr = new sun.misc.BASE64Decoder().decodeBuffer(trimmed);
            System.out.print("Back to UUID Byte Array: ");
            for(byte b: backArr){
                System.out.print(b +" ");
            }
            System.out.println();
            System.out.println("Number of Bytes: " + backArr.length);

            byte[] fixedArr = new byte[16];
            for(int i= 0; i<16; i++){
                fixedArr[i] = backArr[i];
            }
            System.out.println();
            System.out.print("Fixed UUID Byte Array: ");
            for(byte b: fixedArr){
                System.out.print(b +" ");
            }
            System.out.println();
            System.out.println("Number of Bytes: " + fixedArr.length);

            System.out.println();
            UUID newUUID = toUUID(fixedArr);
            System.out.println("UUID String: " + newUUID.toString());
            System.out.println("Number of Bytes: " + newUUID.toString().getBytes().length);
            System.out.println();

            System.out.println("Equal to Start UUID? "+newUUID.equals(uuid));
            if(!newUUID.equals(uuid)){
                System.exit(0);
            }


        } catch (IOException e) {
        }

    }


    public static byte[] asByteArray(UUID uuid) {

        long msb = uuid.getMostSignificantBits();
        long lsb = uuid.getLeastSignificantBits();
        byte[] buffer = new byte[16];

        for (int i = 0; i < 8; i++) {
            buffer[i] = (byte) (msb >>> 8 * (7 - i));
        }
        for (int i = 8; i < 16; i++) {
            buffer[i] = (byte) (lsb >>> 8 * (7 - i));
        }

        return buffer;

    }

    public static UUID toUUID(byte[] byteArray) {

        long msb = 0;
        long lsb = 0;
        for (int i = 0; i < 8; i++)
            msb = (msb << 8) | (byteArray[i] & 0xff);
        for (int i = 8; i < 16; i++)
            lsb = (lsb << 8) | (byteArray[i] & 0xff);
        UUID result = new UUID(msb, lsb);

        return result;
    }

}

输出:

UUID String: cdaed56d-8712-414d-b346-01905d0026fe
Number of Bytes: 36

UUID Byte Array: -51 -82 -43 109 -121 18 65 77 -77 70 1 -112 93 0 38 -2 
Number of Bytes: 16

UUID Base64 String: za7VbYcSQU2zRgGQXQAm/g==
Number of Bytes: 24

UUID Base64 String Trimmed: za7VbYcSQU2zRgGQXQAm/g
Number of Bytes: 22

Back to UUID Byte Array: -51 -82 -43 109 -121 18 65 77 -77 70 1 -112 93 0 38 -2 0 38 
Number of Bytes: 18

Fixed UUID Byte Array: -51 -82 -43 109 -121 18 65 77 -77 70 1 -112 93 0 38 -2 
Number of Bytes: 16

UUID String: cdaed56d-8712-414d-b346-01905d0026fe
Number of Bytes: 36

Equal to Start UUID? true

一个角度来看,UUID 是 128 个随机位,每个 base64 项有 6 个位,所以是 128/6=21.3,因此你说需要 22 个 base64 位置来存储相同的数据是正确的。 - Stijn Sanders
1
你之前的问题似乎本质上是一样的:https://dev59.com/gUfRa4cB1Zd3GeqP70pX - erickson
1
我不确定你的代码在 asByteBuffer 的第二个 for 循环中是否正确。你从 7 中减去了 i,但是 i 的迭代范围是从 8 到 16,这意味着它将会以负数进行移位。如果我没记错的话,<<< 会循环,但它似乎仍然不正确。 - Jon Tirsen
1
我认为更容易的方法是使用ByteBuffer将这两个long转换为字节数组,就像这个问题中所示:https://dev59.com/Wmw15IYBdhLWcg3wA3GT - Jon Tirsen
“人类可读性”有什么意义?看看mysql/mariadb函数uuid_short()的作用。 - theking2
URLs和分享它们 - mainstringargs
11个回答

70
我也在尝试做类似的事情。我正在使用一个使用Java应用程序,该应用程序使用以下形式的UUID:6fcb514b-b878-4c9d-95b7-8dc3a7ce6fd8(这些是使用Java中的标准UUID库生成的)。在我的情况下,我需要将此UUID缩短到30个字符或更少。我使用了Base64,并且这些是我的方便函数。希望它们对某人有所帮助,因为解决方案对我来说并不明显。

用法:

String uuid_str = "6fcb514b-b878-4c9d-95b7-8dc3a7ce6fd8";
String uuid_as_64 = uuidToBase64(uuid_str);
System.out.println("as base64: "+uuid_as_64);
System.out.println("as uuid: "+uuidFromBase64(uuid_as_64));

输出:

as base64: b8tRS7h4TJ2Vt43Dp85v2A
as uuid  : 6fcb514b-b878-4c9d-95b7-8dc3a7ce6fd8

功能:

import org.apache.commons.codec.binary.Base64;

private static String uuidToBase64(String str) {
    Base64 base64 = new Base64();
    UUID uuid = UUID.fromString(str);
    ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
    bb.putLong(uuid.getMostSignificantBits());
    bb.putLong(uuid.getLeastSignificantBits());
    return base64.encodeBase64URLSafeString(bb.array());
}
private static String uuidFromBase64(String str) {
    Base64 base64 = new Base64(); 
    byte[] bytes = base64.decodeBase64(str);
    ByteBuffer bb = ByteBuffer.wrap(bytes);
    UUID uuid = new UUID(bb.getLong(), bb.getLong());
    return uuid.toString();
}

1
抱歉,我没有注意到这个评论。是的,我正在使用Apache commons-codec。import org.apache.commons.codec.binary.Base64; - swill
1
大小减少了39%。不错。 - Stu Thompson
10
自Java 8起,您可以使用内置的 Base64.getUrlEncoder().encodeToString(bb.array())Base64.getUrlDecoder().decode(id) - Wpigott
1
你可以选择不实例化Base64类,方法encodeBase64URLSafeString(b [])和decodeBase64(str)是静态的,对吗? - Kumar Mani

35

在这个应用程序中,您可以安全地删除填充的“==”。如果您将Base-64文本解码回字节,某些库可能希望它存在,但由于您只是使用生成的字符串作为密钥,所以这不是一个问题。

我会使用Base-64,因为它的编码字符是URL安全的,并且看起来不像乱码。但也有Base-85。它使用更多的符号,将4个字节编码为5个字符,因此您可以将文本缩短为20个字符。


20
Base85 只能节省 2 个字符。此外,使用 Base85 在 URL 中是不安全的,而 UUID 的一个主要用途是数据库中的实体标识符,最终会出现在 URL 中。 - Dennis
@erickson,您能否分享一些代码片段来将内容转换为Base85编码?我尝试过了,但是没有找到可靠的Java Base85库。 - Manish
@Manish,基于85进制的变种有几种,但每一种都需要编写代码才能实现。这种类型的答案真的不适合在本网站上回答。您已经尝试过哪些库并遇到了什么问题呢?我真的建议您使用基于64进制的编码方式,因为它在Java核心中得到支持,并且仅会额外增加7%的编码值空间。 - erickson
@erickson,但Base64不能解决我将UUID缩短为20个字符长度的目的。 - Manish
@Manish 我明白了。你的需求是否禁止使用任何特殊字符,例如引号、百分号(%)或反斜杠(\\)?你需要对标识符进行编码和解码吗?(也就是说,你想要能够将其转换回传统的UUID,还是只想缩短它们?) - erickson

14

这是我的代码,它使用org.apache.commons.codec.binary.Base64产生22个字符长度的url安全且具有与UUID相同独特性的字符串。

private static Base64 BASE64 = new Base64(true);
public static String generateKey(){
    UUID uuid = UUID.randomUUID();
    byte[] uuidArray = KeyGenerator.toByteArray(uuid);
    byte[] encodedArray = BASE64.encode(uuidArray);
    String returnValue = new String(encodedArray);
    returnValue = StringUtils.removeEnd(returnValue, "\r\n");
    return returnValue;
}
public static UUID convertKey(String key){
    UUID returnValue = null;
    if(StringUtils.isNotBlank(key)){
        // Convert base64 string to a byte array
        byte[] decodedArray = BASE64.decode(key);
        returnValue = KeyGenerator.fromByteArray(decodedArray);
    }
    return returnValue;
}
private static byte[] toByteArray(UUID uuid) {
    byte[] byteArray = new byte[(Long.SIZE / Byte.SIZE) * 2];
    ByteBuffer buffer = ByteBuffer.wrap(byteArray);
    LongBuffer longBuffer = buffer.asLongBuffer();
    longBuffer.put(new long[] { uuid.getMostSignificantBits(), uuid.getLeastSignificantBits() });
    return byteArray;
}
private static UUID fromByteArray(byte[] bytes) {
    ByteBuffer buffer = ByteBuffer.wrap(bytes);
    LongBuffer longBuffer = buffer.asLongBuffer();
    return new UUID(longBuffer.get(0), longBuffer.get(1));
}

你为什么说这段代码生成了URL安全的UUID?据我理解,URL安全的UUID不应包含“+”和“/”。但是在你的代码中,我没有看到这些符号被替换。你能解释一下吗? - Pavel_K
commons-codec库中的Base64类有一个urlSafe构造函数参数,我将其设置为true(如果为true,则此编码器将发出“-”和“_”,而不是通常的“+”和“/”字符)。 (https://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/binary/Base64.html#Base64-boolean-) - stikkos
非常感谢您的解释。 - Pavel_K

8

我有一个应用程序,几乎完全符合这个要求。22位字符编码的UUID。它运行良好。然而,我这样做的主要原因是ID在Web应用程序的URI中暴露出来,36个字符对于出现在URI中的内容来说真的很大。22个字符仍然有点长,但我们可以应付。

以下是此操作的Ruby代码:

  # Make an array of 64 URL-safe characters
  CHARS64 = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + ["-", "_"]
  # Return a 22 byte URL-safe string, encoded six bits at a time using 64 characters
  def to_s22
    integer = self.to_i # UUID as a raw integer
    rval = ""
    22.times do
      c = (integer & 0x3F)
      rval += CHARS64[c]
      integer = integer >> 6
    end
    return rval.reverse
  end

这不完全等同于Base64编码,因为Base64使用的字符如果出现在URI路径组件中,就必须进行转义。Java实现可能会有所不同,因为您更可能拥有原始字节数组而不是一个非常大的整数。


5

以下是JDK8中引入的java.util.Base64示例:

import java.nio.ByteBuffer;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.UUID;

public class Uuid64 {

  private static final Encoder BASE64_URL_ENCODER = Base64.getUrlEncoder().withoutPadding();

  public static void main(String[] args) {
    // String uuidStr = UUID.randomUUID().toString();
    String uuidStr = "eb55c9cc-1fc1-43da-9adb-d9c66bb259ad";
    String uuid64 = uuidHexToUuid64(uuidStr);
    System.out.println(uuid64); //=> 61XJzB_BQ9qa29nGa7JZrQ
    System.out.println(uuid64.length()); //=> 22
    String uuidHex = uuid64ToUuidHex(uuid64);
    System.out.println(uuidHex); //=> eb55c9cc-1fc1-43da-9adb-d9c66bb259ad
  }

  public static String uuidHexToUuid64(String uuidStr) {
    UUID uuid = UUID.fromString(uuidStr);
    byte[] bytes = uuidToBytes(uuid);
    return BASE64_URL_ENCODER.encodeToString(bytes);
  }

  public static String uuid64ToUuidHex(String uuid64) {
    byte[] decoded = Base64.getUrlDecoder().decode(uuid64);
    UUID uuid = uuidFromBytes(decoded);
    return uuid.toString();
  }

  public static byte[] uuidToBytes(UUID uuid) {
    ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
    bb.putLong(uuid.getMostSignificantBits());
    bb.putLong(uuid.getLeastSignificantBits());
    return bb.array();
  }

  public static UUID uuidFromBytes(byte[] decoded) {
    ByteBuffer bb = ByteBuffer.wrap(decoded);
    long mostSigBits = bb.getLong();
    long leastSigBits = bb.getLong();
    return new UUID(mostSigBits, leastSigBits);
  }
}

在Base64中编码的UUID是URL安全的且不带填充。

3

您没有说明使用的是什么数据库管理系统,但如果您关心节省空间,似乎RAW会是最佳选择。您只需要记得对所有查询进行转换,否则会出现巨大的性能下降。

但我必须问一下:在您所在的地方,字节真的这么昂贵吗?


是的,我认为是这样的... 我想尽可能节省空间,同时仍然保持可读性。 - mainstringargs
好的,你为什么这么想呢?你是在存储十亿行数据吗?你只能节省 8 十亿字节,这并不多。实际上,你会节省更少的空间,因为你的数据库管理系统可能会为编码保留额外的空间。如果你使用 VARCHAR 而不是固定大小的 CHAR,你将失去保存实际长度所需的空间。 - kdgregory
...而且只有在使用CHAR(32)时才能实现“节省”。如果您使用RAW,您实际上会节省空间。 - kdgregory
10
任何合理的DBMS都可以用原生格式存储UUID,需要16字节。任何合理的数据库工具都会在查询结果中将其转换为标准格式(例如“cdaed56d-8712-414d-b346-01905d0026fe”)。人们已经这样做很长时间了,没有必要重新发明轮子。 - Robert Lewis
1
他可能正在尝试在QR码中包含UUID,这意味着压缩对于创建更易于扫描的QR码非常有用。 - nym

3

虽然它不是Base64编码,但这也值得一看,因为增加了灵活性:有一个Clojure库实现了UUID的紧凑26个字符URL安全表示(https://github.com/tonsky/compact-uuids)。

以下是一些亮点:

  • 生成的字符串比传统36个字符缩小30%(只有26个字符)
  • 支持完整的UUID范围(128位)
  • 编码安全(仅使用ASCII中可读的字符)
  • URL/文件名安全
  • 大小写安全
  • 避免模糊的字符(i/I/l/L/1/O/o/0)
  • 编码后的26个字符的字母排序与默认的UUID排序顺序匹配

这些都是非常好的属性。 我在我的应用程序中将此编码用于数据库键和用户可见标识符,并且效果非常好。


如果最有效的格式是16个二进制字节,为什么要将其用作数据库键? - kravemir
为了方便起见,使用字符串形式的UUID是显而易见的:每个软件都能够处理它。使用二进制形式的UUID作为键是一种优化,这将产生显着的开发和维护成本。我决定不值得这样做的努力。 - Jan Rychter

2

编解码器Base64CodecBase64UrlCodec可以高效地将UUID编码为base-64和base-64-url格式。

// Returns a base-64 string
// input:: 01234567-89AB-4DEF-A123-456789ABCDEF
// output: ASNFZ4mrTe+hI0VniavN7w
String string = Base64Codec.INSTANCE.encode(uuid);

// Returns a base-64-url string
// input:: 01234567-89AB-4DEF-A123-456789ABCDEF
// output: ASNFZ4mrTe-hI0VniavN7w
String string = Base64UrlCodec.INSTANCE.encode(uuid);

uuid-creator的同一中有其他编码的编解码器。


1

这是我在Kotlin中的方法:

            val uuid: UUID = UUID.randomUUID()
            val uid = BaseEncoding.base64Url().encode(
                ByteBuffer.allocate(16)
                    .putLong(uuid.mostSignificantBits)
                    .putLong(uuid.leastSignificantBits)
                    .array()
            ).trimEnd('=')


1

以下是我使用的 UUID(Comb 风格)代码。它包含将 UUID 字符串或 UUID 类型转换为 base64 的代码。我每 64 位进行一次转换,因此不需要处理任何等号:

JAVA

import java.util.Calendar;
import java.util.UUID;
import org.apache.commons.codec.binary.Base64;

public class UUIDUtil{
    public static UUID combUUID(){
        private UUID srcUUID = UUID.randomUUID();
        private java.sql.Timestamp ts = new java.sql.Timestamp(Calendar.getInstance().getTime().getTime());

        long upper16OfLowerUUID = this.zeroLower48BitsOfLong( srcUUID.getLeastSignificantBits() );
        long lower48Time = UUIDUtil.zeroUpper16BitsOfLong( ts );
        long lowerLongForNewUUID = upper16OfLowerUUID | lower48Time;
        return new UUID( srcUUID.getMostSignificantBits(), lowerLongForNewUUID );
    }   
    public static base64URLSafeOfUUIDObject( UUID uuid ){
        byte[] bytes = ByteBuffer.allocate(16).putLong(0, uuid.getLeastSignificantBits()).putLong(8, uuid.getMostSignificantBits()).array();
        return Base64.encodeBase64URLSafeString( bytes );
    }
    public static base64URLSafeOfUUIDString( String uuidString ){
    UUID uuid = UUID.fromString( uuidString );
        return UUIDUtil.base64URLSafeOfUUIDObject( uuid );
    }
    private static long zeroLower48BitsOfLong( long longVar ){
        long upper16BitMask =  -281474976710656L;
        return longVar & upper16BitMask;
    }
    private static void zeroUpper16BitsOfLong( long longVar ){
        long lower48BitMask =  281474976710656L-1L;
        return longVar & lower48BitMask;
    }
}

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