Key Value Store con Chronicle Map

1. Panoramica

In questo tutorial, vedremo come possiamo usare la Chronicle Map per memorizzare le coppie chiave-valore. Creeremo anche brevi esempi per dimostrarne il comportamento e l'utilizzo.

2. Che cos'è una mappa delle cronache?

In base alla documentazione, "Chronicle Map è un archivio chiave-valore super veloce, in memoria, non bloccante, progettato per applicazioni a bassa latenza e / o multi-processo".

In poche parole, è un archivio di valori-chiave off-heap. La mappa non richiede una grande quantità di RAM per funzionare correttamente. Può crescere in base alla capacità del disco disponibile . Inoltre, supporta la replica dei dati in una configurazione server multi-master.

Vediamo ora come configurarlo e lavorarci.

3. Dipendenza da Maven

Per iniziare, dobbiamo aggiungere la dipendenza della mappa cronologica al nostro progetto:

 net.openhft chronicle-map 3.17.2 

4. Tipi di mappa delle cronache

Possiamo creare una mappa in due modi: come mappa in memoria o come mappa persistente.

Vediamoli entrambi in dettaglio.

4.1. Mappa in memoria

Una Chronicle Map in memoria è un archivio di mappe creato all'interno della memoria fisica del server. Ciò significa che è accessibile solo all'interno del processo JVM in cui viene creato l'archivio mappe .

Vediamo un rapido esempio:

ChronicleMap inMemoryCountryMap = ChronicleMap .of(LongValue.class, CharSequence.class) .name("country-map") .entries(50) .averageValue("America") .create();

Per semplicità, stiamo creando una mappa che memorizza 50 ID di paesi e i loro nomi. Come possiamo vedere nello snippet di codice, la creazione è piuttosto semplice tranne che per la configurazione averageValue () . Questo dice alla mappa di configurare il numero medio di byte presi dai valori delle voci della mappa.

In altre parole, quando si crea la mappa, Chronicle Map determina il numero medio di byte presi dalla forma serializzata di valori. Lo fa serializzando il valore medio dato utilizzando i marshaller di valori configurati. Quindi assegnerà il numero determinato di byte per il valore di ciascuna voce della mappa.

Una cosa che dobbiamo notare quando si tratta della mappa in memoria è che i dati sono accessibili solo quando il processo JVM è attivo. La libreria cancellerà i dati al termine del processo.

4.2. Mappa persistente

A differenza di una mappa in memoria, l'implementazione salverà una mappa persistente su disco . Vediamo ora come creare una mappa persistente:

ChronicleMap persistedCountryMap = ChronicleMap .of(LongValue.class, CharSequence.class) .name("country-map") .entries(50) .averageValue("America") .createPersistedTo(new File(System.getProperty("user.home") + "/country-details.dat"));

Questo creerà un file chiamato country-details.dat nella cartella specificata. Se questo file è già disponibile nel percorso specificato, l'implementazione del builder aprirà un collegamento all'archivio dati esistente da questo processo JVM.

Possiamo fare uso della mappa persistente nei casi in cui vogliamo che:

  • sopravvivere oltre il processo creativo; ad esempio, per supportare la ridistribuzione di applicazioni a caldo
  • renderlo globale in un server; ad esempio, per supportare più accessi simultanei al processo
  • agire come un archivio di dati che salveremo sul disco

5. Configurazione delle dimensioni

È obbligatorio configurare il valore medio e la chiave media durante la creazione di una mappa cronaca, tranne nel caso in cui il nostro tipo chiave / valore sia una primitiva in scatola o un'interfaccia valore. Nel nostro esempio, non stiamo configurando la chiave media poiché il tipo di chiave LongValue è un'interfaccia valore.

Vediamo ora quali sono le opzioni per configurare il numero medio di byte chiave / valore:

  • averageValue () - Il valore da cui viene determinato il numero medio di byte da allocare per il valore di una voce della mappa
  • averageValueSize () - Il numero medio di byte da allocare per il valore di una voce della mappa
  • constantValueSizeBySample () - Il numero di byte da allocare per il valore di una voce di mappa quando la dimensione del valore è sempre la stessa
  • averageKey() – The key from which the average number of bytes to be allocated for the key of a map entry is determined
  • averageKeySize() – The average number of bytes to be allocated for the key of a map entry
  • constantKeySizeBySample() – The number of bytes to be allocated for the key of a map entry when the size of the key is always the same

6. Key and Value Types

There are certain standards that we need to follow when creating a Chronicle Map, especially when defining the key and value. The map works best when we create the key and value using the recommended types.

Here are some of the recommended types:

  • Value interfaces
  • Any class implementing Byteable interface from Chronicle Bytes
  • Any class implementing BytesMarshallable interface from Chronicle Bytes; the implementation class should have a public no-arg constructor
  • byte[] and ByteBuffer
  • CharSequence, String, and StringBuilder
  • Integer, Long, and Double
  • Any class implementing java.io.Externalizable; the implementation class should have a public no-arg constructor
  • Any type implementing java.io.Serializable, including boxed primitive types (except those listed above) and array types
  • Any other type, if custom serializers are provided

7. Querying a Chronicle Map

Chronicle Map supports single-key queries as well as multi-key queries.

7.1. Single-Key Queries

Single-key queries are the operations that deal with a single key. ChronicleMap supports all the operations from the Java Map interface and ConcurrentMap interface:

LongValue qatarKey = Values.newHeapInstance(LongValue.class); qatarKey.setValue(1); inMemoryCountryMap.put(qatarKey, "Qatar"); //... CharSequence country = inMemoryCountryMap.get(key);

In addition to the normal get and put operations, ChronicleMap adds a special operation, getUsing(), that reduces the memory footprint while retrieving and processing an entry. Let's see this in action:

LongValue key = Values.newHeapInstance(LongValue.class); StringBuilder country = new StringBuilder(); key.setValue(1); persistedCountryMap.getUsing(key, country); assertThat(country.toString(), is(equalTo("Romania"))); key.setValue(2); persistedCountryMap.getUsing(key, country); assertThat(country.toString(), is(equalTo("India")));

Here we've used the same StringBuilder object for retrieving values of different keys by passing it to the getUsing() method. It basically reuses the same object for retrieving different entries. In our case, the getUsing() method is equivalent to:

country.setLength(0); country.append(persistedCountryMap.get(key));

7.2. Multi-Key Queries

There may be use cases where we need to deal with multiple keys at the same time. For this, we can use the queryContext() functionality. The queryContext() method will create a context for working with a map entry.

Let's first create a multimap and add some values to it:

Set averageValue = IntStream.of(1, 2).boxed().collect(Collectors.toSet()); ChronicleMap
    
      multiMap = ChronicleMap .of(Integer.class, (Class
     
      ) (Class) Set.class) .name("multi-map") .entries(50) .averageValue(averageValue) .create(); Set set1 = new HashSet(); set1.add(1); set1.add(2); multiMap.put(1, set1); Set set2 = new HashSet(); set2.add(3); multiMap.put(2, set2);
     
    

To work with multiple entries, we have to lock those entries to prevent inconsistency that may occur due to a concurrent update:

try (ExternalMapQueryContext
    
      fistContext = multiMap.queryContext(1)) { try (ExternalMapQueryContext
     
       secondContext = multiMap.queryContext(2)) { fistContext.updateLock().lock(); secondContext.updateLock().lock(); MapEntry
      
        firstEntry = fistContext.entry(); Set firstSet = firstEntry.value().get(); firstSet.remove(2); MapEntry
       
         secondEntry = secondContext.entry(); Set secondSet = secondEntry.value().get(); secondSet.add(4); firstEntry.doReplaceValue(fistContext.wrapValueAsData(firstSet)); secondEntry.doReplaceValue(secondContext.wrapValueAsData(secondSet)); } } finally { assertThat(multiMap.get(1).size(), is(equalTo(1))); assertThat(multiMap.get(2).size(), is(equalTo(2))); }
       
      
     
    

8. Closing the Chronicle Map

Ora che abbiamo finito di lavorare con le nostre mappe, chiamiamo il metodo close () sui nostri oggetti mappa per rilasciare la memoria off-heap e le risorse ad essa associate:

persistedCountryMap.close(); inMemoryCountryMap.close(); multiMap.close();

Una cosa da tenere a mente qui è che tutte le operazioni sulla mappa devono essere completate prima di chiudere la mappa. In caso contrario, la JVM potrebbe bloccarsi in modo imprevisto.

9. Conclusione

In questo tutorial, abbiamo imparato a utilizzare una Chronicle Map per archiviare e recuperare coppie chiave-valore. Anche se la versione community è disponibile con la maggior parte delle funzionalità principali, la versione commerciale ha alcune funzionalità avanzate come la replica dei dati su più server e le chiamate remote.

Tutti gli esempi che abbiamo discusso qui possono essere trovati nel progetto Github.