第十五章 ETS和DETS:大数据的存储机制

ETS和DETS都提供"键-值"搜索表, 只不过ETS驻留在内存而DETS驻留在磁盘, 因此ETS高效但数据存储是临时的, DETS数据存储是持久的且节省内存但比较低效。

15.1 表的基本操作

  • 创建和打开表

ets:new或dets:open_file

  • 插入表

insert(TableName, X)

  • 查找元组

lookup(TableName, Key)

  • 释放表

ets:delete(TableId)或dets:close(TableId)

15.2 表的类型

  • set表
    每个元组的键值不能相同
  • ordered set表
    排序的set表
  • bag表
    多个元组可以有相同的键值, 但不能有两个完全相同的元组
  • duplicate bag表
    多个元组可以有相同的键值, 且同一个元组可以在表中出现多次

代码示例

-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).

运行结果:

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

15.3 ETS表的效率考虑

set表保证键值不相同, 因此相对耗费空间;ordered_set保证了排序, 因此相对耗费时间。
因为bag表插入数据时需要比较是否存在相同的键值, 因此如果大量元组存在相同键值, 使用bag表将会非常低效。
ETS表隶属于创建它的进程, 随进程消亡而消亡, 因此无需考虑垃圾回收。
尽可能的使用二进制数据。

15.4 创建ETS表

创建ETS表的函数原型

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

其中[Opt]的取值:

# 表类型
set | ordered_set | bag | duplicate_bag

# 权限
private      只有所有者进程可以读写
public       所有可以获取TableId的进程都可以读写
protected    所有可以获取TableId的进程都可以读, 但只有所有者进程可以写

# 命名
named_table  是否可以使用Name来操作表

# 指定键的位置
{keypos, K}  通常是第一个位置, 当需要存储记录时, 第一个位置是记录的名字, 因此需要指定键的位置 

# 默认选项为[set, protected, {keypos, 1}]

15.5 ETS程序示例

15.5.1 三字索引迭代器

%% 从压缩文件中读取英文单词, 每个单词都使用F函数处理
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).

%% 单词匹配, 遇到换行符则添加空格
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.

15.5.2 构造表

%% 分别创建两者类型的ets表
make_ets_ordered_set() ->make_a_set(ordered_set, "trigramsOS.tab").
make_ets_set()         ->make_a_set(set, "trigramsS.tab").

%% 根据指定类型创建ets表, 导入数据后转成文件保存
make_a_set(Type, FileName) ->
    Tab = ets:new(table, [Type]),
    %% 声明遍历英文单词时所使用的函数
    %% 这里将每个单词插入到ets表
    F = fun(Str, _) ->ets:insert(Tab, {list_to_binary(Str)}) end,
    for_each_trigram_in_the_english_language(F, 0),
    %% 转换为文件, 获取记录数后在内存中删除ets表
    ets:tab2file(Tab, FileName),
    Size = ets:info(Tab, size),
    ets:delete(Tab),
    Size.

make_mod_set() ->
    %% 使用sets存储单词
    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)]).

15.5.3 构造表有多快

%% 统计有多少个单词
how_many_trigrams() ->
    %% 遇到一个单词就将累加器增加1
    F = fun(_, N) ->1 + N end,
    for_each_trigram_in_the_english_language(F, 0).

make_tables() ->
    %% 统计有多少个三字索引
    {Micro1, N} = timer:tc(?MODULE, how_many_trigrams, []),
    io:format("Counting - No of trigrams=~p time/trigram=~p~n", [N, Micro1/N]),

    %% 统计排序的ets表平均占用存储和时间
    {Micro2, Ntri} = timer:tc(?MODULE, make_ets_ordered_set, []),
    FileSize1 = filelib:file_size("trigramsOS.tab"),
    io:format("Ets ordered Set size=~p time/trigram=~p~n", [FileSize1/Ntri, Micro2/N]),

    %% 统计普通ets表平均占用存储和时间
    {Micro3, _} = timer:tc(?MODULE, make_ets_set, []),
    FileSize2 = filelib:file_size("trigramsS.tab"),
    io:format("Ets set size=~p time/trigram=~p~n", [FileSize2/Ntri, Micro3/N]),

    %% 统计使用set方式平均占用存储和时间
    {Micro4, _} = timer:tc(?MODULE, make_mod_set, []),
    FileSize3 = filelib:file_size("trigrams.set"),
    io:format("Module sets size=~p time/trigram=~p~n", [FileSize3/Ntri, Micro4/N]).

运行结果:

1> c(lib_trigrams).
{ok,lib_trigrams}
2> lib_trigrams:make_tables().
Counting - No of trigrams=3357707 time/trigram=0.1504470163715893
Ets ordered Set size=19.029156153630503 time/trigram=0.6226862558287546
Ets set size=19.028408559947668 time/trigram=0.40671625010758833
Module sets size=9.433978132884777 time/trigram=1.728039700903027
ok

共有3 357 707个三字索引, 处理每个三字索引平均时间为0.15微秒;
ordered_set类型的ETS表平均每个三字索引占用19字节耗费0.62微秒;
普通类型的ETS表平均每个三字索引占用19字节耗费0.41微秒;
而使用Erlang的集合模块平均存储占用9字节耗费1.73微秒

15.5.4 访问表有多快

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.45313522100738246 micro seconds
Ets set lookup=0.1906363891225119 micro seconds
Module set lookup=0.31632557704887393 micro seconds
ok

15.5.5 胜出的是…

%% 查询某个字符串是否为单词
%% 表中存储时已经添加了空格, 因此这里要补全
is_word(Tab, Str) ->is_word1(Tab, "\s" ++ Str ++ "\s").

%% 查询符合[A, B, C]格式的字符串是否为单词
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.

%% 查询ETS表中是否存在此索引
is_this_a_trigram(Tab, X) ->
    case ets:lookup(Tab, list_to_binary(X)) of
        [] ->false;
        _  ->true
    end.

%% 找到与源码文件同目录下的数据文件并转换成ETS表
open() ->
    {ok, I} = ets:file2tab(filename:dirname(code:which(?MODULE)) ++ "/trigramsS.tab"),
    I.

close(Tab) ->ets:delete(Tab).

运行结果

1> Tab = lib_trigrams:open().
16402
2> lib_trigrams:is_word(Tab, "example").
true

15.6 DETS

如果在打开DETS文件后没有关闭, 则Erlang将自动进行修复, 而修复可能会耗费大量的时间, 因此应在使用完后关闭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),
    %% 使用?MODULE宏获取文件名作为TableName
    case dets:open_file(?MODULE, [{file, File}]) of
        {ok, ?MODULE} ->
            case Bool of
                true  ->void;
                %% 插入键为free, 值为1的记录, 用于统计表中记录的总数
                false ->ok = dets:insert(?MODULE, {free, 1})
            end,
            true;
        {error, _Reason} ->
            io:format("cannot open dets table~n"),
            exit(eDetsOpen)
    end.

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

%% 将二进制格式的文件名插入到dets表
filename2index(FileName) when is_binary(FileName) ->
    %% 插入前先查询文件名是否已经存在
    case dets:lookup(?MODULE, FileName) of
        [] ->
            %% 不存在则先查询最大索引, 然后将索引-文件名, 文件名-索引, free-最大索引插入到dets表
            [{_, Free}] = dets:lookup(?MODULE, free),
            ok = dets:insert(?MODULE, [{Free, FileName}, {FileName, Free}, {free, Free+1}]), 
            Free;
        %% 存在则直接返回索引值
        [{_, N}] ->N
    end.

%% 根据索引值查询文件名
index2filename(Index) when is_integer(Index) ->
    case dets:lookup(?MODULE, Index) of
        []         ->error;
        [{_, Bin}] ->Bin
    end.

test() ->   
    %% 每次测试都重建dets表
    file:delete("./filenames.dets"),
    open("./filenames.dets"),
    %% 使用正则查找当前目录下的所有erl文件添加到dets表中
    F = lib_files_find:files(".", ".*[.](erl).*$", true),
    lists:foreach(fun(I) ->filename2index(list_to_binary(I)) end, F).

运行结果:

1> c(lib_filenames_dets).
{ok,lib_filenames_dets}
2> lib_filenames_dets:test().
dets opened:"./filenames.dets"
ok
3> lib_filenames_dets:index2filename(1).
<<"./lib_trigrams.erl">>
4> lib_filenames_dets:index2filename(2).
<<"./lib_files_find.erl">>
5> lib_filenames_dets:index2filename(3).
<<"./lib_filenames_dets.erl">>
6> lib_filenames_dets:index2filename(4).
<<"./ets_test.erl">>
7> lib_filenames_dets:filename2index(list_to_binary("./ets_test.erl")).
4

15.7 我们没有提及的部分

ETS和DETS的在线手册:
http://www.erlang.org/doc/man/ets.html
http://www.erlang.org/doc/man/dets.html

posted @ 2020-08-24 10:17  养诚  阅读(669)  评论(0编辑  收藏  举报