En introduksjon til ETS-tabeller i Elixir

Når du lager et Elixir-program, må du ofte dele en stat. For eksempel viste jeg i en av mine tidligere artikler hvordan å kode en server for å utføre ulike beregninger og holde resultatet i minnet (og senere har vi sett hvordan du gjør denne serveren kollisikker ved hjelp av veiledere). Det er imidlertid et problem: Hvis du har en enkelt prosess som tar vare på staten og mange andre prosesser som får tilgang til det, kan ytelsen bli alvorlig påvirket. Dette skyldes ganske enkelt at prosessen kun kan betjene én forespørsel om gangen. 

Det er imidlertid måter å overvinne dette problemet, og i dag skal vi snakke om en av dem. Møt Erlang Term Storage-tabeller eller bare ETS-tabeller, en rask lagring i minnet som kan være vert for tilfeldig data. Som navnet antyder, ble disse tabellene innledningsvis introdusert i Erlang, men som med enhver annen Erlang-modul, kan vi også enkelt bruke dem i Elixir.

I denne artikkelen vil du:

  • Lær hvordan du oppretter ETS-tabeller og tilgjengelige alternativer ved opprettelsen.
  • Lær hvordan du utfører lese, skrive, slette og andre operasjoner.
  • Se ETS-tabeller i aksjon.
  • Lær om diskbaserte ETS-tabeller og hvordan de adskiller seg fra minnetabeller.
  • Se hvordan du konverterer ETS og DETS frem og tilbake.

Alle kodeeksempler fungerer med både Elixir 1.4 og 1.5, som nylig ble utgitt.

Introduksjon til ETS-tabeller

Som nevnt tidligere er ETS-tabeller lagret i minnet som inneholder tupler av data (kalt rader). Flere prosesser kan få tilgang til tabellen med sin id eller et navn som er representert som et atom og utføre lese, skrive, slette og andre operasjoner. ETS-tabeller opprettes ved en egen prosess, så hvis denne prosessen er avsluttet, blir tabellen ødelagt. Imidlertid er det ingen automatisk søppelinnsamlingsmekanisme, slik at bordet kan hænge ut i minnet i ganske lang tid.

Data i ETS-tabellen er representert av en tuple : nøkkel, verdi1, verdi2, verdsettelse. Du kan enkelt slå opp dataene med nøkkelen eller sette inn en ny rad, men som standard kan det ikke være to rader med samme tast. Nøkkelbaserte operasjoner er svært raske, men hvis du av en eller annen grunn trenger å lage en liste fra en ETS-tabell, og si, utfør komplekse manipulasjoner av dataene, er det også mulig.

I tillegg finnes det diskbaserte ETS-tabeller som lagrer innholdet i en fil. Selvfølgelig opererer de tregere, men på den måten får du en enkel filoppbevaring uten å måtte måtte. I tillegg kan ETS-minne enkelt konverteres til diskbasert og omvendt.

Så jeg tror det er på tide å starte reisen og se hvordan ETS-tabellene er opprettet!

Opprette en ETS-tabell

For å opprette en ETS-tabell, bruk nye / 2 funksjon. Så lenge vi bruker en Erlang-modul, bør navnet være skrevet som et atom:

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

Vær oppmerksom på at inntil nylig kan du bare opprette opptil 1400 tabeller per BEAM-forekomst, men dette er ikke tilfelle lenger - du er bare begrenset til mengden tilgjengelig minne.

Det første argumentet passerte til ny funksjonen er tabellens navn (alias), mens den andre inneholder en liste over alternativer. De cool_table variabel inneholder nå et nummer som identifiserer tabellen i systemet:

IO.inspect cool_table # => 12306

Du kan nå bruke denne variabelen til å utføre etterfølgende operasjoner til tabellen (lese og skrive data, for eksempel).

Tilgjengelige alternativer

La oss snakke om alternativene du kan spesifisere når du lager et bord. Den første (og litt merkelige) ting å merke seg er at du som standard ikke kan bruke tabellens alias på noen måte, og i utgangspunktet har den ingen effekt. Men fortsatt aliaset bli sendt på bordets skapelse.

For å få tilgang til tabellen med aliaset, må du oppgi en : named_table alternativ som dette:

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

Forresten, hvis du vil endre navn på tabellen, kan den gjøres ved hjelp av endre navn / 2 funksjon:

: ets.rename (cool_table,: cooler_table)

Deretter, som allerede nevnt, kan et bord ikke inneholde flere rader med samme nøkkel, og dette dikteres av type. Det finnes fire mulige bordtyper:

  • :sett-det er standard en. Det betyr at du ikke kan ha flere rader med nøyaktig samme nøkler. Rammene blir ikke bestilt på noen spesiell måte.
  • : ordered_set-det samme som :sett, men radene er bestilt av vilkårene.
  • :bag-flere rader kan ha samme nøkkel, men radene kan fortsatt ikke være helt identiske.
  • : duplicate_bag-rader kan være helt identiske.

Det er en ting verdt å nevne om : ordered_set tabeller. Som Erlangs dokumentasjon sier, behandler disse tabellene nøklene like like når de er sammenlign like, ikke bare når de kamp. Hva betyr det?

To termer i Erlang-kamp bare hvis de har samme verdi og samme type. Så heltall 1 matcher bare et annet heltall 1, men ikke flyte 1.0 som de har forskjellige typer. To termer er sammenlignbare likevel, dersom enten de har samme verdi og type eller hvis begge er numeriske og strekker seg til samme verdi. Dette betyr at 1 og 1.0 er sammenlign like.

For å oppgi tabellens type legger du bare til et element i listen over alternativer:

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

Et annet interessant alternativ som du kan passere er : komprimert. Det betyr at dataene inne i bordet (men ikke nøklene) vil bli gjett hva som lagres i kompakt form. Selvfølgelig vil operasjonene som utføres på bordet bli langsommere.

Deretter kan du kontrollere hvilket element i tupelen som skal brukes som nøkkel. Som standard er det første elementet (posisjon 1) brukes, men dette kan endres enkelt:

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

Nå blir de andre elementene i tuplene behandlet som nøklene.

Det siste men ikke minst alternativet styrer tabellens tilgangsrettigheter. Disse rettighetene dikterer hvilke prosesser som er tilgjengelige for å få tilgang til tabellen:

  • :offentlig-enhver prosess kan utføre enhver operasjon til bordet.
  • : beskyttet-standardverdien. Bare eierprosessen kan skrive til bordet, men alle prosessene kan lese.
  • :privat-bare eieren prosessen kan få tilgang til bordet.

Så, for å lage et bord privat, ville du skrive:

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

Ok, nok å snakke om alternativer-la oss se noen vanlige operasjoner som du kan utføre til bordene!

Skriv operasjoner

For å lese noe fra bordet må du først skrive noen data der, så la oss starte med sistnevnte operasjon. Bruke sette inn / 2 funksjon for å sette data inn i bordet:

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

Du kan også sende en liste over tuples som dette:

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

Merk at hvis tabellen har en type :sett og en ny nøkkel samsvarer med en eksisterende, blir de gamle data overskrevet. Tilsvarende, hvis et bord har en type : ordered_set og en ny nøkkel sammenligner med den gamle, blir dataene overskrevet, så vær oppmerksom på dette.

Innsatsoperasjonen (selv med flere tupler på en gang) er garantert å være atomisk og isolert, noe som betyr at alt er lagret i bordet eller ingenting i det hele tatt. Også andre prosesser vil ikke kunne se mellomresultatet fra operasjonen. Alt i alt er dette ganske lik SQL-transaksjoner.

Hvis du er bekymret for duplisering av nøkler eller ikke vil overskrive dataene dine ved et uhell, bruk insert_new / 2 fungere i stedet. Det ligner på sette inn / 2 men vil aldri sette inn dupliserende nøkler og vil i stedet returnere falsk. Dette er tilfellet for :bag og : duplicate_bag bord også:

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

Hvis du oppgir en liste over tuples, vil hver nøkkel bli sjekket, og operasjonen vil bli kansellert selv om en av tastene er duplisert.

Les Operasjoner

Flott, nå har vi noen data i bordet vårt - hvordan henter vi dem? Den enkleste måten er å utføre oppslag med en nøkkel:

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

Husk at for : ordered_set tabell, nøkkelen skal sammenlignes med den angitte verdien. For alle andre bordtyper bør den matche. Også, hvis et bord er en :bag eller en : ordered_bag, de oppslag / 2 funksjonen kan returnere en liste med flere elementer:

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

I stedet for å hente en liste kan du hente et element i ønsket posisjon ved hjelp av lookup_element / 3 funksjon:

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

I denne koden får vi raden under nøkkelen :Nummer og deretter ta elementet i den andre posisjonen. Det fungerer også perfekt med :bag eller : duplicate_bag:

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

Hvis du bare vil sjekke om noen nøkkel er til stede i tabellen, bruk medlem / 2, som returnerer heller ekte eller falsk:

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

Du kan også få den første eller siste nøkkelen i et bord ved å bruke første / 1 og siste / 1 henholdsvis:

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

Dessuten er det mulig å bestemme forrige eller neste nøkkel basert på den medfølgende. Hvis en slik nøkkel ikke kan bli funnet, : "$ End_of_table" vil bli returnert:

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 "

Vær imidlertid oppmerksom på at tabellen traverserer ved hjelp av funksjoner som først, neste, siste eller prev er ikke isolert. Det betyr at en prosess kan fjerne eller legge til flere data i tabellen mens du er iterating over det. En måte å løse dette problemet på er å bruke safe_fixtable / 2, som fikserer bordet og sikrer at hvert element bare hentes en gang. Tabellen forblir løst med mindre prosessen frigjør det:

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]: ets.safe_fixtable (cool_table, false) # => tabellen er utgitt på dette tidspunktet: ets.info (cool_table,: safe_fixed_monotonic_time) |> IO.inspect # => false

Til slutt, hvis du vil finne et element i tabellen og fjerne det, bruker du ta / 2 funksjon:

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 # => []

Slett operasjoner

Ok, så nå la oss si at du ikke lenger trenger bordet og ønsker å bli kvitt det. Bruk slette / 1 for det:

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

Selvfølgelig kan du slette en rad (eller flere rader) med nøkkelen også:

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

For å fjerne hele bordet, bruk delete_all_objects / 1:

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

Og til slutt, for å finne og fjerne et bestemt objekt, bruk 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]

Konvertering av tabellen

En ETS-tabell kan konverteres til en liste når som helst ved å bruke tab2list / 1 funksjon:

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]

Husk at henting av data fra bordet med tastene er en veldig rask operasjon, og du bør holde fast ved det hvis det er mulig.

Du kan også dumpe bordet til en fil ved hjelp av 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

Legg merke til at det andre argumentet skal være en charlist (en enkeltnotert streng).

Det finnes en håndfull andre operasjoner som kan brukes på ETS-tabellene, og selvfølgelig skal vi ikke diskutere dem alle. Jeg anbefaler virkelig å skumre gjennom Erlang-dokumentasjonen på ETS for å lære mer.

Fortsetter staten med ETS

For å oppsummere fakta som vi har lært så langt, la oss endre et enkelt program som jeg har presentert i artikkelen min om GenServer. Dette er en modul som kalles CalcServer som lar deg utføre ulike beregninger ved å sende forespørsler til serveren eller hente resultatet:

defmodule CalcServer bruker GenServer def start (initial_value) gjør GenServer.start (__ MODULE__, initial_value, navn: __MODULE__) avslutt def init (initial_value) når is_number (initial_value) gjør : ok, initial_value stopp, "Verdien må være et heltall!" End def sqrt gjør GenServer.cast (__ MODULE__,: sqrt) ende def add (tall) gjør GenServer.cast (__ MODULE__, : add, number) ende def multiplisere ) gjør GenServer.cast (__ MODULE__, : multiply, nummer) ende def div (nummer) gjør GenServer.cast (__ MODULE__, : div, nummer) ende def resultat gjør GenServer.call (__ MODULE__,: result) end def handle_call (: result, _ state) gjør : svar, state, state ende def handle_cast (operasjon, state) gjør saksoperasjon gjør: sqrt -> : noreply: math.sqrt (state)  multiplikator -> : noreply, stat * multiplikator : div, tall -> : noreply, state / number : add, number -> : noreply, state + nummer _ -> , "Ikke implementert", state ende ende def terminate (_reason, _state) gjør IO.puts "Serverterminalen ted "slutten CalcServer.start (6.1) CalcServer.sqrt CalcServer.multiply (2) CalcServer.result |> IO.puts # => 4.9396356140913875

For øyeblikket støtter ikke serveren vår alle matematiske operasjoner, men du kan utvide den etter behov. Også, min andre artikkel forklarer hvordan man konverterer denne modulen til et program og dra nytte av veiledere for å ta vare på serveren krasjer.

Det jeg vil gjerne gjøre nå, er å legge til en annen funksjon: evnen til å logge alle matematiske operasjoner som ble utført sammen med det overførte argumentet. Disse operasjonene lagres i en ETS-tabell slik at vi senere kan hente den.

Først av alt, endre i det fungere slik at et nytt navngitt privat bord med en type : duplicate_bag er skapt. Vi bruker : duplicate_bag fordi to identiske operasjoner med samme argument kan utføres:

 def init (initial_value) når is_number (initial_value) gjør: ets.new (: calc_log, [: duplicate_bag,: private,: named_table]) : ok, initial_value slutt

Nå juster du handle_cast tilbakeringing slik at den logger den forespurte operasjonen, utarbeider en formel, og utfører deretter den faktiske beregningen:

 def handle_cast (drift, state) gjør operasjon |> prepare_and_log |> beregne (state) end

Her er prepare_and_log privat funksjon:

 defp prepare_and_log (operasjon) gjør drift |> logg saksoperasjon gjør: sqrt -> fn (current_value) ->: math.sqrt (current_value) ende : multiply, tall -> fn (current_value) -> current_value * : div, tall -> fn (nåværende verdi) -> nåværende verdi / nummer ende : legg til, tall -> fn (nåværende verdi) -> nåværende_verdien + tallendene _ -> null ende

Vi logger operasjonen med en gang (den tilsvarende funksjonen presenteres på et øyeblikk). Deretter returnerer du den aktuelle funksjonen eller nil hvis vi ikke vet hvordan man skal håndtere operasjonen.

Når det gjelder Logg funksjon, bør vi enten støtte en tuple (som inneholder både operasjonens navn og argumentet) eller et atom (som bare inneholder operasjonens navn, for eksempel, : sqrt):

 def log (operasjon) når is_tuple (operasjon) gjør: ets.insert (: calc_log, operasjon) ende def log (operasjon) når is_atom (operasjon) gjør: ets.insert (: calc_log, operasjon, null) slutten def log (_) gjør: ets.insert (: calc_log, : unsupported_operation, null) slutten

Deretter regne ut funksjon, som enten returnerer et riktig resultat eller en stoppmelding:

 defp beregne (func state) når is_function (func) gjør : noreply, func. (state) ende defp beregne (_func, state) gjør : stop, "Ikke implementert", state end

Til slutt, la oss presentere en ny grensesnittfunksjon for å hente alle utførte operasjoner av deres type:

 def operasjoner (type) gjør GenServer.call (__ MODULE__, : operations, type) slutt

Håndter anropet:

 def handle_call (: operasjoner, type, _, state) gjør : svar, fetch_operations_by (type), state end

Og utfør selve oppslaget:

 defp fetch_operations_by (type) gjør: ets.lookup (: calc_log, type) ende

Test nå alt:

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. inspiser # => [legg til: 1, legg til: 2]

Resultatet er riktig fordi vi har utført to :Legg til operasjoner med argumentene 1 og 2. Selvfølgelig kan du videre utvide dette programmet slik det passer best. Likevel, misbruk ikke ETS-tabeller, og bruk dem når det virkelig kommer til å øke ytelsen. I mange tilfeller er det å bruke immutables en bedre løsning.

Disk ETS

Før innpakning av denne artikkelen ville jeg si et par ord om diskbaserte ETS-tabeller eller bare DETS. 

DETS er ganske lik ETS: de bruker tabeller for å lagre ulike data i form av tuples. Forskjellen, som du har gjettet, er at de stole på lagringsplass i stedet for minne og har færre funksjoner. DETS har funksjoner som ligner de som vi diskuterte ovenfor, men enkelte operasjoner utføres litt annerledes.

For å åpne et bord, må du bruke enten open_file / 1 eller open_file / 2-det er ingen nye / 2 fungere som i : ets modul. Siden vi ikke har noen eksisterende tabell ennå, la oss holde oss til open_file / 2, som skal lage en ny fil for oss:

: dets.open_file (: file_table, [])

Filnavnet er som tabellens navn som standard, men dette kan endres. Det andre argumentet gikk til åpen fil er listen over alternativer skrevet i form av tuples. Det finnes en håndfull tilgjengelige alternativer som :adgang eller : auto_save. For eksempel, for å endre et filnavn, bruk følgende alternativ:

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

Legg merke til at det også er a :type alternativ som kan ha en av følgende verdier:

  • :sett
  • :bag
  • : duplicate_bag

Disse typene er de samme som for ETS. Vær oppmerksom på at DETS ikke kan ha en type : ordered_set.

Det er ingen : named_table alternativet, slik at du alltid kan bruke tabellens navn for å få tilgang til det.

En annen ting å nevne er at DETS-tabellene må være ordentlig lukket:

: Dets.close (: file_table)

Hvis du ikke gjør dette, repareres bordet neste gang det åpnes.

Du utfører lese- og skriveoperasjoner som du gjorde med 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)

Vær imidlertid oppmerksom på at DETS er tregere enn ETS fordi Elixir trenger tilgang til disken som selvfølgelig tar lengre tid.

Merk at du kan konvertere ETS og DETS tabeller frem og tilbake med letthet. For eksempel, la oss bruke to_ets / 2 og kopier innholdet i DETS-tabellen i minnet:

: 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]

Kopier ETS innhold til DETS bruk 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)

For å oppsummere er diskbasert ETS en enkel måte å lagre innhold på i filen, men denne modulen er litt mindre kraftig enn ETS, og operasjonene er også langsommere.

Konklusjon

I denne artikkelen har vi snakket om ETS og diskbaserte ETS-tabeller som tillater oss å lagre vilkårlig vilkår i minnet og i henholdsvis filer. Vi har sett hvordan du lager slike tabeller, hvilke tilgjengelige typer, hvordan du utfører lese- og skriveoperasjoner, hvordan du ødelegger tabeller og hvordan du konverterer dem til andre typer. Du kan finne mer informasjon om ETS i Elixir-veiledningen og på Erlangs offisielle side.

Igjen, ikke bruk ETS-tabellene for mye, og prøv å holde fast med immutables hvis det er mulig. I noen tilfeller kan ETS imidlertid være en fin ytelse, så det er nyttig å vite om denne løsningen. 

Forhåpentligvis har du hatt glede av denne artikkelen. Som alltid, takk for at du bodde hos meg, og ser deg veldig snart!