Erlang Thursday - ETS介绍第五篇:keypos,compressed,read_conncurrency 和 write_concurren

今天的Erlang Thursday继续介绍ETS并且就上星期我所预告的,要研究ETS表的 keypos 设置以及其他一下设置。

首先我们来看看 keypos 设置。

keypos是被存储的元组的基于1的索引,并且将被作为表的数据项的键。如果你记得我们第三篇介绍ETS的关于不同表类型的文章所描述,表用这个索引作为它们的键进行比较来决定数据是否唯一。

如果我们创建一个新表而不指定它的 keypos 选项,则 keypos 默认是1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Table = ets:new(some_name, []).
% 20498
ets:info(Table).
% [{read_concurrency,false},
% {write_concurrency,false},
% {compressed,false},
% {memory,305},
% {owner,<0.50.0>},
% {heir,none},
% {name,some_name},
% {size,0},
% {node,nonode@nohost},
% {named_table,false},
% {type,set},
% {keypos,1},
% {protection,protected}]

为了显示keypos的作用,我们将创建一些数据插入到我们的ETS表,这样我们就能看到keypos的作用。

1
2
3
4
5
6
7
8
9
10
Item1 = {1, a}.
% {1,a}
Item2 = {1.0, "a"}.
% {1.0,"a"}
Item3 = {1, "one"}.
% {1,"one"}
Item4 = {a, "a"}.
% {a,"a"}
Item5 = {"a", a}.
% {"a",a}

上述数据,我们在二元素元组里既有第一元素重复的也有第二元素重复的。

我们将继续依次把所有元素插入表中,我们要记住这个表是set类型的,所以任何新数据插入都会覆盖与它有相同键的前面插入的数据值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ets:insert(Table, Item1).
% true
ets:tab2list(Table).
% [{1,a}]
ets:insert(Table, Item2).
% true
ets:tab2list(Table).
% [{1,a},{1.0,"a"}]
ets:insert(Table, Item3).
% true
ets:tab2list(Table).
% [{1,"one"},{1.0,"a"}]
ets:insert(Table, Item4).
% true
ets:tab2list(Table).
% [{1,"one"},{a,"a"},{1.0,"a"}]
ets:insert(Table, Item5).
% true
ets:tab2list(Table).
% [{"a",a},{1,"one"},{a,"a"},{1.0,"a"}]

当我们如上例子插入Item3,它覆盖表中的Item1,因为它们的元组第一个元素都是1。

我们现在创建一个kepos是2的新表,然后看看按上述例子同样的步骤来插入数据会有什么样的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
KeyPosTwo = ets:new(key_pos_2, [{keypos, 2}]).
% 24595
ets:insert(KeyPosTwo, Item1).
% true
ets:tab2list(KeyPosTwo).
% [{1,a}]
ets:insert(KeyPosTwo, Item2).
% true
ets:tab2list(KeyPosTwo).
% [{1.0,"a"},{1,a}]
ets:insert(KeyPosTwo, Item3).
% true
ets:tab2list(KeyPosTwo).
% [{1,"one"},{1.0,"a"},{1,a}]
ets:insert(KeyPosTwo, Item4).
% true
ets:tab2list(KeyPosTwo).
% [{1,"one"},{a,"a"},{1,a}]
ets:insert(KeyPosTwo, Item5).
% true
ets:tab2list(KeyPosTwo).
% [{1,"one"},{a,"a"},{"a",a}]

这个例子里,当插入Item4时,就发生了覆盖,因为Item2和Item4的第二个元素都是“a”。然后插入Item5时,会覆盖Item1,因为它们的第二个元素都是原子a。

如果我们设置的keypos是其他值,比如说3,然后我们尝试插入一个元素小于3的元组,我们会得到一个bag argument异常。

1
2
3
4
5
6
KeyPosThree = ets:new(key_pos_3, [{keypos, 3}]).
% 28692
ets:insert(KeyPosThree, Item1).
% ** exception error: bad argument
% in function ets:insert/2
% called as ets:insert(28692,{1,a})

现在我们来看看创建表的时候用compressed选项。

当创建一个新表,默认是不压缩的,因为我们可以从下面例子里看到表信息显示{compressed, false}。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
UncompressedTable = ets:new(uc, []).
% 32786
ets:info(UncompressedTable).
% [{read_concurrency,false},
% {write_concurrency,false},
% {compressed,false},
% {memory,305},
% {owner,<0.81.0>},
% {heir,none},
% {name,uc},
% {size,0},
% {node,nonode@nohost},
% {named_table,false},
% {type,set},
% {keypos,1},
% {protection,protected}]

我们创建一个新表,用了compressed选项,然后用ets:info/1查看表信息,我们看到{compressed, true}。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CompressedTable = ets:new(uc, [compressed]).
% 45074
ets:info(CompressedTable).
% [{read_concurrency,false},
% {write_concurrency,false},
% {compressed,true},
% {memory,305},
% {owner,<0.81.0>},
% {heir,none},
% {name,uc},
% {size,0},
% {node,nonode@nohost},
% {named_table,false},
% {type,set},
% {keypos,1},
% {protection,protected}]

至少根据官方文档的说法,compressed选项会使得数据以更压缩的格式存储而减少内存的消耗。文档也警告这也会造成获取元素元组的操作更慢,并且键是不压缩存储的,至少在当前的版本里是这样。

让我们来看看compressed对内存的消耗有什么样的不同。

我们将给两种表分别插入100000条记录,然后看它们的内存大小。插入纪录的格式是{X, X},X从1到100000。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
lists:foreach(fun(X) -> ets:insert(CompressedTable, {X, X}) end,
lists:seq(1, 100000)).
% ok
lists:foreach(fun(X) -> ets:insert(UncompressedTable, {X, X}) end,
lists:seq(1, 100000)).
% ok
ets:info(UncompressedTable).
% [{read_concurrency,false},
% {write_concurrency,false},
% {compressed,false},
% {memory,714643},
% {owner,<0.109.0>},
% {heir,none},
% {name,uc},
% {size,100000},
% {node,nonode@nohost},
% {named_table,false},
% {type,set},
% {keypos,1},
% {protection,protected}]
ets:info(CompressedTable).
% [{read_concurrency,false},
% {write_concurrency,false},
% {compressed,true},
% {memory,814643},
% {owner,<0.109.0>},
% {heir,none},
% {name,uc},
% {size,100000},
% {node,nonode@nohost},
% {named_table,false},
% {type,set},
% {keypos,1},
% {protection,protected}]

有意思!

压缩表的内存大小是814643,而非压缩表的内存大小却小一点,是714643。

可能是对整数值的压缩效果不好,所以我们再做一次,这次是用字符串替换元组的第二个元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
lists:foreach(fun(X) -> ets:insert(UncompressedTable, {X, integer_to_list(X)}) end,
lists:seq(1, 100000)).
% ok
lists:foreach(fun(X) -> ets:insert(CompressedTable, {X, integer_to_list(X)}) end,
lists:seq(1, 100000)).
% ok
ets:info(CompressedTable).
% [{read_concurrency,false},
% {write_concurrency,false},
% {compressed,true},
% {memory,914644},
% {owner,<0.109.0>},
% {heir,none},
% {name,uc},
% {size,100000},
% {node,nonode@nohost},
% {named_table,false},
% {type,set},
% {keypos,1},
% {protection,protected}]
ets:info(UncompressedTable).
% [{read_concurrency,false},
% {write_concurrency,false},
% {compressed,false},
% {memory,1692433},
% {owner,<0.109.0>},
% {heir,none},
% {name,uc},
% {size,100000},
% {node,nonode@nohost},
% {named_table,false},
% {type,set},
% {keypos,1},
% {protection,protected}]

用字符串替换元素第二元素后,压缩表内存大小是914644,而非压缩表的内存大小是1692433。

所以当你要决定是否使用压缩表的时候除了要更仔细的考虑你将用何种方式如何匹配数据外,你还要考虑你将要放入ETS表的数据是什么样的类型。

最后两个要讨论的选项是 read_concurrency 和 write_concurrency。

read_concurrency 默认被设置为false,根据官方文档的说法这个选项在“读比写操作频繁很多,或者当并发读写的量非常巨大的时候”最适合设置为true。

因此如果你有一个表有大量的读操作,而写操作零零散散,这个时候你应该设置read_concurrency为true,因为官方文档的说法是在读和写之间切换是很昂贵的。

write_concurrency 默认被设置为false,这会使得当一个写操作正在进行的时候会造成其它并发的写操作阻塞。当把该选项设置为true,则同一个表的不同的元组可以通过并发进程写入,并且不影响任何ordered_set类型表。

ETS的一系列介绍文章到此告一段路。下星期我们将开始研究用ETS和ETS表进行不同的操作。

原文链接: https://www.proctor-it.com/erlang-thursday-ets-introduction-part-5/