Что такое ETS и для чего используется?

ETS (Erlang Term Storage) — это встроенный механизм хранения данных в оперативной памяти, предоставляемый виртуальной машиной BEAM, на которой работает Elixir. Он предназначен для высокоэффективного хранения и быстрого доступа к структурам данных, разделяемым между процессами. Хотя ETS родом из Erlang, он активно используется в Elixir благодаря полной совместимости.

Основные особенности ETS

  • Хранение данных в памяти (RAM): ETS таблицы живут в оперативной памяти и не сериализуются в процесс.

  • Разделение доступа: ETS позволяет хранить данные вне процесса-владельца, и другие процессы могут получать к ним доступ.

  • Высокая производительность: чтение и запись в ETS — быстрые, особенно при больших объемах данных.

  • Поддержка различных типов таблиц: для разных сценариев используются разные типы хешей или деревьев.

  • Нет распределенности: ETS работает только в рамках одного узла BEAM (в отличие от Mnesia, которая может быть распределенной).

Когда и зачем использовать ETS

  1. Кэширование: часто ETS используется как кэш — для хранения данных, к которым нужно иметь быстрый доступ без повторного вычисления или запросов к БД.

  2. Маппинг и справочники: создание быстрых ассоциативных таблиц, например, ID → имя.

  3. Общий доступ между процессами: ETS — один из немногих способов безопасно делиться структурой данных между множеством процессов без сериализации.

  4. Сбор и анализ статистики: хранение промежуточных метрик и счётчиков.

  5. Реализация очередей, графов, деревьев — с помощью ETS можно строить сложные в структуре хранилища.

Типы таблиц ETS

ETS поддерживает 4 типа таблиц, каждый из которых имеет собственные особенности:

  • :set — уникальные ключи, быстрый доступ; аналог Map.

  • :ordered_set — отсортированные уникальные ключи; на основе сбалансированного дерева.

  • :bag — допускаются дубли ключей, но значения не повторяются (один ключ — много значений).

  • :duplicate_bag — допускаются дубли как ключей, так и значений.

Пример создания таблицы:

:ets.new(:my_table, \[:set, :public, :named_table\])

Здесь:

  • :set — тип таблицы;

  • :public — любой процесс может читать и писать;

  • :named_table — таблица доступна по имени, а не только по идентификатору.

Основные операции

:ets.insert(:my_table, {:key, "value"})
:ets.lookup(:my_table, :key) # => \[{:key, "value"}\]
:ets.delete(:my_table, :key)
:ets.match(:my_table, {:"$1", :"$2"})
:ets.tab2list(:my_table) # => возвращает все элементы
  • insert/2: добавление или обновление записи;

  • lookup/2: поиск по ключу;

  • delete/2: удаление записи;

  • match/2: шаблонный поиск;

  • tab2list/1: экспорт всех данных из таблицы в List.

Управление доступом

ETS таблицы имеют разные уровни доступа:

  • :private — доступен только владельцу (по умолчанию).

  • :protected — только владелец может писать, но другие могут читать.

  • :public — любой может читать и писать.

Важно помнить: владелец таблицы — это процесс, создавший её. Если этот процесс завершается, таблица автоматически уничтожается (если явно не указано иное через :heir).

ETS и Elixir: обёртки и инструменты

Хотя можно использовать :ets напрямую, существует ряд библиотек, облегчающих работу:

  • :ets внутри GenServer — часто используют ETS в качестве хранилища внутри GenServer, особенно для кэширования.

  • :persistent_term — альтернатива ETS для хранения неизменяемых данных, быстрее при доступе, но дороже при обновлении.

  • :cachex — сторонняя библиотека на базе ETS с поддержкой TTL, fallbacks, TTL-refresh и т.п.

Ограничения и подводные камни

  • ETS не сериализует структуры, поэтому хранить внутри PIDs, функции, порты — возможно, но требует аккуратности.

  • Удаляется вместе с процессом, если не передать наследника (:heir).

  • Нет встроенного TTL — нужно реализовывать руками или использовать библиотеки.

  • Мутабельность: данные в ETS изменяемые, что нарушает принципы чистых функций.

Примеры использования в Elixir

Создание ETS-кэша внутри GenServer:

defmodule MyCache do
use GenServer
def start_link(\_) do
GenServer.start_link(\__MODULE_\_, %{}, name: \__MODULE_\_)
end
def init(\_) do
table = :ets.new(:my_cache, \[:set, :named_table, :public\])
{:ok, table}
end
def put(key, value), do: :ets.insert(:my_cache, {key, value})
def get(key), do: :ets.lookup(:my_cache, key)
end

ETS в Elixir — мощный инструмент, когда требуется быстрое, разделяемое между процессами хранилище, при этом не требующее постоянной сериализации или обращения к внешней базе. Он идеально подходит для кэшей, таблиц соответствия, справочников и статистических агрегатов.