Un'introduzione alle tabelle ETS in elisir

Quando realizzi un programma Elixir, devi spesso condividere uno stato. Ad esempio, in uno dei miei articoli precedenti ho mostrato come codificare un server per eseguire vari calcoli e mantenere il risultato in memoria (e in seguito abbiamo visto come rendere questo server a prova di proiettile con l'aiuto dei supervisori). Tuttavia, esiste un problema: se si dispone di un singolo processo che si occupa dello stato e di molti altri processi che vi accedono, le prestazioni potrebbero essere seriamente compromesse. Questo è semplicemente perché il processo può servire solo una richiesta alla volta. 

Tuttavia, ci sono modi per superare questo problema e oggi ne parleremo uno. Incontra i tavoli Erlang Term Storage o semplicemente Tavoli ETS, una memoria veloce in memoria che può ospitare tuple di dati arbitrari. Come suggerisce il nome, queste tabelle sono state inizialmente introdotte in Erlang ma, come con qualsiasi altro modulo di Erlang, possiamo facilmente utilizzarle anche in elisir.

In questo articolo dovrai:

  • Scopri come creare tabelle ETS e opzioni disponibili al momento della creazione.
  • Scopri come eseguire operazioni di lettura, scrittura, eliminazione e altre operazioni.
  • Vedi le tabelle ETS in azione.
  • Informazioni sulle tabelle ETS basate su disco e sulla loro differenza rispetto alle tabelle in memoria.
  • Vedi come convertire ETS e DETS avanti e indietro.

Tutti gli esempi di codice funzionano con Elixir 1.4 e 1.5, che è stato rilasciato di recente.

Introduzione alle tabelle ETS

Come accennato in precedenza, le tabelle ETS sono storage in memoria che contengono tuple di dati (chiamate righe). Più processi possono accedere alla tabella tramite il suo id o un nome rappresentato come un atomo ed eseguire operazioni di lettura, scrittura, cancellazione e altre operazioni. Le tabelle ETS sono create da un processo separato, quindi se questo processo viene terminato, la tabella viene distrutta. Tuttavia, non esiste un meccanismo automatico di garbage collection, quindi la tabella potrebbe rimanere in sospeso per un po 'di tempo.

I dati nella tabella ETS sono rappresentati da una tupla : chiave, valore1, valore2, valore. Puoi facilmente cercare i dati con la sua chiave o inserire una nuova riga, ma per impostazione predefinita non ci possono essere due righe con la stessa chiave. Le operazioni basate sulle chiavi sono molto veloci, ma se per qualche ragione hai bisogno di produrre una lista da una tabella ETS e, ad esempio, eseguire manipolazioni complesse dei dati, è anche possibile.

Inoltre, sono disponibili tabelle ETS basate su disco che memorizzano i loro contenuti in un file. Certo, funzionano più lentamente, ma in questo modo si ottiene una semplice archiviazione di file senza problemi. Inoltre, l'ETS in memoria può essere facilmente convertito su disco e viceversa.

Quindi, penso che sia il momento di iniziare il nostro viaggio e vedere come vengono create le tabelle ETS!

Creazione di una tabella ETS

Per creare una tabella ETS, impiegare il nuova / 2 funzione. Finché utilizziamo un modulo Erlang, il suo nome dovrebbe essere scritto come un atomo:

cool_table =: ets.new (: cool_table, [])

Nota che fino a poco tempo fa puoi creare solo fino a 1.400 tabelle per istanza di BEAM, ma questo non è più il caso: sei limitato solo alla quantità di memoria disponibile.

Il primo argomento è passato al nuovo la funzione è il nome della tabella (alias), mentre la seconda contiene un elenco di opzioni. Il cool_table la variabile ora contiene un numero che identifica la tabella nel sistema:

IO.inspect cool_table # => 12306

Ora puoi usare questa variabile per eseguire operazioni successive alla tabella (leggi e scrivi dati, per esempio).

Opzioni disponibili

Parliamo delle opzioni che puoi specificare quando crei una tabella. La prima (e piuttosto strana) cosa da notare è che di default non si può usare l'alias della tabella in alcun modo, e in pratica non ha alcun effetto. Ma ancora, lo pseudonimo dovere essere passati sulla creazione del tavolo.

Per poter accedere alla tabella con il suo alias, devi fornire a : named_table opzione come questa:

cool_table =: ets.new (: cool_table, [: named_table])

A proposito, se vuoi rinominare il tavolo, puoi farlo usando il rinomina / 2 funzione:

: ets.rename (cool_table,: cooler_table)

Successivamente, come già accennato, una tabella non può contenere più righe con la stessa chiave, e questo è dettato dal genere. Esistono quattro possibili tipi di tabella:

  • :impostato-quello è quello predefinito. Significa che non puoi avere più righe con esattamente le stesse chiavi. Le righe non vengono riordinate in alcun modo particolare.
  • : ordered_set-lo stesso di :impostato, ma le righe sono ordinate secondo i termini.
  • :Borsa-più righe possono avere la stessa chiave, ma le righe non possono ancora essere completamente identiche.
  • : duplicate_bag-le righe possono essere completamente identiche.

C'è una cosa che vale la pena menzionare per il : ordered_set tabelle. Come dice la documentazione di Erlang, queste tabelle trattano le chiavi quando sono uguali confronta uguale, non solo quando loro incontro. Cosa significa?

Due termini in Erlang corrispondono solo se hanno lo stesso valore e lo stesso tipo. Così intero 1 corrisponde solo a un altro intero 1, ma non galleggiare 1.0 come hanno diversi tipi. Due termini sono uguali, tuttavia, se hanno lo stesso valore e tipo o se entrambi sono numeri e si estendono allo stesso valore. Ciò significa che 1 e 1.0 sono paragonabili.

Per fornire il tipo di tabella, aggiungi semplicemente un elemento all'elenco di opzioni:

cool_table =: ets.new (: cool_table, [: named_table,: ordered_set])

Un'altra opzione interessante che puoi passare è : compressa. Significa che i dati all'interno della tabella (ma non le chiavi) saranno-indovina quale-archiviato in una forma compatta. Naturalmente, le operazioni eseguite sul tavolo diventeranno più lente.

Successivamente, è possibile controllare quale elemento della tupla deve essere usato come chiave. Di default, il primo elemento (posizione 1) è usato, ma questo può essere cambiato facilmente:

cool_table =: ets.new (: cool_table, [: keypos, 2]))

Ora i secondi elementi nelle tuple saranno trattati come le chiavi.

L'ultima opzione, ma non meno importante, controlla i diritti di accesso della tabella. Questi diritti determinano quali processi sono in grado di accedere alla tabella:

  • :pubblico-qualsiasi processo può eseguire qualsiasi operazione sul tavolo.
  • : protetto-il valore predefinito. Solo il processo proprietario può scrivere sulla tabella, ma tutti i processi possono leggere.
  • :privato-solo il processo proprietario può accedere alla tabella.

Quindi, per rendere privato un tavolo, dovresti scrivere:

cool_table =: ets.new (: cool_table, [: private])

Va bene, basta parlare delle opzioni: vediamo alcune operazioni comuni che è possibile eseguire sui tavoli!

Scrivi operazioni

Per leggere qualcosa dalla tabella, devi prima scrivere alcuni dati lì, quindi iniziamo con quest'ultima operazione. Utilizzare il inserire / 2 funzione per inserire i dati nella tabella:

cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, : number, 5)

Puoi anche passare un elenco di tuple come questo:

: ets.insert (cool_table, [: number, 5, : string, "test"])

Si noti che se la tabella ha un tipo di :impostato e una nuova chiave corrisponde a una esistente, i vecchi dati verranno sovrascritti. Allo stesso modo, se un tavolo ha un tipo di : ordered_set e una nuova chiave è paragonabile a quella vecchia, i dati verranno sovrascritti, quindi fai attenzione a questo.

L'operazione di inserimento (anche con più tuple contemporaneamente) è garantita per essere atomica e isolata, il che significa che o tutto è memorizzato nella tabella o niente del tutto. Inoltre, altri processi non saranno in grado di vedere il risultato intermedio dell'operazione. Tutto sommato, è abbastanza simile alle transazioni SQL.

Se sei preoccupato di duplicare le chiavi o non vuoi sovrascrivere i tuoi dati per errore, usa il insert_new / 2 funzione invece. È simile a inserire / 2 ma non inserirà mai chiavi duplicate e ritornerà invece falso. Questo è il caso per il :Borsa e : duplicate_bag tavoli pure:

cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, : number, 5): ets.insert_new (cool_table, : number, 6) |> IO.inspect # = > falso

Se si fornisce un elenco di tuple, ciascuna chiave verrà controllata e l'operazione verrà annullata anche se una delle chiavi è duplicata.

Leggi le operazioni

Bene, ora abbiamo alcuni dati nella nostra tabella: come li recuperiamo? Il modo più semplice è eseguire la ricerca con una chiave:

: ets.insert (cool_table, : number, 5) IO.inspect: ets.lookup (cool_table,: number) # => [numero: 5]

Ricordalo per il : ordered_set tabella, la chiave dovrebbe essere uguale al valore fornito. Per tutti gli altri tipi di tabella, dovrebbe corrispondere. Inoltre, se un tavolo è un :Borsa o un : ordered_bag, il ricerca / 2 la funzione può restituire una lista con più elementi:

cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: number, 5, : number, 6]) IO.inspect: ets.lookup (cool_table,: number ) # => [numero: 5, numero: 6]

Invece di recuperare una lista, puoi prendere un elemento nella posizione desiderata usando il lookup_element / 3 funzione:

cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, : number, 6) IO.inspect: ets.lookup_element (cool_table,: number, 2) # => 6

In questo codice, otteniamo la riga sotto la chiave :numero e poi prendendo l'elemento nella seconda posizione. Funziona perfettamente anche con :Borsa o : duplicate_bag:

cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: number, 5, : number, 6]) IO.inspect: ets.lookup_element (cool_table,: number , 2) # => 5,6

Se si desidera semplicemente verificare se è presente una chiave nella tabella, utilizzare membro / 2, che restituisce entrambi vero o falso:

cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: number, 5, : number, 6]) if: ets.member (cool_table,: number) do IO.inspect: ets.lookup_element (cool_table,: number, 2) # => 5,6 end

Puoi anche ottenere la prima o l'ultima chiave in una tabella usando primo / 1 e ultima / 1 rispettivamente:

cool_table =: ets.new (: cool_table, [: ordered_set]): ets.insert (cool_table, [: b, 3, : a, 100]): ets.last (cool_table) |> IO.inspect # =>: b: ets.first (cool_table) |> IO.inspect # =>: a

Inoltre, è possibile determinare la chiave precedente o successiva in base a quella fornita. Se tale chiave non può essere trovata, : "$ End_of_table" sarà restituito:

cool_table =: ets.new (: cool_table, [: ordered_set]): ets.insert (cool_table, [: b, 3, : a, 100]): ets.prev (cool_table,: b) |> IO.inspect # =>: a: ets.next (cool_table,: a) |> IO.inspect # =>: b: ets.prev (cool_table,: a) |> IO.inspect # =>: "$ end_of_table "

Si noti, tuttavia, che l'attraversamento della tabella utilizzando funzioni come primo, Il prossimo, scorso o prev non è isolato Significa che un processo può rimuovere o aggiungere più dati alla tabella mentre si sta iterando su di esso. Un modo per superare questo problema è usando safe_fixtable / 2, che corregge la tabella e garantisce che ogni elemento venga recuperato una sola volta. La tabella rimane fissa a meno che il processo non la rilasci:

cool_table =: ets.new (: cool_table, [: bag]): ets.safe_fixtable (cool_table, true): ets.info (cool_table,: safe_fixed_monotonic_time) |> IO.inspect # => 256000, [#PID<0.69.0>, 1]: la tabella ets.safe_fixtable (cool_table, false) # => viene rilasciata a questo punto: ets.info (cool_table,: safe_fixed_monotonic_time) |> IO.inspect # => false

Infine, se desideri trovare un elemento nella tabella e rimuoverlo, utilizza il comando prendere / 2 funzione:

cool_table =: ets.new (: cool_table, [: ordered_set]): ets.insert (cool_table, [: b, 3, : a, 100]): ets.take (cool_table,: b) |> IO.inspect # => [b: 3]: ets.take (cool_table,: b) |> IO.inspect # => []

Elimina operazioni

Okay, quindi ora diciamo che non hai più bisogno del tavolo e desideri liberartene. Uso cancellare / 1 per quello:

cool_table =: ets.new (: cool_table, [: ordered_set]): ets.delete (cool_table)

Naturalmente, puoi cancellare una riga (o più righe) anche con la sua chiave:

cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, [: b, 3, : a, 100]): ets.delete (cool_table,: a)

Per cancellare l'intero tavolo, utilizzare delete_all_objects / 1:

cool_table =: ets.new (: cool_table, []): ets.insert (cool_table, [: b, 3, : a, 100]): ets.delete_all_objects (cool_table)

E, infine, per trovare e rimuovere un oggetto specifico, utilizzare delete_object / 2:

cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: a, 3, : a, 100]): ets.delete_object (cool_table, : a, 3 ): ets.lookup (cool_table,: a) |> IO.inspect # => [a: 100]

Convertire la tabella

Una tabella ETS può essere convertita in un elenco in qualsiasi momento usando il tab2list / 1 funzione:

cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: a, 3, : a, 100]): ets.tab2list (cool_table) |> IO.inspect # => [a: 3, a: 100]

Ricorda, tuttavia, che scaricare i dati dalla tabella con le chiavi è un'operazione molto veloce, e dovresti attenervisi se possibile.

Puoi anche scaricare la tua tabella su un file usando tab2file / 2:

cool_table =: ets.new (: cool_table, [: bag]): ets.insert (cool_table, [: a, 3, : a, 100]): ets.tab2file (cool_table, 'cool_table.txt' ) |> IO.inspect # =>: ok

Si noti che il secondo argomento dovrebbe essere una charlist (una stringa a virgolette singole).

Ci sono una manciata di altre operazioni disponibili che possono essere applicate alle tabelle ETS e, naturalmente, non le discuteremo tutte. Consiglio vivamente di sfogliare la documentazione di Erlang su ETS per saperne di più.

Persistere lo stato con ETS

Per riassumere i fatti che abbiamo imparato finora, modifichiamo un semplice programma che ho presentato nel mio articolo su GenServer. Questo è un modulo chiamato CalcServer che consente di eseguire vari calcoli inviando richieste al server o recuperando il risultato:

defmodule CalcServer usa GenServer def start (initial_value) do GenServer.start (__ MODULE__, initial_value, name: __MODULE__) end def init (initial_value) quando is_number (initial_value) do : ok, initial_value end def init (_) do : stop, "Il valore deve essere un numero intero!" end def sqrt do GenServer.cast (__ MODULE__,: sqrt) end def add (numero) do GenServer.cast (__ MODULE__, : add, number) end def moltiplicato (numero ) GenServer.cast (__ MODULE__, : moltiplicazione, numero) end def div (numero) do GenServer.cast (__ MODULE__, : div, numero) end def result do GenServer.call (__ MODULE__,: result) end def handle_call (: result, _, state) do : reply, state, state end def handle_cast (operazione, stato) do case operation do: sqrt -> : noreply,: math.sqrt (stato) : multiply, moltiplicatore -> : noreply, stato * moltiplicatore : div, numero -> : noreply, stato / numero : aggiungi, numero -> : noreply, stato + numero _ -> : stop , "Non implementato", stato end end def terminate (_reason, _state) do IO.puts "Il server termina ted "fine fine CalcServer.start (6.1) CalcServer.sqrt CalcServer.multiply (2) CalcServer.result |> IO.puts # => 4.9396356140913875

Attualmente il nostro server non supporta tutte le operazioni matematiche, ma è possibile estenderlo secondo necessità. Inoltre, il mio altro articolo spiega come convertire questo modulo in un'applicazione e sfruttare i supervisori per occuparsi dei crash del server.

Quello che mi piacerebbe fare ora è aggiungere un'altra funzionalità: la possibilità di registrare tutte le operazioni matematiche che sono state eseguite insieme all'argomento passato. Queste operazioni saranno memorizzate in una tabella ETS in modo che saremo in grado di recuperarla in seguito.

Prima di tutto, modifica il dentro funzione in modo tale che una nuova tabella privata con un tipo di nome : duplicate_bag è creato. Stiamo usando : duplicate_bag perché possono essere eseguite due operazioni identiche con lo stesso argomento:

 def init (initial_value) quando is_number (initial_value) do: ets.new (: calc_log, [: duplicate_bag,: private,: named_table]) : ok, initial_value end

Ora modificare il handle_cast callback in modo che registri l'operazione richiesta, prepari una formula e quindi esegua il calcolo effettivo:

 def handle_cast (operazione, stato) do operazione |> prepare_and_log |> calcola (stato) fine

Ecco il prepare_and_log funzione privata:

 defp prepare_and_log (operazione) do operazione |> log case operation do: sqrt -> fn (current_value) ->: math.sqrt (current_value) end : multiply, number -> fn (current_value) -> current_value * numero end  : div, number -> fn (current_value) -> current_value / number end : add, number -> fn (current_value) -> current_value + number end _ -> nil end end

Stiamo registrando l'operazione immediatamente (la funzione corrispondente verrà presentata in un momento). Quindi restituire la funzione appropriata o zero se non sappiamo come gestire l'operazione.

Per quanto riguarda la ceppo funzione, dovremmo supportare una tupla (contenente sia il nome dell'operazione che l'argomento) o un atomo (contenente solo il nome dell'operazione, ad esempio, : sqrt):

 def log (operation) quando is_tuple (operation) do: ets.insert (: calc_log, operation) end def log (operation) quando is_atom (operation) do: ets.insert (: calc_log, operation, nil) end def log (_) do: ets.insert (: calc_log, : unsupported_operation, nil) end

Quindi, il calcolare funzione, che restituisce un risultato corretto o un messaggio di arresto:

 defp calculate (func, state) quando is_function (func) do : noreply, func. (stato) end defp calculate (_func, state) do : stop, "Non implementato", stato fine

Infine, presentiamo una nuova funzione di interfaccia per recuperare tutte le operazioni eseguite dal loro tipo:

 operazioni def (type) fanno GenServer.call (__ MODULE__, : operations, type) end

Gestire la chiamata:

 def handle_call (: operazioni, tipo, _, stato) do : reply, fetch_operations_by (type), state end

Ed esegui la ricerca effettiva:

 defp fetch_operations_by (type) do: ets.lookup (: calc_log, type) end

Ora prova tutto:

CalcServer.start (6.1) CalcServer.sqrt CalcServer.add (1) CalcServer.multiply (2) CalcServer.add (2) CalcServer.result |> IO.inspect # => 8.939635614091387 CalcServer.operations (: add) |> IO. inspect # => [aggiungi: 1, aggiungi: 2]

Il risultato è corretto perché ne abbiamo eseguiti due :Inserisci operazioni con gli argomenti 1 e 2. Naturalmente, puoi estendere ulteriormente questo programma come meglio credi. Tuttavia, non abusare delle tabelle ETS e utilizzarle quando aumenterà le prestazioni, in molti casi, l'uso di immutables è una soluzione migliore.

Disco ETS

Prima di concludere questo articolo, volevo dire un paio di parole sulle tabelle ETS basate su disco o semplicemente DETS. 

I DET sono molto simili a ETS: usano tabelle per memorizzare vari dati sotto forma di tuple. La differenza, come hai intuito, è che si basano sull'archiviazione di file anziché sulla memoria e hanno meno funzioni. I DET hanno funzioni simili a quelle che abbiamo discusso sopra, ma alcune operazioni sono eseguite in modo leggermente diverso.

Per aprire un tavolo, è necessario utilizzare entrambi open_file / 1 o open_file / 2-Non c'è nuova / 2 funzione come nel : ETS modulo. Dato che non abbiamo ancora nessun tavolo esistente, restiamo fedeli open_file / 2, che sta per creare un nuovo file per noi:

: dets.open_file (: file_table, [])

Il nome file è uguale al nome della tabella per impostazione predefinita, ma può essere modificato. Il secondo argomento è passato al apri il file è la lista di opzioni scritte sotto forma di tuple. Ci sono una manciata di opzioni disponibili come :accesso o : auto_save. Ad esempio, per modificare un nome file, utilizzare la seguente opzione:

: dets.open_file (: file_table, [: file, 'cool_table.txt'])

Nota che c'è anche un :genere opzione che può avere uno dei seguenti valori:

  • :impostato
  • :Borsa
  • : duplicate_bag

Questi tipi sono gli stessi dell'Ets. Si noti che DETS non può avere un tipo di : ordered_set.

Non c'è : named_table opzione, in modo da poter sempre utilizzare il nome della tabella per accedervi.

Un'altra cosa degna di nota è che le tabelle DETS devono essere correttamente chiuse:

: Dets.close (: file_table)

Se non lo fai, la tabella verrà riparata alla successiva apertura.

Esegui operazioni di lettura e scrittura proprio come hai fatto con ETS:

: dets.open_file (: file_table, [: file, 'cool_table.txt']): dets.insert (: file_table, : a, 3): dets.lookup (: file_table,: a) |> IO .inspect # => [a: 3]: dets.close (: file_table)

Tenete a mente, però, che i DETS sono più lenti di ETS perché Elixir avrà bisogno di accedere al disco che, ovviamente, richiede più tempo.

Si noti che è possibile convertire le tabelle ETS e DETS avanti e indietro con facilità. Ad esempio, usiamo to_ets / 2 e copia il contenuto della nostra tabella DETS in memoria:

: dets.open_file (: file_table, [: file, 'cool_table.txt']): dets.insert (: file_table, : a, 3) my_ets =: ets.new (: my_ets, []): dets.to_ets (: file_table, my_ets): dets.close (: file_table): ets.lookup (my_ets,: a) |> IO.inspect # => [a: 3]

Copia i contenuti dell'ETS su DETS utilizzando to_dets / 2:

my_ets =: ets.new (: my_ets, []): ets.insert (my_ets, : a, 3): dets.open_file (: file_table, [: file, 'cool_table.txt']): ets .to_dets (my_ets,: file_table): dets.lookup (: file_table,: a) |> IO.inspect # => [a: 3]: dets.close (: file_table)

Per riassumere, l'ETS basato su disco è un modo semplice per memorizzare i contenuti nel file, ma questo modulo è leggermente meno potente di ETS e anche le operazioni sono più lente.

Conclusione

In questo articolo, abbiamo parlato di ETS e delle tabelle ETS basate su disco che ci consentono di archiviare termini arbitrari rispettivamente nella memoria e nei file. Abbiamo visto come creare tali tabelle, quali sono i tipi disponibili, come eseguire operazioni di lettura e scrittura, come distruggere le tabelle e come convertirle in altri tipi. Puoi trovare maggiori informazioni su ETS nella guida Elixir e nella pagina ufficiale di Erlang.

Ancora una volta, non esagerare con le tabelle ETS e, se possibile, provare ad attaccare con immutables. In alcuni casi, tuttavia, l'ETS può essere un buon incremento delle prestazioni, quindi conoscere questa soluzione è utile in ogni caso. 

Spero che questo articolo ti sia piaciuto. Come sempre, grazie per essere stato con me e arrivederci a presto!