Elixir中的ETS表简介

在设计Elixir程序时,通常需要共享一个状态。 例如,在我以前的一篇文章中,我展示了如何对服务器进行编码以执行各种计算并将结果保存在内存中(后来, 我们看到了如何在主管的帮助下使该服务器成为防弹产品)。 但是,有一个问题:如果您有一个负责状态的进程以及访问该状态的许多其他进程,则性能可能会受到严重影响。 这仅仅是因为该过程一次只能处理一个请求。

但是,有多种方法可以解决此问题,今天我们将讨论其中一种。 认识Erlang Term Storage表或简单的ETS表,这是一种可以存储任意数据元组的快速内存存储。 顾名思义,这些表最初是在Erlang中引入的,但是与其他任何Erlang模块一样,我们也可以在Elixir中轻松使用它们。

在本文中,您将:

  • 了解如何创建创建后可用的ETS表和选项。
  • 了解如何执行读取,写入,删除和其他一些操作。
  • 请参阅实际使用的ETS表。
  • 了解基于磁盘的ETS表以及它们与内存表的区别。
  • 了解如何来回转换ETS和DETS。

所有代码示例均适用于最近发布的 Elixir 1.4和1.5。

ETS表简介

如前所述,ETS表是包含数据元组(称为行)的内存中存储。 多个进程可以通过其ID或表示为原子的名称访问该表,并执行读取,写入,删除和其他操作。 ETS表是由单独的进程创建的,因此,如果该进程终止,则该表将被销毁。 但是,由于没有自动垃圾收集机制,因此表可能会在内存中闲逛一段时间。

ETS表中的数据由元组{:key, value1, value2, valuen} 。 您可以轻松地通过数据键查找数据或插入新行,但是默认情况下,不会有两行使用相同的键。 基于键的操作非常快,但是如果由于某种原因您需要从ETS表中生成一个列表,例如对数据执行复杂的操作,那也是可能的。

此外,还有一些基于磁盘的ETS表可用于将其内容存储在文件中。 当然,它们的运行速度较慢,但​​是通过这种方式,您可以轻松地存储简单的文件。 最重要的是,内存中的ETS可以轻松转换为基于磁盘的,反之亦然。

因此,我认为是时候开始我们的旅程,看看如何创建ETS表!

创建ETS表

要创建ETS表,请使用new/2函数。 只要我们使用Erlang模块,它的名称就应该写成一个原子:

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

请注意,直到最近,每个BEAM实例最多只能创建1400个表,但是现在不再是这种情况—您仅受可用内存量的限制。

传递给new函数的第一个参数是表的名称(别名),而第二个参数包含选项列表。 现在, cool_table变量包含一个标识系统中表的数字:

IO.inspect cool_table # => 12306

现在,您可以使用此变量对表执行后续操作(例如,读取和写入数据)。

可用选项

让我们讨论一下在创建表时可以指定的选项。 要注意的第一件事(有点奇怪)是,默认情况下,您不能以任何方式使用表的别名,并且基本上没有任何作用。 但是仍然必须在创建表时传递别名。

为了能够通过别名访问表,必须提供一个:named_table选项,如下所示:

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

顺便说一句,如果您想重命名表,可以使用rename/2 函数来完成:

:ets.rename(cool_table, :cooler_table)

接下来,如前所述,一个表不能包含多个具有相同键的行,而这由type决定。 有四种可能的表类型:

  • :set —这是默认值。 这意味着您不能有多个键完全相同的行。 不会以任何特定方式对行进行重新排序。
  • :ordered_set —与:set相同,但各行按术语排序。
  • :bag —多个行可能具有相同的键,但是这些行仍然不能完全相同。
  • :duplicate_bag行可以完全相同。

:ordered_set表值得一提。 如Erlang的文档所述 ,这些表在比较相等时不仅将它们匹配键视为相等。 那是什么意思?

仅当Erlang中的两个术语具有相同的值和相同的类型时,它们才匹配。 所以整数1场另一个整数比赛1 ,但不能上浮1.0 ,因为他们有不同的类型。 但是,如果两个术语具有相同的值和类型, 或者它们都是数字并且扩展为相同的值,则它们比较相等。 这意味着11.0比较相等。

要提供表的类型,只需在选项列表中添加一个元素:

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

您可以传递的另一个有趣的选项是:compressed 。 这意味着表内的数据(而不是键)将以紧凑的形式存储(猜测是什么)。 当然,在表上执行的操作将变慢。

接下来,您可以控制元组中的哪个元素应用作键。 默认情况下,使用第一个元素(位置1 ),但这可以轻松更改:

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

现在,元组中的第二个元素将被视为键。

最后但并非最不重要的选项控制表的访问权限。 这些权限决定了哪些进程可以访问该表:

  • :public任何进程都可以对该表执行任何操作。
  • :protected —默认值。 只有所有者进程可以写入表,但是所有进程都可以读取。
  • :private private-只有所有者进程可以访问该表。

因此,要使表私有,您可以编写:

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

好了,关于选项的讨论足够多了,让我们看看可以对表执行的一些常见操作!

写操作

为了从表中读取内容,您首先需要在其中写入一些数据,因此让我们从后面的操作开始。 使用insert/2函数将数据放入表中:

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

您还可以传递这样的元组列表:

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

请注意,如果表的类型为:set并且新键与现有键匹配,则旧数据将被覆盖。 同样,如果表的类型为:ordered_set并且新键比较旧键,则数据将被覆盖,因此请注意这一点。

插入操作(即使一次具有多个元组)也保证是原子的和隔离的 ,这意味着要么所有内容都存储在表中,要么什么都不存储。 另外,其他进程将看不到该操作的中间结果。 总而言之,这与SQL事务非常相似。

如果您担心重复键或不想误覆盖数据,请改用insert_new/2 函数 。 它类似于insert/2但是将永远不会插入重复键,而将返回false:bag:duplicate_bag表也​​是如此:

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

如果提供元组列表,则将检查每个键,即使其中一个键重复,操作也会被取消。

读取操作

太好了,现在我们的表中有一些数据—如何获取它们? 最简单的方法是通过按键执行查找

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

请记住,对于:ordered_set表,键应比较等于提供的值。 对于所有其他表类型,它应该匹配。 另外,如果表是:bag:ordered_bag ,则lookup/2函数可能返回包含多个元素的列表:

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

除了获取列表,您还可以使用lookup_element/3 函数在所需位置捕获元素:

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

在这段代码中,我们将在键:number下获得该行,然后将该元素放在第二个位置。 它也可以与:bag: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

如果您只想检查表中是否存在某些键,请使用member/2 ,该键返回truefalse

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

您还可以分别通过使用first/1last/1来获得表中的第一个或最后一个键:

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

最重要的是,可以根据提供的密钥确定上一个或下一个密钥。 如果找不到这样的键,将返回: :"$end_of_table"

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"

但是请注意,使用firstnextlastprev类的表遍历不是孤立的。 这意味着在您对其进行迭代时,进程可能会向表中删除或添加更多数据。 解决此问题的一种方法是使用safe_fixtable/2 ,它可以修复表并确保每个元素仅被提取一次。 该表保持固定,除非进程释放它:

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) # => table is released at this point
:ets.info(cool_table, :safe_fixed_monotonic_time) |> IO.inspect # => false

最后,如果您想在表格中找到一个元素并将其删除,请使用take/2 函数

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

删除作业

好的,现在让我们说您不再需要桌子,而希望摆脱它。 为此使用delete/1

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

当然,您也可以通过其键删除一行(或多行):

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

要清除整个表,请使用delete_all_objects/1

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

最后,要查找和删除特定对象,请使用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]

转换表

通过使用tab2list/1 函数,可以随时将ETS表转换为列表:

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]

但是请记住,通过键从表中获取数据是非常快速的操作,如果可能,您应该坚持使用它。

您也可以使用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

请注意,第二个参数应为字符列表(单引号字符串)。

还有一些其他操作可以应用于ETS表,当然,我们将不讨论它们。 我真的建议浏览ETS上的Erlang文档以了解更多信息。

通过ETS坚持国家

总结到目前为止我们已经学到的事实,让我们修改一下我在有关GenServer的文章中介绍的一个简单程序。 这是一个称为CalcServer的模块,该模块允许您通过向服务器发送请求或获取结果来执行各种计算:

defmodule CalcServer do
  use GenServer

  def start(initial_value) do
    GenServer.start(__MODULE__, initial_value, name: __MODULE__)
  end

  def init(initial_value) when is_number(initial_value) do
    {:ok, initial_value}
  end

  def init(_) do
    {:stop, "The value must be an integer!"}
  end

  def sqrt do
    GenServer.cast(__MODULE__, :sqrt)
  end

  def add(number) do
    GenServer.cast(__MODULE__, {:add, number})
  end

  def multiply(number) do
    GenServer.cast(__MODULE__, {:multiply, number})
  end

  def div(number) do
    GenServer.cast(__MODULE__, {:div, number})
  end

  def result do
    GenServer.call(__MODULE__, :result)
  end

  def handle_call(:result, _, state) do
    {:reply, state, state}
  end

  def handle_cast(operation, state) do
    case operation do
      :sqrt -> {:noreply, :math.sqrt(state)}
      {:multiply, multiplier} -> {:noreply, state * multiplier}
      {:div, number} -> {:noreply, state / number}
      {:add, number} -> {:noreply, state + number}
      _ -> {:stop, "Not implemented", state}
    end
  end

  def terminate(_reason, _state) do
    IO.puts "The server terminated"
  end
end

CalcServer.start(6.1)
CalcServer.sqrt
CalcServer.multiply(2)
CalcServer.result |> IO.puts # => 4.9396356140913875

当前,我们的服务器不支持所有数学运算,但是您可以根据需要扩展它。 另外, 我的另一篇文章介绍了如何将此模块转换为应用程序,并利用管理程序来处理服务器崩溃。

我现在想做的是添加另一个功能:记录与传递的参数一起执行的所有数学运算的功能。 这些操作将存储在ETS表中,以便我们稍后能够获取它。

首先,修改init函数,以便创建一个类型为:duplicate_bag的新命名专用表。 我们使用:duplicate_bag是因为可以执行两个具有相同参数的相同操作:

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

现在调整handle_cast回调,以便它记录请求的操作,准备公式,然后执行实际计算:

def handle_cast(operation, state) do
    operation |> prepare_and_log |> calculate(state)
  end

这是prepare_and_log私有函数:

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

我们正在立即记录该操作(稍后将显示相应的功能)。 然后返回适当的函数;如果我们不知道如何处理该操作,则返回nil

至于log函数,我们应该支持一个元组(既包含操作名称又包含参数)或原子(仅包含操作名称,例如:sqrt ):

def log(operation) when is_tuple(operation) do
    :ets.insert(:calc_log, operation)
  end

  def log(operation) when is_atom(operation) do
    :ets.insert(:calc_log, {operation, nil})
  end

  def log(_) do
    :ets.insert(:calc_log, {:unsupported_operation, nil})
  end

接下来, calculate函数,将返回正确的结果或一条停止消息:

defp calculate(func, state) when is_function(func) do
    {:noreply, func.(state)}
  end

  defp calculate(_func, state) do
    {:stop, "Not implemented", state}
  end

最后,让我们介绍一个新的接口函数,以按类型提取所有已执行的操作:

def operations(type) do
    GenServer.call(__MODULE__, {:operations, type})
  end

处理呼叫:

def handle_call({:operations, type}, _, state) do
    {:reply, fetch_operations_by(type), state}
  end

并执行实际的查找:

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

现在测试所有内容:

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 # => [add: 1, add: 2]

结果是正确的,因为我们使用参数12执行了两个:add操作。 当然,您可以根据需要进一步扩展该程序。 不过,请不要滥用ETS表,并在确实要提高性能的情况下使用它们-在许多情况下,使用不可变是更好的解决方案。

磁盘ETS

在结束本文之前,我想谈谈基于磁盘的ETS表或DETS

DETS与ETS非常相似:它们使用表以元组的形式存储各种数据。 正如您所猜到的,区别在于它们依赖文件存储而不是内存,并且功能更少。 DETS的功能类似于我们上面讨论的功能,但是某些操作的执行方式略有不同。

要打开表,您需要使用open_file/1open_file/2没有像:ets模块那样的new/2函数。 由于我们还没有任何表,让我们继续使用open_file/2 ,它将为我们创建一个新文件:

:dets.open_file(:file_table, [])

文件名默认情况下等于表的名称,但是可以更改。 传递给open_file的第二个参数是以元组形式编写的选项列表。 有一些可用的选项,例如:access:auto_save 。 例如,要更改文件名,请使用以下选项:

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

请注意,还有一个:type选项可能具有以下值之一:

  • :set
  • :bag
  • :duplicate_bag

这些类型与ETS相同。 请注意,DETS不能具有:ordered_set类型。

没有:named_table选项,因此您始终可以使用表的名称来访问它。

值得一提的另一件事是必须正确关闭DETS表:

:dets.close(:file_table)

如果您不这样做,则下次打开表时将对其进行修复。

您可以像使用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)

但是请记住,DETS比ETS慢,因为Elixir需要访问磁盘,这当然会花费更多时间。

请注意,您可以轻松地来回转换ETS和DETS表。 例如,让我们使用to_ets/2并在内存中复制DETS表的内容:

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

使用to_dets/2将ETS的内容复制到DETS:

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)

综上所述,基于磁盘的ETS是一种将内容存储在文件中的简单方法,但是此模块的功能不如ETS强大,操作也较慢。

结论

在本文中,我们讨论了ETS和基于磁盘的ETS表,这些表使我们能够分别在内存和文件中存储任意术语。 我们已经看到了如何创建此类表,可用类型是什么,如何执行读写操作,如何销毁表以及如何将它们转换为其他类型。 您可以在Elixir指南Erlang官方页面上找到有关ETS的更多信息。

再一次,不要过度使用ETS表,如果可能的话,尽量使用不可变的表。 但是,在某些情况下,ETS可能会很好地提高性能,因此了解此解决方案在任何情况下都是有帮助的。

希望您喜欢这篇文章。 与往常一样,感谢您与我在一起,并很快见到您!

翻译自: https://code.tutsplus.com/articles/ets-tables-in-elixir--cms-29526

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Elixir是一种运行在Erlang虚拟机上的函数式编程语言,它具有并发性、容错性和高可扩展性。学习Elixir可以帮助我们更好地理解函数式编程的思维方式,提高并发编程的能力,并且为构建可靠的、高性能的分布式应用提供了一种优秀的工具。 首先,学习Elixir可以让我们更好地理解并掌握函数式编程的思维方式。函数式编程强调不可变性、纯函数和高阶函数等概念,这些概念在Elixir得到了很好的体现。通过学习Elixir,我们可以学会如何编写函数式风格的代码,提高代码的可读性和可维护性。 其次,Elixir的并发编程特性也是学习的重点之一。Elixir提供了轻量级的原生线程——进程,以及消息传递机制来实现并发。通过学习Elixir,我们可以了解如何使用进程来实现并发编程,以及如何避免常见的并发编程问题,如死锁和竞争条件。 再者,学习Elixir还可以帮助我们构建高性能的分布式应用。由于Elixir建立在Erlang虚拟机之上,所以它继承了Erlang对于分布式应用的支持。在Elixir,我们可以轻松地编写分布式系统,并且可以利用Erlang提供的强大的容错机制来确保系统的可靠性。 综上所述,学习Elixir对我们来说是非常有价值的。通过学习Elixir,我们可以更好地理解函数式编程的思维方式,提高并发编程的能力,并且为构建可靠的、高性能的分布式应用提供了一种强大的工具。因此,学习Elixir将使我们成为更加全面和高效的程序员。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值