使用 ETS 和 DETS 存储数据
ets
和 dets
是两个咱们可用作大量 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:new
或dets: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 数据表强大功能的 “炫耀” 程序。
我们的目标是编写个尝试预测给定字符串,是否是个英语单词的启发式程序。
要预测某个随机字母序列是否是个英语单词,我们将分析该单词中出现了哪些 三元组。所谓三元组,是由三个字母组成的序列。现在,并非所有的三字母序列,都能以一个有效英语单词形式出现。例如,就没有其中三个字母组合为 akj
及 rwb
的英语单词。因此,要测试某个字符串是否可能是个英语单词,我们所要做的就是,将字符串中三个连续字母的所有序列,与从大量英语单词中生成的三元组集合对比。
咱们程序要做的第一件事,是要从一个非常大的单词集中,计算出英语中的所有三元组。为完成这一目的,我们会使用 ETS 的集合。使用 ETS 集合的决定,是基于对 ETS 集合与有序集合,及使用由 sets
模组所提供的 “纯” Erlang 集合的相对性能的一套测量。
以下是我们在接下来的几个小节中,要完成的事情:
-
构造一个遍历英语语言中的所有三元组的 迭代器。这将大大简化将三元组插入不同数据表类型的代码编写;
-
创建表示全部这些三元组的
set
和ordered_set
类型 ETS 数据表。同时,建立一个包含所有这些三元组的集合; -
测量 建立 这些不同数据表的时间;
-
测量 访问 这些不同数据表的时间;
-
根据 测量结果,选择最佳方法,并编写针对最佳方法的访问例程。
所有代码都在 lib_trigrams
中。我们将分节介绍这个模组,省略一些细节。不过不用担心,完整的代码在本书主页上的文件 code/lib_trigrams.erl
中。
三元组迭代器
我们将定义一个名为 for_each_trigram_in_the_english_language(F, A)
的函数。该函数将会函数 F
,应用到英语中每个三元组。F
是个类型为 fun(Str, A) -> A
的 fun
,其中 Str
的范围是这门语言中的所有三元组,而 A
是个累加器。
要写出咱们的迭代器,我们需要一个庞大的单词列表。(注意:我(作者)在这里称其为迭代器;更严格地说,他实际上是一个折叠运算符,非常像是 lists:foldl
。)我(作者)使用了个 包含 354,984 个英语单词的集合 1,生成这些三元组。使用这个单词列表,我们可将这个三元组迭代器定义如下:
注:
1:
原始链接 http://www.dcs.shef.ac.uk/research/ilash/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).
函数 open
和 close
会打开我们早先计算得到的 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
。调用者可以区分开有效的文件名与不正确值,因为所有有效的返回文件名,都是二进制类型的值。
还要注意 filename2index
和 index2filename
中的条件测试。这些测试将检查参数是否有着要求的类型。对这些参数进行测试是个好主意,因为将错误类型的数据,输入到 DETS 数据表中,会导致难以调试的情况。我们可以设想一下,在某个数据表中以错误类型存储数据,并在几个月后再读取该表时,届时要做什么补救就已经为时已晚了。在将数据添加到数据表前,最好要检查所有数据是否正确。
哪些我们还没有讨论过?
ETS 和 DETS 的数据表支持本章中我们还未讨论到的许多操作。这些操作可分为以下几个类别:
- 基于模式的对象获取及删除
- 在 ETS 与 DETS 数据表间,及 ETS 数据表与磁盘文件间的转换
- 找出数据表的资源使用情况
- 遍历数据表的所有元素
- 修复损坏的 DETS 数据表
- 数据表的可视化
ETS 和 DETS 数据表,是为高效地在内存中和磁盘上存储 Erlang 项而设计,但这不是故事的全部。对于更复杂的数据存储,我们需要数据库。
在下一章中,我们将引入 Mnesia,他是个用 Erlang 编写的实时数据库,也是标准 Erlang 发行版的一部分。Mnesia 内部用到了 ETS 和 DETS 数据表,同时许多从 ets
和 dets
两个模组导出的许多例程,都是为 Mnesia 的内部使用而准备的。Mnesia 可以完成那些以单个 ETS 与 DETS 数据表,无法完成的所有操作。例如,我们可在主键之外建立索引,因此我们在 filename2index
示例中,用到的双重插入技巧就没有必要了。实际上,Mnesia 将创建多个 ETS 或 DETS 数据表完成这点,但该细节对用户隐藏了。
练习
-
Mod:module_info(exports)
返回模组Mod
中,所有导出函数的列表。请使用这个函数,找出 Erlang 系统库中所有导出的函数。构造一个键值的查找表,其中键为{Function,Arity}
对,值为模组名。请将这些数据存储在 ETS 及 DETS 数据表中。提示:请使用
code:lib_dir()
和code:lib_dir(LibName)
,查找系统中全部模组的名字。 -
请构造一个共用的 ETS 计数数据表。实现一个可添加到咱们代码中的名为
count:me(Mod,Line)
的函数。通过将一些count:me(?MODULE,?LINE)
行添加到咱们的代码,咱们便可调用该函数。每次该函数被调用时,其应递增一个计算该行被执行的次数的计数器。请实现初始化与读取计数器的一些例程; -
请编写一个检测文本中的抄袭行为的程序。为实现这一目的,请使用一种两步算法。在第 1 步中,会将文本拆分为一些 40 个字符的数据块,并计算每个 40 字符数据块的校验和。将校验和与文件名存储在一个 ETS 的数据表中。第 2 步,计算数据中每个 40 字符块的校验和,然后与 ETS 数据表中的校验和比较。
提示:咱们将需计算 “滚动校验和”2 才能做到这点。例如,当
C1 = B1 + B2 + ... B40
及C2 = B2 + B3 + ... B41
时,那么经由观察C2 = C1 + B41 - B1
,C2
即可被快速计算出来。