87 votes

Stockage de l'UUID en tant que chaîne base64

J'ai expérimenté l'utilisation des UUID comme clés de base de données. Je veux occuper le moins d'octets possible, tout en gardant la représentation de l'UUID lisible par l'homme.

Je pense être parvenu à réduire le nombre d'octets à 22 en utilisant la base64 et en supprimant quelques "==" de fin de ligne qui semblent inutiles à stocker pour mes besoins. Cette approche présente-t-elle des lacunes ?

En fait, mon code de test effectue un certain nombre de conversions pour ramener l'UUID à une chaîne de 22 octets, puis le reconvertit en 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;
    }

}

sortie :

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

0 votes

Une façon de voir les choses est qu'un UUID est composé de 128 bits aléatoires, donc 6 bits par élément base64, soit 128/6=21.3, donc vous avez raison de dire qu'il faut 22 positions base64 pour stocker les mêmes données.

1 votes

Votre question précédente semble essentiellement la même : stackoverflow.com/questions/772325/

1 votes

Je ne suis pas sûr que votre code soit correct dans la deuxième boucle for de asByteBuffer vous soustrayez i de 7 mais i itère de 8 à 16 ce qui signifie qu'il sera décalé d'un nombre négatif. IIRC <<< s'enroule autour mais cela ne semble toujours pas correct.

66voto

swill Points 344

J'ai également essayé de faire quelque chose de similaire. Je travaille avec une application Java qui utilise des UUID de la forme suivante 6fcb514b-b878-4c9d-95b7-8dc3a7ce6fd8 (qui sont générés avec la librairie UUID standard de Java). Dans mon cas, je devais être en mesure de réduire cet UUID à 30 caractères ou moins. J'ai utilisé Base64 et ce sont mes fonctions de commodité. J'espère qu'elles seront utiles à quelqu'un, car la solution n'était pas évidente pour moi tout de suite.

Utilisation :

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

Sortie :

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

Fonctions :

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 votes

Désolé, je n'avais pas remarqué ce commentaire. Oui, j'utilise Apache commons-codec. import org.apache.commons.codec.binary.Base64;

0 votes

Une réduction de 39% de la taille. Sympa.

9 votes

Vous pouvez utiliser le système intégré depuis Java 8. Base64.getUrlEncoder().encodeToString(bb.array()) y Base64.getUrlDecoder().decode(id)

33voto

erickson Points 127945

Vous pouvez sans crainte laisser tomber le remplissage "==" dans cette application. Si vous décodez le texte en base-64 en octets, certaines bibliothèques s'attendent à ce qu'il soit présent, mais comme vous utilisez la chaîne résultante comme clé, ce n'est pas un problème.

J'utiliserais la base 64 parce que ses caractères de codage peuvent être sécurisés pour les URL et qu'ils ressemblent moins à du charabia. Mais il y a aussi Base-85 . Il utilise davantage de symboles et code 4 octets comme 5 caractères, ce qui vous permet de réduire votre texte à 20 caractères.

19 votes

BAse85 n'enregistre que 2 caractères. De plus, l'utilisation de Base85 dans les URL n'est pas sûre, et l'une des principales utilisations des UUID est l'identification des entités dans les bases de données, qui se retrouvent ensuite dans les URL.

0 votes

@erickson pouvez-vous s'il vous plaît partager un extrait de code pour convertir en Base85. J'ai essayé mais je n'ai pas trouvé de bibliothèque java Base85 fiable.

0 votes

@Manish Il existe plusieurs variantes de la base 85, mais chacune d'entre elles nécessite plus qu'un "bout de code" pour être implémentée ; ce genre de réponse n'a pas sa place sur ce site. Quels types de problèmes avez-vous rencontrés dans les bibliothèques que vous avez essayées ? Je recommande vraiment la base-64, car elle est prise en charge par le noyau de Java et ne coûte qu'environ 7 % d'espace supplémentaire pour les valeurs codées.

12voto

stikkos Points 342

Voici mon code, il utilise org.apache.commons.codec.binary.Base64 pour produire des chaînes uniques url-safe d'une longueur de 22 caractères (et qui ont la même unicité que 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));
}

0 votes

Pourquoi dites-vous que ce code produit un url safe uuid ? Comme je comprends url safe uuid ne doit pas contenir "+" et "/". Mais dans votre code je ne vois pas, que ces symboles sont remplacés. Pourriez-vous expliquer ?

0 votes

La classe Base64 de comons-codec possède un paramètre de construction urlSafe que j'ai défini à true (si true, cet encodeur émettra les caractères - et _ au lieu des caractères + et / habituels). ( commons.apache.org/proper/commons-codec/apidocs/org/apache/ )

0 votes

Merci beaucoup pour votre explication.

8voto

Bob Aman Points 19110

J'ai une application où je fais presque exactement cela. UUID codé sur 22 caractères. Cela fonctionne bien. Cependant, la principale raison pour laquelle je procède de cette manière est que les ID sont exposés dans les URI de l'application Web, et que 36 caractères sont vraiment très grands pour quelque chose qui apparaît dans un URI. 22 caractères, c'est encore un peu long, mais nous nous en contentons.

Voici le code Ruby pour cela :

  # 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

Ce n'est pas exactement la même chose que l'encodage base64 parce que base64 utilise des caractères qui devraient être échappés s'ils apparaissaient dans un composant de chemin URI. L'implémentation Java sera probablement très différente, car il est plus probable que vous disposiez d'un tableau d'octets bruts plutôt que d'un très grand nombre entier.

3voto

kdgregory Points 21849

Vous ne dites pas quel SGBD vous utilisez, mais il semble que le format RAW soit la meilleure approche si vous êtes soucieux de gagner de l'espace. Vous devez simplement vous rappeler de convertir pour toutes les requêtes, sinon vous risquez une énorme baisse de performance.

Mais je dois demander : les octets sont-ils vraiment si chers là où vous vivez ?

0 votes

Oui, je pense que oui... Je veux économiser autant d'espace que possible tout en restant lisible par l'homme.

0 votes

OK, pourquoi pensez-vous cela ? Vous stockez un milliard de lignes ? Vous économiserez 8 milliards d'octets, ce qui n'est pas beaucoup. En fait, vous économiserez moins, car votre SGBD pourrait réserver de l'espace supplémentaire pour l'encodage. Et si vous optez pour VARCHAR au lieu de CHAR à taille fixe, vous allez perdre l'espace nécessaire pour enregistrer la longueur réelle.

0 votes

... et cette "économie" n'est valable que si vous utilisez un CHAR(32). Si vous utilisez le format RAW, vous gagnerez réellement de l'espace.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X