Speichern der UUID als base64-Zeichenfolge

80

Ich habe mit der Verwendung von UUIDs als Datenbankschlüssel experimentiert. Ich möchte so wenig Bytes wie möglich belegen und gleichzeitig die UUID-Darstellung für den Menschen lesbar halten.

Ich denke, dass ich es mit base64 auf 22 Bytes reduziert und einige nachfolgende "==" entfernt habe, die für meine Zwecke unnötig zu speichern scheinen. Gibt es irgendwelche Mängel bei diesem Ansatz?

Grundsätzlich führt mein Testcode eine Reihe von Konvertierungen durch, um die UUID auf eine 22-Byte-Zeichenfolge zu reduzieren, und konvertiert sie dann wieder in eine 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;
    }

}

Ausgabe:

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
Hauptstringargs
quelle
Eine Möglichkeit, dies zu betrachten, besteht darin, dass eine UUID 128 zufällige Bits, also 6 Bits pro base64-Element, 128/6 = 21,3 ist. Sie haben also Recht, dass Sie 22 base64-Positionen benötigen, um dieselben Daten zu speichern.
Stijn Sanders
Ihre
erickson
Ich bin nicht sicher, ob Ihr Code in der zweiten for-Schleife von asByteBuffer korrekt ist. Sie subtrahieren i von 7, aber i iteriert von 8 bis 16, was bedeutet, dass er sich um eine negative Zahl verschiebt. IIRC <<< dreht sich um, aber es scheint immer noch nicht korrekt zu sein.
Jon Tirsen
Ich denke, es ist einfacher, nur ByteBuffer zu verwenden, um die beiden Longs in ein Byte-Array wie in dieser Frage zu konvertieren: stackoverflow.com/questions/6881659/…
Jon Tirsen

Antworten:

31

Sie können die Polsterung "==" in dieser Anwendung sicher ablegen. Wenn Sie den Base-64-Text wieder in Bytes dekodieren würden, würden einige Bibliotheken erwarten, dass er vorhanden ist. Da Sie jedoch nur die resultierende Zeichenfolge als Schlüssel verwenden, ist dies kein Problem.

Ich würde Base-64 verwenden, da seine Codierungszeichen URL-sicher sein können und es weniger nach Kauderwelsch aussieht. Es gibt aber auch Base-85 . Es werden mehr Symbole und Codes mit 4 Bytes als 5 Zeichen verwendet, sodass Sie Ihren Text auf 20 Zeichen reduzieren können.

erickson
quelle
17
BAse85 speichert nur 2 Zeichen. Außerdem ist die Verwendung von Base85 in URLs nicht sicher, und eine Hauptverwendung von UUIDs sind Entitätskennungen in Datenbanken, die dann in URLs enden.
Dennis
@erickson können Sie bitte ein Code-Snippet teilen, um es in Base85 zu konvertieren. Ich habe es versucht, konnte aber keine zuverlässige Base85-Java-Bibliothek erhalten
Manish
@Manish Es gibt verschiedene Varianten von base-85, aber für die Implementierung ist jeweils mehr als ein „Snippet“ von Code erforderlich. Diese Art von Antwort passt wirklich nicht auf diese Seite. Welche Probleme haben Sie in den Bibliotheken gefunden, die Sie ausprobiert haben? Ich würde base-64 wirklich empfehlen, da es Unterstützung in Core Java bietet und nur etwa 7% mehr Speicherplatz für codierte Werte kostet.
Erickson
@erickson, aber base64 löst nicht meinen Zweck, die UUID auf 20 Zeichen zu reduzieren.
Manish
@ Manish verstehe ich. Verbieten Ihre Anforderungen Sonderzeichen wie Anführungszeichen, Prozentzeichen ( %) oder Backslash (`\`)? Müssen Sie den Bezeichner codieren und decodieren? (Das heißt, möchten Sie in der Lage sein, wieder in eine herkömmliche UUID zu konvertieren oder diese einfach zu verkürzen?)
erickson
62

Ich habe auch versucht, etwas Ähnliches zu tun. Ich arbeite mit einer Java-Anwendung, die UUIDs des Formulars verwendet 6fcb514b-b878-4c9d-95b7-8dc3a7ce6fd8(die mit der Standard-UUID lib in Java generiert werden). In meinem Fall musste ich in der Lage sein, diese UUID auf 30 Zeichen oder weniger zu reduzieren. Ich habe Base64 verwendet und dies sind meine praktischen Funktionen. Hoffentlich sind sie für jemanden hilfreich, da mir die Lösung nicht sofort klar war.

Verwendung:

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));

Ausgabe:

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

Funktionen:

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();
}
schlucken
quelle
1
Entschuldigung, ich hatte diesen Kommentar nicht bemerkt. Ja, ich verwende den Apache Commons-Codec. import org.apache.commons.codec.binary.Base64;
Swill
Eine Verkleinerung um 39%. Nett.
Stu Thompson
6
Sie können eingebaute seit Java 8. Base64.getUrlEncoder().encodeToString(bb.array())undBase64.getUrlDecoder().decode(id)
Wpigott
Sie können die Base64-Klasse nicht instanziieren. Die Methoden encodeBase64URLSafeString (b []) und decodeBase64 (str) sind statisch, nicht wahr?
Kumar Mani
9

Hier ist mein Code, der org.apache.commons.codec.binary.Base64 verwendet, um url-sichere eindeutige Zeichenfolgen mit einer Länge von 22 Zeichen (und derselben Eindeutigkeit wie UUID) zu erstellen.

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));
}
Stikkos
quelle
8

Ich habe eine Anwendung, in der ich fast genau das mache. 22 Zeichen codierte UUID. Es funktioniert gut. Der Hauptgrund, warum ich das so mache, ist, dass die IDs in den URIs der Web-App verfügbar sind und 36 Zeichen für etwas, das in einem URI angezeigt wird, wirklich ziemlich groß sind. 22 Zeichen sind noch ein bisschen lang, aber wir schaffen es.

Hier ist der Ruby-Code dafür:

  # 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

Es ist nicht genau dasselbe wie die Base64-Codierung, da Base64 Zeichen verwendet, die maskiert werden müssten, wenn sie in einer URI-Pfadkomponente erscheinen würden. Die Java-Implementierung ist wahrscheinlich ganz anders, da Sie eher ein Array von Rohbytes als eine wirklich große Ganzzahl haben.

Bob Aman
quelle
3

Sie sagen nicht, welches DBMS Sie verwenden, aber es scheint, dass RAW der beste Ansatz ist, wenn Sie Platz sparen möchten. Sie müssen nur daran denken, für alle Abfragen zu konvertieren, sonst riskieren Sie einen enormen Leistungsabfall.

Aber ich muss fragen: Sind Bytes bei Ihnen wirklich so teuer?

kdgregory
quelle
Ja, ich denke schon ... Ich möchte so viel Platz wie möglich sparen, während es dennoch für Menschen lesbar ist.
Mainstringargs
OK, warum denkst du so? Speichern Sie eine Milliarde Zeilen? Sie sparen 8 Milliarden Bytes, was nicht viel ist. Tatsächlich sparen Sie weniger, da Ihr DBMS möglicherweise zusätzlichen Speicherplatz für die Codierung reserviert. Und wenn Sie VARCHAR anstelle von CHAR mit fester Größe verwenden, verlieren Sie den Platz, der zum Speichern der tatsächlichen Länge erforderlich ist.
kdgregory
... und diese "Einsparungen" sind nur möglich, wenn Sie einen CHAR (32) verwenden. Wenn Sie RAW verwenden, sparen Sie tatsächlich Platz.
kdgregory
8
Mit jedem vernünftigen DBMS können Sie UUIDs im nativen Format speichern, für das 16 Byte erforderlich sind. Alle vernünftigen DB-Tools konvertieren diese in den Abfrageergebnissen in das Standardformat (z. B. "cdaed56d-8712-414d-b346-01905d0026fe"). Die Leute machen das schon lange. Das Rad muss nicht neu erfunden werden.
Robert Lewis
1
Er könnte versuchen, eine UUID in einen QR-Code aufzunehmen, was bedeuten würde, dass die Komprimierung nützlich ist, um einen leichter scannbaren QR-Code zu erstellen.
Nym
3

Hier ist ein Beispiel mit java.util.Base64in JDK8 eingeführt:

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);
  }
}

Die in Base64 codierte UUID ist URL-sicher und ohne Auffüllen.

Sergey Ponomarev
quelle
3

Dies ist nicht genau das, wonach Sie gefragt haben (es ist nicht Base64), aber aufgrund der zusätzlichen Flexibilität einen Blick wert: Es gibt eine Clojure-Bibliothek, die eine kompakte URL-sichere Darstellung von UUIDs mit 26 Zeichen implementiert ( https: // github) .com / Tonsky / Compact-Uuids ).

Einige Highlights:

  • Erzeugt 30% kleinere Saiten (26 Zeichen gegenüber herkömmlichen 36 Zeichen)
  • Unterstützt den vollen UUID-Bereich (128 Bit)
  • Codierungssicher (verwendet nur lesbare Zeichen aus ASCII)
  • URL / Dateiname sicher
  • Klein- / Großbuchstaben sicher
  • Vermeidet mehrdeutige Zeichen (i / I / l / L / 1 / O / o / 0)
  • Die alphabetische Sortierung in codierten Zeichenfolgen mit 26 Zeichen entspricht der Standard-UUID-Sortierreihenfolge

Das sind ziemlich schöne Eigenschaften. Ich habe diese Codierung in meinen Anwendungen sowohl für Datenbankschlüssel als auch für vom Benutzer sichtbare Bezeichner verwendet und sie funktioniert sehr gut.

Jan Rychter
quelle
Warum verwenden Sie es für Datenbankschlüssel, wenn das effektivste Format 16 Binärbytes ist?
Kravemir
Zur Bequemlichkeit. Die Verwendung einer UUID in Zeichenfolgenform ist offensichtlich: Jede Software kann damit umgehen. Die Verwendung als Schlüssel in binärer Form ist eine Optimierung, die erhebliche Entwicklungs- und Wartungskosten verursachen würde. Ich entschied, dass es die Mühe nicht wert ist.
Jan Rychter
1

Unten ist, was ich für eine UUID (Comb Style) verwende. Es enthält Code zum Konvertieren einer UUID-Zeichenfolge oder eines UUID-Typs in base64. Ich mache es pro 64 Bit, also beschäftige ich mich nicht mit Gleichheitszeichen:

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;
    }
}
Dennis
quelle