0

What is ETS in Elixir?

Tuần này, mình không làm project nữa mà cùng các bạn tìm hiểu lý thuyết nhé. Mình sẽ dịch một bài viết liên quan tới ETS trong Elixir.

What is ETS in Elixir? (ETS trong Elixir là gì?)

Nếu bạn đã tìm hiểu về Elixir một thời gian thì chắc hẳn các bạn đã biết về "ETS". ETS là viết tắt của Erlang Term Storage. Nó là một enginge lưu trữ có sẵn trong Elixir dựa được xây dựng từ OTP thông qua tương tác với Erlang. ETS lưu trữ dưới dạng key/value giống như là Redis. ETS được tích hợp từ OTP nên bạn có thể sử dụng nó miễn phí mà không cần sự cho phép từ bên thứ 3. Chúng ta sẽ tìm hiểu về các sử dụng ETS trong Elixir trong bài hướng dẫn hôm nay.

What is ETS? (ETS là gì?)

ETS là một bộ nhớ lưu trữ dưới dạng key / value là một phần của OPT vs Erlang. Bạn có thể lưu trữ bất kỳ Erlang term nào trong ETS mà không cần phải serialize hay deserialize sang một định dạng khác. ETS cho phép bạn lưu trữ một lượng lớn dữ liệu với thời gian truy cập dữ liệu liên tục. Giống như mọi thứ khác trong Elixir và Erlang, mỗi bảng ETS được tạo ra và quản lý bởi một tiến trình. Khi tiến trình này bị tắt thì bảng đó cũng tự dộng bị xóa đi.

Creating tables (Tạo bảng)

ETS có sẵn trong Elixir thông qua sự tương tác với Erlang. Có nghĩa là chúng ta sẽ phải sử dụng :etsđể tương tác với ETS trong Elixir. Để tạo một bảng mới, sử dụng hàm new/2 với 2 tham số là tên bảng (đạng atom) và danh sách các tùy chọn:

table = :ets.new(:todos, [:set, :private])

Trong ví dụ trên, tham số đầu tiên là tên bảng (:todos). Với các tùy chọn trong tham số thứ 2 thì có nhiểu tùy chọn khác nhau tùy thuộc vào trường hợp khác nhau.

Trong trường hợp này mình chọn là :set. Đây là giá trị mặc định và có thế nó sẽ là cơ chế lưu trữ mà bạn thích nhất trong "database". Với loại này thì cho phép lưu 1 value 1 key và key là duy nhất không được trùng lặp.

Bạn cũng có thể sử dụng :ordered_set, chắc bạn sẽ có thể đoán được như khi đọc ý nghĩa của option đó, nó cũng giống như options :set nhưng mà nó được sắp xếp các đối tượng.

Tiếp theo đó là :bag, nó cho phép được tạo nhiều đối tượng với mỗi khoá, nhưng chỉ có một instance của từng đối tượng cho mỗi key. Và cuối là :duplicate_bag, nó cũng giống như :bag nhưng cho phép tạo được các bản sao.

Tham số thứ 2 dùng để kiểm soát truy cập cho bảng. Trong ví dụ trên, tôi đã thiết lập bảng với option là :private. Nó có nghĩa là chỉ có các tiến trình (process) đã tạo ra bảng đó mới có thể đọc hoặc ghi với bảng này.

Dí nhiên bạn cũng có thể set :public, nó có nghĩa là bất kỳ process nào cũng có thể đọc và ghi vào table đó. Cuối cùng, bạn có thể set là :protected, nó có nghĩa là process nào cũng có thể đọc nhưng chỉ có process đã tạo ra mới có thể viết hoặc ghi vào table đó.

Adding data (Thêm dữ liệu)

Thêm dữ liệu vào một bảng ETS là rất dễ dàng bởi vì nó chỉ cần truyền vào một bộ dữ liệu với thành phần đầu tiên là key và thành phần thứ hai là value:

:ets.insert(table, {:shopping, ["milk", "bread", "cheese"]})

Trong trường hợp này key chính là các kiểu atom :shopping, và value là dạng array, list ["milk", "bread", "cheese"].

Reading data (Đọc dữ liệu)

Có một vài cách khác nhau để bạn có thể đọc dữ liệu từ một bảng ETS.

Phương pháp dễ nhất và nhanh nhất là tìm kiếm bằng key:

:ets.lookup(table, :shopping)

Nếu bạn đang sử dụng ETS để lưu trữ dạng key / value, thì nó sẽ làm việc rất tốt ngay cả đối với các tập dữ liệu lớn hơn.

Bạn cũng có thể thực hiện các matches và các câu quries nâng cao hơn tới table để lấy dữ liệu từ partial matches. Tuy nhiên, đó là một chủ đề trong của chính nó và tôi sẽ không đề cập tới nó vào ngày hôm nay. Bạn tìm hiểu tốt nhất tại tài liệu ETS .

Deleting data and destroying tables (Xóa dữ liệu và xóa bảng)

Xóa một đối tường từ bảng rất dễ dàng, tất cả những gì mà bạn cần làm là truyền key của đối tượng đó vào hàm delete/2:

:ets.delete(table, :shopping)

Nếu bạn thử đọc dữ liệu từ đối tượng đó một lần nữa thì bạn sẽ thấy nó không còn ở đó.

Nếu bạn muốn xóa bảng, bạn có thể sử dụng hàm delete/1:

:ets.delete(table)

Ngoài ra, nếu bạn tắt process, thì bảng đó cũng sẽ bị xóa như trên.

DETS là gì?

Nếu bạn đã tìm hiểu về Elixir / Erlang một thời gian đủ dài thì có thể các bạn sẽ được biết về "DETS". DETS tương tự như ETS, nhưng thay vì lưu trữ các dữ liệu trong bộ nhớ, thì nó được lưu vào đĩa - disk.

DETS cũng có một API tương tự như ETS, nhưng thay vì sử dụng new/2 để tạo ra một bảng, thì bạn sử dụng open_file/2:

{:ok, table} = :dets.open_file(:shopping, [type: :set])

Với bảng đã được tạo ra, sử dụng DETS là khá giống với ETS:

# Insert a row
:dets.insert(table, {:shopping, ["milk", "bread", "cheese"]})
 
# Find a row by it's id
:dets.lookup(table, :shopping)

Nếu bạn chạy các mã trên trong iex và sau đó thoát khỏi session đó bạn có thể tìm một file shopping mới trong cùng một nơi, nó chứng minh rằng dữ liệu đã được lưu vào đĩa.

Using ETS with GenServer (Sử dụng ETS với GenServer)

Hy vọng rằng đó là một bài giới thiệu tốt để sử dụng ETS. Bây giờ chúng ta hãy xem xét một ví dụ thực tế của việc sử dụng ETS kết hợp với GenServer ( được hiểu là GenServer trong Elixir ). Chúng ta sẽ tạo ra một todo server, nó có thể xử lý nhiều danh sách việc cần làm khác nhau.

Đầu tiên tạo một file mới có tên todos.ex và thêm GenServer

defmodule Todos do
  use GenServer
 
  def start_link do
    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end
end

Như bạn có thể thấy, đây là chỉ là implementation chuẩn của GenServer. Tôi đang đặt cho server một cái tên để làm việc dễ dàng hơn trong ví dụ này, nhưng trong thực tế có thể bạn sẽ có một todos server cho mỗi người dùng.

Trong hàm init chúng ta có thể tạo ra một bảng ETS mới cho process này và sau đó lưu nó như một state:

def init(:ok) do
  {:ok, :ets.new(:todos, [:named_table, :protected])}
end

Mặc định các bảng sẽ thiest lập dạng :set nên chúng ta không cần phải thêm tùy chọn này. Tôi đang sử dụng tùy chọn :named_table để tôi có thể truy cập vào table bằng tên của nó.

Và tôi sử dụng các tùy chọn điều khiển truy cập :protected để có thể đọc dữ liệu từ bất kỳ process nào, nhưng chỉ có process nào tạo ra mới có thể ghi hay xóa bảng đó.

Hàm đầu tiên tôi thêm vào sẽ lấy ra một danh sách việc cần làm từ server:

def find(name) do
  case :ets.lookup(:todos, name) do
    [{^name, items}] -> {:ok, items}
    [] -> :error
  end
end

Hàm này có 1 đối số là tên của danh sách và sau đó sẽ tìm và lấy ra từ ETS.

Tôi đang sử dụng một trương hợp mẫu để trả về kết quả. Nếu danh sách được tìm thấy trong bảng tôi sẽ trả lại các đối tượng đó, nhưng nếu danh sách không có tôi sẽ trả lại lỗi dạng atom :error.

Ký tự ^ để gán giá trị. Vì vậy, nếu các list biến có chứa atom :shopping, các mô hình phù hợp sẽ tìm kiếm lại các trong :shopping.

Chúng tôi có thể đưa các máy chủ cho một spin nhanh chóng để đảm bảo tất cả mọi thứ đang làm việc một cách chính xác. Bật iex lên và chạy lệnh sau:

# Start the server
Todos.start_link
 
# Try and find the shopping list
Todos.find(:shopping)
:error

Như bạn có thể thấy, chúng ta có kết quả là atom :error vì danh sách shopping chưa tồn tại. Hãy thêm một chức năng để tạo ra một danh sách mới

Đầu tiên chúng ta có thể thêm các public API để tạo một danh sách mới:

def new(list) do
  GenServer.call(__MODULE__, {:new, list})
end

Như bạn có thể thấy, đây chỉ là một cách đơn giản để call một process sử dụng GenServer.

Ở server chúng ta có thể xử lý các yêu cầu này với một hàm handle_call/3:

def handle_call({:new, list}, _from, table) do
  case find(list) do
    {:ok, list} ->
      {:reply, list, table}
    :error ->
      :ets.insert(table, {list, []})
      {:reply, [], table}
  end
end

Đầu tiên tôi cố gắng để tìm bảng bằng cách sử dụng hàm find/1 từ trước đó. Nếu danh sách đã tồn tại, tôi sẽ trả lại nó từ hàm đó luôn, nhưng nếu danh sách không tồn tại tôi sẽ tạo ra một danh sách rỗng và sau đó trả lại danh sách rỗng cho client.

Tiếp theo chúng ta có thể thêm một hàm để thêm một item vào một list, public API giống như thế này:

def add(name, item) do
  GenServer.call(__MODULE__, {:add, name, item})
end

Trong hàm này, tôi nhận tên và các item của danh sách để thêm vào.

Về phía máy chủ chúng ta có thể chấp nhận cuộc gọi này với một handle_call/3chức năng:

def handle_call({:add, name, item}, _from, table) do
  case find(name) do
    {:ok, items} ->
      items = [item | items]
      :ets.insert(table, {name, items})
      {:reply, items, table}
    :error ->
      {:reply, {:error, :list_not_found}, table}
  end
end

Một lần nữa tôi sử dụng hàm find/1 để tìm danh sách. Nếu danh sách đã tồn tại tôi có thể thêm các item mới vào danh sách các items cũ sau đó chèn danh sách mới vào ETS. Cuối cùng, tôi sẽ trở lại với danh sách mới đó cho client.

Nếu không tìm được danh sách tôi sẽ trả về một bộ dữ liệu chứa :error và lý do tại sao xảy ra lỗi.

Dưới đây là module này đầy đủ:

defmodule Todos do
  use GenServer
 
  def start_link do
    GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
  end
 
  def init(:ok) do
    {:ok, :ets.new(:todos, [:named_table, :public])}
  end
 
  def find(name) do
    case :ets.lookup(:todos, name) do
      [{^name, items}] -> {:ok, items}
      [] -> :error
    end
  end
 
  def new(name) do
    GenServer.call(__MODULE__, {:new, name})
  end
 
  def add(name, item) do
    GenServer.call(__MODULE__, {:add, name, item})
  end
 
  def handle_call({:new, name}, _from, table) do
    case find(name) do
      {:ok, name} ->
        {:reply, name, table}
      :error ->
        :ets.insert(table, {name, []})
        {:reply, [], table}
    end
  end
 
  def handle_call({:add, name, item}, _from, table) do
    case find(name) do
      {:ok, items} ->
        items = [item | items]
        :ets.insert(table, {name, items})
        {:reply, items, table}
      :error ->
        {:reply, {:error, :list_not_found}, table}
    end
  end
end

Bây giờ chúng ta có mọi thứ để có thể kiểm tra một vòng thích hợp. Bật iex lên và include file name để load nó vào session:

# Start the todos server
Todos.start_link
 
# Create a new todo list
Todos.new(:shopping)
 
# Add some items to the todo list
Todos.add(:shopping, "milk")
Todos.add(:shopping, "bread")
Todos.add(:shopping, "cheese")
 
# Find the todo list
Todos.find(:shopping)
 
# Try and find a todo list that does not exist
Todos.find(:work)
:error
 
# Try to add an item to a list that does not exist
Todos.add(:work, "do stuff")
{:error, :list_not_found}

Conclusion (Kết luận)

Bài hướng dẫn hôm nay, chúng ta đã xem xét cách sử dụng ETS (và DETS) trong Elixir.

ETS là một giải pháp lưu trữ trong bộ nhớ mà được xây dựng từ OTP. Điều này có nghĩa là bạn sử dụng nó miễn phí với Elixir vì Elixir được xây dựng trên nền tảng của Erlang.

ETS cho phép bạn lưu trữ bất Elixir or Erlang term nào, nó cung cấp việc tìm kiếm rất nhanh, và có thể lưu trữ rất nhiều dữ liệu với một vài cách khác nhau.

ETS table được tạo ra bởi các process. Một process có thể kiểm soát những người có thể truy cập dữ liệu và ETS bị phá hủy khi quá trình này sẽ tắt đi.

Như bạn có thể tưởng tượng, có rất nhiều công dụng thiết thực cho ETS như một cơ chế lưu trữ. Tôi có xu hướng sử dụng nó trong trường hợp bình thường đạt được như Redis. Tôi thích các bạn sử dụng nó như là một thay thế miễn phí cho Redis bời vì chúng ta đang xây dựng trên nền tẳng của Erlang và OTP.

Nguồn: http://culttt.com/2016/10/05/what-is-ets-in-elixir/


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí