使用 ETS 和 DETS 存储数据

etsdets 是两个咱们可用作大量 Erlang 项高效存储的系统模组。ETS 是 Erlang term storage 的简写,而 DETS 是 disk ETS 的简写。

ETS 和 DETS 执行基本同样的任务:他们提供了大型的键值查找表。ETS 属于内存驻留,而 DETS 则属于磁盘驻留。ETS 是相当高效率的 -- 使用 ETS 时,咱们可存储大量数据(只要咱们有足够内存),并可在恒定(或在某些情况下对数)时间内执行查找。DETS 提供与 ETS 几乎同样的一些接口,但将数据表存储在磁盘上。由于 DETS 使用磁盘存储,因此他要比 ETS 慢得多,但运行时将有着小得多的内存足迹。此外,ETS 和 DETS 的数据表,可由多个进程共用,使公共数据的进程间访问效率非常高。

ETS 和 DETS 的数据表,均是 关联的一些数据结构。对数据表的最常执行操作,是 插入查找。ETS 或 DETS 数据表,只是 Erlang 元组的集合。

存储在 ETS 数据表中的数据,被存储在 RAM 中,是 短暂的。当 ETS 数据表被弃置,或所属 Erlang 进程终止时,这些数据就将被删除。存储在 DETS 数据表中的数据,则是 持久性的,并在整个系统崩溃后,应仍存活。当某个 DETS 数据表被打开时,其会受一致性检查。当其被发现损坏时,那么就会进行修复该数据表的尝试(由于该数据表中的所有数据都会检查,此操作可能需要很长时间)。

此操作应会恢复该数据表中的所有数据,不过,当该数据表中最后一个条目在系统崩溃时正被构造,则其可能会丢失。

ETS 数据表被广泛应用于那些必须高效处理大量数据的应用,其中使用非破坏性赋值,及 “纯” Erlang 数据结构编程的成本太高。

ETS 的数据表看起来像是以 Erlang 实现,但实际上他们是在底层运行时系统中实现的,与普通 Erlang 对象相比有着不同性能特征。特别是,ETS 的数据表不会被垃圾回收;这意味着在使用超大 ETS 数据表时,没有垃圾回收代价,但在创建或访问 ETS 对象时,会产生轻微代价。

数据表的类型

ETS 和 DETS 表存储的是元组。元组中的一个元素(默认下第一个元素)称为数据表的 。我们会根据键,向数据表中插入元组及从数据表中提取元组。当我们向数据表中插入元组时,会发生什么,取决于该数据表的类型及键的值。有的称为 集合 的数据表,要求数据表中所有键都是唯一的。而另一些称为 的数据表,则允许多个元组有着同样的键。

选择正确类型的数据表,对咱们应用性能,有重要影响。

基本的集合和包数据表类型,又各有两个变种,构成了总共四种类型的数据表:

  • 集合,sets
  • 有序集合,ordered sets
  • 包,bags
  • 重复包,duplicate bags

在集合下,数据表中不同元组的所有键,都必须唯一。而在有序集合中,元组是排序的。在包下,可以有多个有着同一键的元组,但包中不能有两个相同的元组。而在重复包中,多个元组可以有着同一键,同时同一元组可在同一个数据表中出现多次。

对 ETS 和 DETS 数据表,有四种基本操作。

  • 创建新数据表或打开现有数据表

    我们以 ets:newdets:open_file 完成此操作。

  • 将一个或多个元组插入数据表

    这里我们调用 insert(TableId,X),其中 X 是个元组或元组列表。在 ETS 和 DETS 下,insert 有着同样参数,且工作方式相同。

  • 在数据表中查找某个元组

    这里我们调用 lookup(TableID, Key)。结果是个与 Key 匹配的元组列表。lookup 是同时为 ETS 和 DETS 定义的。

    查找的返回值,始终是个元组列表。这样我们就可以对数据包和数据集,使用同一个查找函数。当数据表类型是包时,那么多个元组就可以有同一个键,而当数据表类型是集合时,那么在查找成功时,该列表中就将只有一个元素。我们将在下一小节中,讨论数据表类型。

    当表中没有元组有着所需键时,则空列表即被返回。

  • 弃置数据表

    当我们使用完毕某个数据表后,我们可通过调用 dets:close(TableId)ets:delete(TableId) 告诉系统。

我们可以下面的这个小测试程序,演示他们的工作原理:

-module(ets_test).
-export([start/0]).

start() ->
    lists:foreach(fun test_ets/1, [set, ordered_set, bag, duplicate_bag]).

test_ets(Mode) ->
    TableId = ets:new(test, [Mode]),
    ets:insert(TableId, {a, 1}),
    ets:insert(TableId, {b, 2}),
    ets:insert(TableId, {a, 1}),
    ets:insert(TableId, {a, 3}),
    List = ets:tab2list(TableId),
    io:format("~-13w => ~p~n", [Mode, List]),
    ets:delete(TableId).

这个程序会以四种模式之一创建一个 ETS 数据表,并将元组 {a,1}{b,2}{a,1}{a,3} 插入该表。然后我们调用将整个数据表转换为列表的 tab2list,并将其打印。

当我们运行这个程序时,我们会得到以下输出:

1> ets_test:start().
set           => [{b,2},{a,3}]
ordered_set   => [{a,3},{b,2}]
bag           => [{b,2},{a,1},{a,3}]
duplicate_bag => [{b,2},{a,1},{a,1},{a,3}]
ok

对于集合的数据表类型,每个键都只会出现一次。当我们将元组 {a,1} 插入该表,然后再插入 {a,3} 时,那么最终值将是 {a,3}。集合与有序集合的唯一区别是,有序集合中的元素是按键排序的。当我们通过调用 tab2list 将该表转换为列表时,我们就可以看到这种顺序。

包数据表类型,可以有键的多次出现。因此,举例来说,当我们插入 {a,1} 之后又插入 {a,3},那么这个包将同时包含这两个元组,而不仅是最后那个。在重复包中,允许多个相同元组在包中,因此当我们将 {a,1} 插入到该包中后在插入 {a,1} 时,得到的数据表中就包含了{a,1} 元组的两份拷贝;而在普通包中,则只有该元组的一个副本。

ETS 数据表效率的考量

在内部,ETS 表是以哈希表表示的(有序集合除外,其是以平衡二叉树表示)。这意味着使用集合,会有轻微的空间代价,而使用有序集合则会有时间代价。插入集合的操作会以恒定时间完成,但插入有序集合会以表中条目数的对数,成正比的时间完成。

当咱们要在集合和有序集合间选择时,咱们应考虑数据表在构建出后,咱们打算用他们做什么 -- 若咱们想要个排序表,那么就用有序集合。

由于每次插入时有着同一键的元素都要比较是否相等,因此相比使用重复包,使用包的开销要更高。当有大量有着同样键的元组时,这样做可能会相当低效。

ETS 的数据表,被存储在与正常进程内存没有关联的独立存储区中。ETS 数据表被说成是由创建他的进程所有 -- 当该进程死亡,或 ets:delete 被调用时,该表就会被删除。ETS 的数据表不会被垃圾回收,这意味着数据表中可存储大量数据,而不会产生垃圾回收代价。

当某个元组被插入到一个 ETS 数据表中时,代表该元组的所有数据结构,都会从进程栈和堆上,拷贝到这个 ETS 数据表中。当在数据表上执行一次查找操作时,结果元组会从这个 ETS 数据表,拷贝到进程的栈和堆上。

除大型二进制值外,对于所有数据结构都是如此。大型二进制值会被存储在他们自己的堆外存储区域。此区域可被多个进程及 ETS 数据表共用,同时单独的二进制值,会在一个引用计数的垃圾回收器下受管理,这种垃圾回收器会跟踪有多少个不同进程,和多少个 ETS 数据表使用着该二进制值。当使用某个特定二进制值的进程和数据表使用计数降为零时,那么该二进制值的存储区域即可被回收。

所有这一切听起来可能相当复杂,但其结果是,在进程间发送包含大型二进制值的消息非常便宜,将元组插入包含二进制值的 ETS 数据表也非常便宜。将二进制值尽可能多地用于表示字符串及未类型化的大型内存块,便是一条良好规则。

创建 ETS 数据表

通过调用 ets:new,我们即可创建 ETS 的数据表。创建数据的进程,称为该数据表的 所有者。当咱们创建数据表时,其有组不可更改的选项。当所有者进程死亡时,数据表的空间会被自动重新分配。咱们可通过调用 ets:delete,删除数据表。

ets:new 的参数如下:

  • -spec ets:new(Name, [Opt]) -> TableId

    其中 Name 是个原子。[Opt] 是个选项列表,取自以下选项:

    • set | ordered_set | bag | duplicate_bag

      这将创建一个给定类型(我们前面已经讨论过这些类型)的 ETS 数据表。

    • private

      这会创建出一个私有表。只有所有者进程,才能读写该数据表。

    • public

      这会创建一个公共表。任何知道表标识符的进程,都可以读写该表。

    • protected

      这会创建一个受保护的数据表。任何知道表标识符的进程都可以读该表,但只有所有者进程才可以写入该表。

    • named_table

      当该选项出现时,那么 Name 就可用于随后的数据表操作。

    • {keypos, K}

      使用 K 作为键的位置。通常情况下,位置 1 会被用作键。可能只有当我们存储其中第一个元素,包含着记录名字的 Erlang 记录(这实际上是个伪装的元组)时,我们才会使用这个选项。

注意:在零个选项下,打开某个 ETS 数据表,与在 [set,protected,{keypos,1}] 选项下打开他,是同样的。

本章中的所有代码,都使用 protected 的 ETS 数据表。受保护的数据表特别有用,因为他们允许以几乎零成本方式共享数据。所有知道表标识符的本地进程,都可以读取数据,但只有一个进程可以更改表中的数据。

将 ETS 数据表比作黑板

受保护数据表,提供了某种类型 “黑板系统”。咱们可把一个受保护的 ETS 数据表,看作是一种命名黑板。任何知道这个黑板名字的人,都可以读这块黑板,但只有所有者才能在黑板上写。

注意:以 public 模式打开的某个 ETS 数据表,可由任何知道该数据表名字的进程写和读。在这种情况下,用户必须确保到该数据表的读和些,以一致方式完成。

ETS 的示例程序

这一小节中的示例,与三元组的生成有关。这是个很好的演示 ETS 数据表强大功能的 “炫耀” 程序。

我们的目标是编写个尝试预测给定字符串,是否是个英语单词的启发式程序。

要预测某个随机字母序列是否是个英语单词,我们将分析该单词中出现了哪些 三元组。所谓三元组,是由三个字母组成的序列。现在,并非所有的三字母序列,都能以一个有效英语单词形式出现。例如,就没有其中三个字母组合为 akjrwb 的英语单词。因此,要测试某个字符串是否可能是个英语单词,我们所要做的就是,将字符串中三个连续字母的所有序列,与从大量英语单词中生成的三元组集合对比。

咱们程序要做的第一件事,是要从一个非常大的单词集中,计算出英语中的所有三元组。为完成这一目的,我们会使用 ETS 的集合。使用 ETS 集合的决定,是基于对 ETS 集合与有序集合,及使用由 sets 模组所提供的 “纯” Erlang 集合的相对性能的一套测量。

以下是我们在接下来的几个小节中,要完成的事情:

  1. 构造一个遍历英语语言中的所有三元组的 迭代器。这将大大简化将三元组插入不同数据表类型的代码编写;

  2. 创建表示全部这些三元组的 setordered_set 类型 ETS 数据表。同时,建立一个包含所有这些三元组的集合;

  3. 测量 建立 这些不同数据表的时间;

  4. 测量 访问 这些不同数据表的时间;

  5. 根据 测量结果,选择最佳方法,并编写针对最佳方法的访问例程。

所有代码都在 lib_trigrams 中。我们将分节介绍这个模组,省略一些细节。不过不用担心,完整的代码在本书主页上的文件 code/lib_trigrams.erl 中。

三元组迭代器

我们将定义一个名为 for_each_trigram_in_the_english_language(F, A) 的函数。该函数将会函数 F,应用到英语中每个三元组。F 是个类型为 fun(Str, A) -> Afun,其中 Str 的范围是这门语言中的所有三元组,而 A 是个累加器。

要写出咱们的迭代器,我们需要一个庞大的单词列表。(注意:我(作者)在这里称其为迭代器;更严格地说,他实际上是一个折叠运算符,非常像是 lists:foldl。)我(作者)使用了个 包含 354,984 个英语单词的集合 1,生成这些三元组。使用这个单词列表,我们可将这个三元组迭代器定义如下:

1

  • 原始链接 http://www.dcs.shef.ac.uk/research/ilash/Moby/ 已不可用,这里使用了互联网档案馆的存档。

  • Project Moby

%% An iterator that iterates through all trigrams in the language
for_each_trigram_in_the_english_language(F, A0) ->
    {ok, Bin0} = file:read_file("354984si.ngl.gz"),
    Bin = zlib:gunzip(Bin0),
    scan_word_list(binary_to_list(Bin), F, A0).

scan_word_list([], _, A) ->
    A;
scan_word_list(L, F, A) ->
    {Word, L1} = get_next_word(L, []),
    A1 = scan_trigrams([$\s|Word], F, A),
    scan_word_list(L1, F, A1).

%% scan the word looking for \r\n
%% the second argument is the word (reversed) so it
%% has to be reversed when we find \r\n or run out of characters

get_next_word([$\r,$\n|T], L) -> {reverse([$\s|L]), T};
get_next_word([H|T], L)       -> get_next_word(T, [H|L]);
get_next_word([], L)          -> {reverse([$\s|L]), []}.

scan_trigrams([X,Y,Z], F, A) ->
    F([X,Y,Z], A);
scan_trigrams([X,Y,Z|T], F, A) ->
    A1 = F([X,Y,Z], A),
    scan_trigrams([Y,Z|T], F, A1);
scan_trigrams(_, _, A) ->
    A.

这里要注意两点。首先,我们使用了 zlib:gunzip(Bin) 解压缩源文件中的二进制值。这个单词表相当长,因此我们选择将其作为压缩文件,而不是原始的 ASCII 文件保存在磁盘上。其次,我们在每个单词前后,添加了个空格;在我们的三元组分析中,我们希望把空格当作普通字母处理。

建立数据表

我们如下建立了咱们的 ETS 数据表:

make_ets_ordered_set() -> make_a_set(ordered_set, "trigramsOS.tab").
make_ets_set()         -> make_a_set(set, "trigramsS.tab").

make_a_set(Type, FileName) ->
    Tab = ets:new(table, [Type]),
    F = fun(Str, _) -> ets:insert(Tab, {list_to_binary(Str)}) end,
    for_each_trigram_in_the_english_language(F, 0),
    ets:tab2file(Tab, FileName),
    Size = ets:info(Tab, size),
    ets:delete(Tab),
    Size.

请注意,当我们分离出某个由 ABC 三字母组成的三元组时,我们实际上是将 {<<"ABC">>} 这个元组,插入到表示三元组的那个 ETS 数据表中。这看起来很滑稽 -- 某个只有 一个 元素的元组。通常情况下,元组是多个元素的容器,因此只有一个元素的元组没有意义。但请记住,ETS 数据表中的所有条目都属于元组,而默认情况下,元组中的键是该元组中的第一个元素。因此,在我们的情形下,元组 {Key} 表示一个没有值的键。

现在我们来看看建立所有三元组集合的代码(这次是以 Erlang 的 sets 模组,而不是 ETS):

make_mod_set() ->
    D = sets:new(),
    F = fun(Str, Set) -> sets:add_element(list_to_binary(Str),Set) end,
    D1 = for_each_trigram_in_the_english_language(F, D),
    file:write_file("trigrams.set", [term_to_binary(D1)]).

数据表建立时间

lib_trigrams:make_tables() 这个函数,会在本章末尾出的列表中给出,会建立所有数据表。他包含了一些我们可以测量我们数据表大小,及建立这些数据表所用时间的工具。

1> lib_trigrams:make_tables().
Counting - No. of trigrams=3357707 time/trigram=0.03999247105241762
Ets ordered Set size=19.039342117559105 time/trigram=0.2565152349505183
Ets set size=19.038594523876274 time/trigram=0.12862200305148722
Module sets size=9.000560695262125 time/trigram=0.130204928542008
ok

这告诉我们,共有 330 万个三元组,处理单词表中每个三元组用了 0.04 微秒。

在 ETS 有序集合下,每个三元组的插入时间为 0.25 微秒,在 ETS 集合下为 0.13 微秒,在 Erlang 集合下为 0.13 微秒。在存储方面,ETS 集合和有序结合在每个三元组上的存储用了 19 字节,而 sets 模组在每个三元组的存储上用了 9 个字节。

数据表访问时间

好吧,这些数据表花了一些时间建立,但在这种情形下 那并不重要。现在,我们将编写一些测量访问时间的代码。我们将查找咱们数据表中的每个三元组恰好一次,然后取每个查找的平均时间。下面是执行此类计时的代码:

timer_tests() ->
    time_lookup_ets_set("Ets ordered Set", "trigramsOS.tab"),
    time_lookup_ets_set("Ets set", "trigramsS.tab"),
    time_lookup_module_sets().

time_lookup_ets_set(Type, File) ->
    {ok, Tab} = ets:file2tab(File),
    L = ets:tab2list(Tab),
    Size = length(L),
    {M, _} = timer:tc(?MODULE, lookup_all_ets, [Tab, L]),
    io:format("~s lookup=~p micro seconds~n",[Type, M/Size]),
    ets:delete(Tab).

lookup_all_ets(Tab, L) ->
    lists:foreach(fun({K}) -> ets:lookup(Tab, K) end, L).

time_lookup_module_sets() ->
    {ok, Bin} = file:read_file("trigrams.set"),
    Set = binary_to_term(Bin),
    Keys = sets:to_list(Set),
    Size = length(Keys),
    {M, _} = timer:tc(?MODULE, lookup_all_set, [Set, Keys]),
    io:format("Module set lookup=~p micro seconds~n",[M/Size]).

lookup_all_set(Set, L) ->
    lists:foreach(fun(Key) -> sets:is_element(Key, Set) end, L).

这就开始:

1> lib_trigrams:timer_tests().
Ets ordered Set lookup=0.2641809176712457 micro seconds
Ets set lookup=0.051677413325857395 micro seconds
Module set lookup=0.09914961218577703 micro seconds
ok

这些计时为每次查找的平均微秒数。

获胜者是......

好吧,这就是场走过场的比赛。ETS 的集合,以较大优势获胜。在我(作者)的机器上,sets 模组的每次查找,大约需要半微秒 -- 这已经很不错了!

注意:执行类似前面的测试,并实际测量某个特定操作要用多长时间,被视为良好的编程实践。我们无需将这种做法做到极致,而对所有操作都计时,我们只需对程序中最耗时的那些操作计时。不耗时的那些操作,应以最 优美的 方式编写。当我们因效率原因,被迫编写一些不直观易懂的丑陋代码时,那么这些代码就要好好编写文档。

现在,我们就可以编写那些尝试预测某个字符串,是否是个正确英语单词的例程了。

要判断某个字符串可能是个英语单词,我们要扫描该字符串中的所有三元组,并检查每个三元组有无出现在我们早先计算得到的三元组数据表中。函数 is_word 实现了这一过程。

%% access routines
%%   open() -> Table
%%   close(Table)
%%   is_word(Table, String) -> Bool

is_word(Tab, Str) -> is_word1(Tab, "\s" ++ Str ++ "\s").
is_word1(Tab, [_,_,_]=X) -> is_this_a_trigram(Tab, X);
is_word1(Tab, [A,B,C|D]) ->
    case is_this_a_trigram(Tab, [A,B,C]) of
	true  -> is_word1(Tab, [B,C|D]);
	false -> false
    end;
is_word1(_, _) ->
    false.
is_this_a_trigram(Tab, X) ->
    case ets:lookup(Tab, list_to_binary(X)) of
	[] -> false;
	_  -> true
    end.
open() ->
    File = filename:join(filename:dirname(code:which(?MODULE)),
			 "/trigramsS.tab"),
    {ok, Tab} = ets:file2tab(File),
    Tab.
close(Tab) -> ets:delete(Tab).

函数 openclose 会打开我们早先计算得到的 ETS 数据表,并且必须将任何对 is_word 的调用,都置于括号内。

另一个我(作者)在这里用到的技巧,是如何我定位包含三元组数据表外部文件的方式。我(作者)将这个文件,存储与当前模组加载处目录的同一个目录下。code:which(?MODULE) 会返回 ?MODULE 的对象代码所在处的文件名。

将元组存储在磁盘上

ETS 数据表,会把元组存储在内存中。DETS(Disk ETS 的缩写)则提供了磁盘上的 Erlang 元组。DETS 文件有着 2GB 的最大大小。DETS 的文件必须先打开才能使用,且使用完毕后应妥善关闭。当他们没有被正确关闭时,那么在下次被打开时,他们将被自动修复。由于修复可能耗时很长,因此在结束咱们的应用前,正确关闭他们就非常重要。

DETS 数据表有着与 ETS 表不同的共享属性。当某个 DETS 数据表被打开时,其必须被给到一个全局名字。当两个或多个本地进程,以同一名字和选项打开某个 DETS 数据表时,那么他们将共用这个数据表。在全部进程关闭该数据表表(或崩溃)前,该表将一直处于打开状态。

示例:文件名索引

我们打算创建个将文件名映射到整数,反之亦然的基于磁盘的数据表。我们将定义函数 filename2index 及其反函数 index2filename

要实现这一程序,我们将创建个 DETS 数据表,并以三种不同类型元组,填入其中。

  • {free, N}

    其中 N 为该数据表中的第一个空闲索引。当我们在该数据表中输入一个新的文件名时,他将被指派索引 N

  • {FileNameBin, K}

    FileNameBin (一个二进制值)已被指派了索引 K

  • {K, FileNameBin}

    K(一个整数)代表着文件 FilenameBin

请注意,每个新文件的添加,都会增加两个条目到该数据表:一个 File -> Index 的条目,和一个 Index -> FileName 的反向条目。这是出于效率的考量。在 ETS 或 DETS 数据表建立时,元组中只有一个项目会作为键。对不是关键字的元组元素匹配可以完成,但这样做效率很低,因为这涉及搜索整个表。当整个数据表都在磁盘上时,这更是个开销特别高的操作。

现在我们来编写这个程序。我们将从打开和关闭将存储所有文件名的 DETS 数据表的例程开始。

-module(lib_filenames_dets).
-export([open/1, close/0, test/0, filename2index/1, index2filename/1]).

open(File) ->
    io:format("dets opened:~p~n", [File]),
    Bool = filelib:is_file(File),
    case dets:open_file(?MODULE, [{file, File}]) of
	{ok, ?MODULE} ->
	    case Bool of
		true  -> void;
		false -> ok = dets:insert(?MODULE, {free,1})
	    end,
	    true;
	{error,Reason} ->
	    io:format("cannot open dets table~n"),
	    exit({eDetsOpen, File, Reason})
    end.

close() -> dets:close(?MODULE).

当新数据表被创建时,open 的代码会通过插入元组 {free, 1},自动初始化这个 DETS 数据表。当 File 存在时,filelib:is_file(File) 会返回 true;否则返回 false。请注意,dets:open_file 要么会创建一个新文件,要么打开某个现有文件,这就是为什么我们必须在调用 dets:open_file 前,检查该文件是否存在。

在这段代码中,我们曾多次使用了 ?MODULE 这个宏;?MODULE 会展开为当前模组的名字(即 lib_filenames_dets)。到 DETS 的许多调用,都需要一个唯一的原子参数作为表名。要生成唯一表名,我们就使用了模组名。由于系统中不可能有两个名字相同的 Erlang 模组,因此当我们在任何地方都遵循这一惯例时,我们将有理由确保,我们有了个作为表名的唯一名字。

我(作者)使用了 ?MODULE 这个宏,而不是每次都显式写下模组名字,因为我有在编写代码时更改模组名的习惯。使用宏时,当我更改了模组名时,代码将仍是正确的。

在我们打开该文件后,注入新文件名到数据表中就容易了。这是作为调用 filename2index 的一项副作用完成的。当文件名在数据表中时,那么其索引会被返回;否则,一个新索引会被生成,同时数据表会被更新,这次有三个元组。

filename2index(FileName) when is_binary(FileName) ->
    case dets:lookup(?MODULE, FileName) of
	[] ->
	    [{_,Free}] = dets:lookup(?MODULE, free),
	    ok = dets:insert(?MODULE,
		     [{Free,FileName},{FileName,Free},{free,Free+1}]),
	    Free;
	[{_,N}] ->
	    N
    end.

请注意,我们在数据表中存储了三个元组的方式。dets:insert 的第二个参数,要么是个元组,要么是 一个元组列表。还要注意的是,其中的文件名是以二进制值表示的。这是出于效率考量。养成以二进制值,表示 ETS 和 DETS 数据表中字符串的习惯,是个好主意。

细心的读者可能已经注意到,在 filename2index 中有个潜在竞争条件。当两个并行进程在调用 dets:insert 前,调用了 dets:lookup 时,那么 filename2index 将返回个不正确值。为使这个例程运作,我们必须确保他每次只被一个进程调用。

将索引转换为文件名,就非常简单。

index2filename(Index) when is_integer(Index) ->
    case dets:lookup(?MODULE, Index) of
	[]        -> error;
	[{_,Bin}] -> Bin
    end.

这里有一个小小的设计决策。我们必须决定,当调用 index2filename(Index),却没有与该索引关联的文件名时,该怎么办。我们可以通过调用 exit(ebadIndex) 崩溃掉调用者,但在这种情形下,我们选择了种更温和的方法:我们只返回原子 error。调用者可以区分开有效的文件名与不正确值,因为所有有效的返回文件名,都是二进制类型的值。

还要注意 filename2indexindex2filename 中的条件测试。这些测试将检查参数是否有着要求的类型。对这些参数进行测试是个好主意,因为将错误类型的数据,输入到 DETS 数据表中,会导致难以调试的情况。我们可以设想一下,在某个数据表中以错误类型存储数据,并在几个月后再读取该表时,届时要做什么补救就已经为时已晚了。在将数据添加到数据表前,最好要检查所有数据是否正确。

哪些我们还没有讨论过?

ETS 和 DETS 的数据表支持本章中我们还未讨论到的许多操作。这些操作可分为以下几个类别:

  • 基于模式的对象获取及删除
  • 在 ETS 与 DETS 数据表间,及 ETS 数据表与磁盘文件间的转换
  • 找出数据表的资源使用情况
  • 遍历数据表的所有元素
  • 修复损坏的 DETS 数据表
  • 数据表的可视化

有关 ETSDETS 的更多信息,可在线上找到。

ETS 和 DETS 数据表,是为高效地在内存中和磁盘上存储 Erlang 项而设计,但这不是故事的全部。对于更复杂的数据存储,我们需要数据库。

在下一章中,我们将引入 Mnesia,他是个用 Erlang 编写的实时数据库,也是标准 Erlang 发行版的一部分。Mnesia 内部用到了 ETS 和 DETS 数据表,同时许多从 etsdets 两个模组导出的许多例程,都是为 Mnesia 的内部使用而准备的。Mnesia 可以完成那些以单个 ETS 与 DETS 数据表,无法完成的所有操作。例如,我们可在主键之外建立索引,因此我们在 filename2index 示例中,用到的双重插入技巧就没有必要了。实际上,Mnesia 将创建多个 ETS 或 DETS 数据表完成这点,但该细节对用户隐藏了。

练习

  1. Mod:module_info(exports) 返回模组 Mod 中,所有导出函数的列表。请使用这个函数,找出 Erlang 系统库中所有导出的函数。构造一个键值的查找表,其中键为 {Function,Arity} 对,值为模组名。请将这些数据存储在 ETS 及 DETS 数据表中。

    提示:请使用 code:lib_dir()code:lib_dir(LibName),查找系统中全部模组的名字。

  2. 请构造一个共用的 ETS 计数数据表。实现一个可添加到咱们代码中的名为 count:me(Mod,Line) 的函数。通过将一些 count:me(?MODULE,?LINE) 行添加到咱们的代码,咱们便可调用该函数。每次该函数被调用时,其应递增一个计算该行被执行的次数的计数器。请实现初始化与读取计数器的一些例程;

  3. 请编写一个检测文本中的抄袭行为的程序。为实现这一目的,请使用一种两步算法。在第 1 步中,会将文本拆分为一些 40 个字符的数据块,并计算每个 40 字符数据块的校验和。将校验和与文件名存储在一个 ETS 的数据表中。第 2 步,计算数据中每个 40 字符块的校验和,然后与 ETS 数据表中的校验和比较。

    提示:咱们将需计算 “滚动校验和”2 才能做到这点。例如,当 C1 = B1 + B2 + ... B40C2 = B2 + B3 + ... B41 时,那么经由观察 C2 = C1 + B41 - B1C2 即可被快速计算出来。

Last change: 2025-10-15, commit: d901196

小额打赏,赞助 xfoss.com 长存......

微信 | 支付宝

若这里内容有帮助到你,请选择上述方式向 xfoss.com 捐赠。