Do you want to pick up from where you left of?
Take me there

Erlang Term Storage (ETS)

Η Erlang Term Storage, γνωστό και ως ETS, είναι μια δυνατή μηχανή αποθήκευσης ενσωματωμένη στο OTP και διαθέσιμη για χρήση στην Elixir. Σε αυτό το μάθημα θα δούμε πως να αλληλεπιδρούμε με την ETS και πως μπορεί να χρησιμοποιηθεί στις εφαρμογές μας.

Επισκόπηση

Η ETS είναι μια προεγκατεστημένη ρωμαλέα αποθήκη μνήμης για αντικείμενα Elixir και Erlang . Η ETS είναι ικανή για αποθήκευση πολύ μεγάλου όγκου δεδομένων και προσφέρει πρόσβαση στα δεδομένα σε σταθερό χρόνο.

Οι πίνακες στην ETS δημιουργούνται και ανήκουν σε ξεχωριστές διεργασίες. Όταν μια διεργασία ιδιοκτήτης τερματίζεται, οι πίνακές της καταστρέφονται. Μπορείτε να έχετε όσους πίνακες ETS θέλετε, το μόνο που σας περιορίζει είναι η μνήμη του σέρβερ. Ένα όριο μπορεί να προσδιοριστεί με την χρήση της μεταβλητής περιβάλλοντος ERL_MAX_ETS_TABLES.

Δημιουργία Πινάκων

Οι πίνακες δημιουργούνται με την new/2, η οποία δέχεται το όνομα πίνακα και ένα σετ επιλογών, και επιστρέφει ένα χαρακτηριστικό πίνακα το οποίο μπορεί να χρησιμοποιηθεί σε επόμενες εργασίες.

Για το παράδειγμά μας θα δημιουργήσουμε ένα πίνακα για να αποθηκεύσουμε και αναζητούμε χρήστες με το ψευδώνυμό τους:

iex> table = :ets.new(:user_lookup, [:set, :protected])
8212

Όπως οι GenServers, υπάρχει τρόπος να έχουμε πρόσβαση σε πίνακες ETS με το όνομα αντί του χαρακτηριστικού. Για να το κάνουμε αυτό θα συμπεριλάβουμε την επιλογή :named_table. Τότε μπορούμε να έχουμε πρόσβαση στον πίνακα κατευθείαν με το όνομα:

iex> :ets.new(:user_lookup, [:set, :protected, :named_table])
:user_lookup

Τύποι Πινάκων

Υπάρχουν τέσσερις τύποι πινάκων διαθέσιμοι στην ETS:

Χειριστές Πρόσβασης

Οι χειριστές πρόσβασης στην ETS είναι παρόμοιοι με τους χεριστές πρσόβασης μέσα στις ενότητες:

Race Conditions

Αν περισσότερες απο μια διεργασία έχουν δικαιώματα εγγραφής σε έναν πίνακα - είτε μεσω πρόσβασης από την :public είτε από μηνύματα στην ιδιοκτήτρια διεργασία - οι race conditions είναι πιθανές. Για παράδειγμα, δύο διεργασίες διαβάζουν μια τιμή μετρητή με τιμή 0, την αυξάνουν, και γράφουν 1· το τελικό αποτέλεσμα αντιπροσωπεύει μια μόνο αύξηση.

Για τους μετρητές συγκεκριμένα, η :ets.update_counter/3 παρέχει για ατομική ενημέρωση-και-ανάγνωση. Για άλλες περιπτώσεις, μπορεί να χρειάζεται η ιδιοτήτρια διεργασία να εκτελέσει προσαρμοσμένες ατομικές λειτουργίες ως απάντηση σε μηνύματα, όπως το “προσθεσε αυτή την τιμή στην λίστα στο κλειδί :results“.

Εισαγωγή Δεδομένων

Η ETS δεν έχει σχήμα. Ο μόνος περιορισμός είναι ότι τα δεδομένα πρέπει να αποθηκεύονται σαν μια τούπλα της οποίας το πρώτο στοιχείο είναι το κλειδί. Για να προσθέσουμε νέα δεδομένα μπορούμε να χρησιμοποιήσουμε την insert/2:

iex> :ets.insert(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
true

Όταν χρησιμοποιούμε την insert/2 με ένα set ή με ένα ordered_set τα υπάρχοντα δεδομένα αντικαθίστανται. Για να το αποφύγουμε αυτό υπάρχει η insert_new/2 η οποία επιστρέφει false/2 για υπάρχοντα κλειδιά:

iex> :ets.insert_new(:user_lookup, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
false
iex> :ets.insert_new(:user_lookup, {"3100", "", ["Elixir", "Ruby", "JavaScript"]})
true

Ανάκτηση Δεδομένων

Η ETS μας προσφέρει μερικούς βολικούς και εύκαμπτους τρόπους να ανακτήσουμε αποθηκευμένα δεδομένα. Θα δούμε πως να ανακτούμε δεδομένα με το κλειδί και μέσω διαφορετικών μορφών αντιπαραβολής προτύπων.

Η πιο αποτελεσματική και ιδανική μέθοδος ανάκτησης είναι με αναζήτηση κλειδιού. Παρ’ ότι χρήσιμο, το ταίριασμα περνάει όλο τον πίνακα και θα πρέπει να χρησιμοποιείται με φειδώ ειδικά για πολύ μεγάλα σετ δεδομένων.

Αναζήτηση Κλειδιού

Δεδομένου ενός κλειδιού, μπορούμε να χρησιμοποιήσουμε την lookup/2 για να ανακτήσουμε όλες τις εγγραφές με αυτό το κλειδί:

iex> :ets.lookup(:user_lookup, "doomspork")
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}]

Απλές Αντιπαραβολές

Η ETS χτίστηκε για την Erlang, έτσι πρέπει να ξέρετε ότι η αντιπαραβολή μεταβλητών μπορεί να είναι λιγάκι άχαρη.

Για να ορίσουμε μια μεταβλητή στην αντιπαραβολή μας χρησιμοποιούμε τα άτομα :"$1", :"$2", :"$3" και ούτω καθεξής. Ο αριθμός μεταβλητής αντικατοπτρίζει την θέση αποτελέσματος και όχι την θέση αντιπαραβολής. Για τιμές που δεν μας ενδιαφέρουν, χρησιμοποιούμε την μεταβλητή :_.

Οι τιμές μπορούν επίσης να χρησιμοποιηθούν σε αντιπαραβολές, αλλά μόνο οι μεταβλητές θα επιστραφούν σαν μέρος του αποτελέσματός μας. Ας τα βάλουμε όλα μαζί και να δούμε πως λειτουργεί:

iex> :ets.match(:user_lookup, {:"$1", "Sean", :_})
[["doomspork"]]

Ας δούμε ένα άλλο παράδειγμα για να καταλάβουμε πως οι μεταβλητές επηρεάζουν την σειρά της λίστας αποτελεσμάτων:

iex> :ets.match(:user_lookup, {:"$99", :"$1", :"$3"})
[["Sean", ["Elixir", "Ruby", "Java"], "doomspork"],
 ["", ["Elixir", "Ruby", "JavaScript"], "3100"]]

Τι γίνεται αν θέλουμε το αρχικό αντικείμενό μας και όχι μια λίστα; Μπορούμε να χρησιμοποιήσουμε την match_object/2, η οποία άσχετα από τις μεταβλητές επιστρέφει ολόκληρο το αντικείμενό μας:

iex> :ets.match_object(:user_lookup, {:"$1", :_, :"$3"})
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]},
 {"3100", "", ["Elixir", "Ruby", "JavaScript"]}]

iex> :ets.match_object(:user_lookup, {:_, "Sean", :_})
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}]

Προχωρημένη Αναζήτηση

Μάθαμε για απλές περιπτώσεις αντιπαραβολής αλλά τι γίνεται αν θέλουμε κάτι πιο κοντινό σε ερώτημα SQL; Ευτυχώς μας γίνεται διαθέσιμο ένα πιο εύρωστο συντακτικο. Για να αναζητήσουμε δεδομένα με την select/2 χρειάζεται να δημιουργήσουμε μια λίστα από τούπλες με τρία ορίσματα. Αυτές οι τούπλες αναπαριστούν το πρότυπό μας, μηδέν ή περισσότερους προστάτες και μια μορφή τιμής επιστροφής.

Οι αντιπαραβολές μεταβλητών και δύο νέες μεταβλητές, οι :"$$" και :"$_", μπορούν να χρησιμοποιηθούν για να δημιουργήσουν την τιμή επιστροφής. Αυτές οι νέες μεταβλητές είναι συντομεύσεις για τη μορφή αποτελέσματος· η :"$$" δέχεται αποτελέσματα σαν λίστες και η :"$_" δέχεται τα αρχικά αντικείμενα δεδομένων.

Ας πάρουμε ένα από τα προηγούμενα παραδείγματα της match/2 και να το μετατρέψουμε σε μια select/2:

iex> :ets.match_object(:user_lookup, {:"$1", :_, :"$3"})
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]},
 {"3100", "", ["Elixir", "Ruby", "JavaScript"]}]

{% raw %}iex> :ets.select(:user_lookup, [{{:"$1", :_, :"$3"}, [], [:"$_"]}]){% endraw %}
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]},
 {"3100", "", ["Elixir", "Ruby", "JavaScript"]}]

Παρόλο που η select/2 επιτρέπει για καλύτερο έλεγχο πάνω στο τι και πως θα ανακτήσουμε εγγραφές, το συντακτικό είναι αρκετά εχθρικό και μόνο χειρότερο θα γίνει. Για να το διαχειριστούμε η ενότητα ETS περιλαμβάνει την fun2ms/1 η οποία μετατρέπει τις συναρτήσεις σε match_specs. Με την fun2ms/1 μπορούμε να δημιουργήσουμε ερωτήματα χρησιμοποιώντας το οικείο συντακτικό συναρτήσεων.

Ας χρησιμοποιήσουμε την fun2ms/1 και την select/2 για να βρούμε όλα τα ψευδώνυμα με περισσότερες από 2 γλώσσες:

iex> fun = :ets.fun2ms(fn {username, _, langs} when length(langs) > 2 -> username end)
{% raw %}[{{:"$1", :_, :"$2"}, [{:>, {:length, :"$2"}, 2}], [:"$1"]}]{% endraw %}

iex> :ets.select(:user_lookup, fun)
["doomspork", "3100"]

Θέλετε να μάθετε περισσότερα για τους προσδιορισμούς αντιπαραβολών; Ελέγξτε την επίσημη τεκμηρίωση της Erlang για την match_spec.

Διαγραφή Δεδομένων

Αφαίρεση Εγγραφών

Οι όροι διαγραφής είναι αρκετά ευθείς όπως η insert/2 και η lookup/2. Με την delete/2 χρειαζόμαστε μόνο τον πίνακα μας και το κλειδί. Αυτή διαγράφει το κλειδί και τις τιμές του:

iex> :ets.delete(:user_lookup, "doomspork")
true

Αφαίρεση Πινάκων

Οι πίνακες ETS δεν καθαρίζονται εκτός και αν ο γονέας τερματιστεί. Μερικές φορές μπορεί να είναι χρήσιμο να διαγράψουμε έναν ολόκληρο πίνακα χωρίς να τερματίσουμε την διεργασία ιδιοκτήτη. Για αυτό μπορούμε να χρησιμοποιήσουμε την delete/1:

iex> :ets.delete(:user_lookup)
true

Παράδειγμα χρήσης της ETS

Δεδομένου όσα μάθαμε πιο πάνω, ας τα βάλουμε όλα μαζί και να χτίσουμε μια απλή cache για ακριβές εκτελέσεις. Θα υλοποιήσουμε τη συνάρτηση get/4 που θα δέχεται μια ενότητα, μια συνάρτηση, ορίσματα και επιλογές. Για τώρα η μόνη επιλογή για την οποία θα ανησυχήσουμε θα είναι η :ttl.

Για αυτό το παράδειγμα θα υποθέσουμε ότι ο πίνακας ETS έχει δημιουργηθεί σαν μέρος μιας άλλης διαδικασίας, όπως ένoς επιτηρητή:

defmodule SimpleCache do
  @moduledoc """
  A simple ETS based cache for expensive function calls.
  """

  @doc """
  Retrieve a cached value or apply the given function caching and returning
  the result.
  """
  def get(mod, fun, args, opts \\ []) do
    case lookup(mod, fun, args) do
      nil ->
        ttl = Keyword.get(opts, :ttl, 3600)
        cache_apply(mod, fun, args, ttl)

      result ->
        result
    end
  end

  @doc """
  Lookup a cached result and check the freshness
  """
  defp lookup(mod, fun, args) do
    case :ets.lookup(:simple_cache, [mod, fun, args]) do
      [result | _] -> check_freshness(result)
      [] -> nil
    end
  end

  @doc """
  Compare the result expiration against the current system time.
  """
  defp check_freshness({mfa, result, expiration}) do
    cond do
      expiration > :os.system_time(:seconds) -> result
      :else -> nil
    end
  end

  @doc """
  Apply the function, calculate expiration, and cache the result.
  """
  defp cache_apply(mod, fun, args, ttl) do
    result = apply(mod, fun, args)
    expiration = :os.system_time(:seconds) + ttl
    :ets.insert(:simple_cache, {[mod, fun, args], result, expiration})
    result
  end
end

Για να επιδείξουμε την cache, θα χρησιμοποιήσουμε μια συνάρτηση που επιστρέφει την τιμή συστήματος και μια TTL 10 δευτερολέπτων. Όπως θα δείτε το παράδειγμα παρακάτω, δεχόμαστε το προσωρινά αποθηκευμένο αποτέλεσμα μέχρι η τιμή να λήξει:

defmodule ExampleApp do
  def test do
    :os.system_time(:seconds)
  end
end

iex> :ets.new(:simple_cache, [:named_table])
:simple_cache
iex> ExampleApp.test
1451089115
iex> SimpleCache.get(ExampleApp, :test, [], ttl: 10)
1451089119
iex> ExampleApp.test
1451089123
iex> ExampleApp.test
1451089127
iex> SimpleCache.get(ExampleApp, :test, [], ttl: 10)
1451089119

Μετά από 10 δευτερόλεπτα αν δοκιμάσουμε ξανά θα πρέπει να πάρουμε ένα φρέσκο αποτέλεσμα:

iex> ExampleApp.test
1451089131
iex> SimpleCache.get(ExampleApp, :test, [], ttl: 10)
1451089134

Όπως θα δείτε είμαστε σε θέση να υλοποιήσουμε μια κλιμακούμενη και γρήγορη προσωρινή μνήμη χωρίς καμμία εξωτερική εξάρτηση και αυτή είναι μόνο μια από τις πολλές χρήσεις της ETS.

ETS βασισμένη στο δίσκο

Τώρα ξέρουμε ότι η ETS είναι για αποθήκευση στη μνήμη αλλά τι γίνεται αν θέλουμε αποθήκευση βασισμένη στο δίσκο; Για αυτό έχουμε την Disk Based Term Storage, ή εν συντομία DETS. Τα API των ETS και DETS είναι ανταλλάξιμα με την εξαίρεση του πως δημιουργούνται οι πίνακες. Η DETS επαφίεται στην open_file/2 και δεν χρειάζεται την επιλογή :named_table:

iex> {:ok, table} = :dets.open_file(:disk_storage, [type: :set])
{:ok, :disk_storage}
iex> :dets.insert_new(table, {"doomspork", "Sean", ["Elixir", "Ruby", "Java"]})
true
iex> select_all = :ets.fun2ms(&(&1))
[{:"$1", [], [:"$1"]}]
iex> :dets.select(table, select_all)
[{"doomspork", "Sean", ["Elixir", "Ruby", "Java"]}]

Αν βγείτε από το iex και κοιτάξετε στον τοπικό σας φάκελο, θα δείτε ένα νέο αρχειο disk_storage:

$ ls | grep -c disk_storage
1

Ένα τελευταίο πράγμα που πρέπει να σημειώσουμε είναι ότι η DETS δεν υποστηρίζει τα ordered_set όπως η ETS, μόνο τα set, bag και duplicate_bag.

Έπιασες λάθος ή θέλεις να συνεισφέρεις στο μάθημα; Επεξεργαστείτε αυτό το μάθημα στο GitHub!