1. 程式人生 > 實用技巧 >第十五章 ETS和DETS:大資料的儲存機制

第十五章 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