查看源码 Mnesia 的其他特性
前面的章节描述了如何开始使用 Mnesia
以及如何构建 Mnesia
数据库。本节描述了在构建分布式、容错的 Mnesia
数据库时可用的更高级功能。包括以下主题
- 索引
- 分布式和容错
- 表分片
- 本地内容表
- 无盘节点
- 关于模式管理的更多信息
Mnesia
事件处理- 调试
Mnesia
应用程序 Mnesia
中的并发进程- 原型设计
- 使用
Mnesia
进行面向对象编程
索引
如果记录的键已知,则可以高效地执行数据检索和匹配。相反,如果键未知,则必须搜索表中的所有记录。表越大,耗时就越长。为了解决这个问题,Mnesia
索引功能用于改进数据的检索和记录的匹配。
以下两个函数操作现有表上的索引
mnesia:add_table_index(Tab, AttributeName) -> {aborted, R} | {atomic, ok}
mnesia:del_table_index(Tab, AttributeName) -> {aborted, R} | {atomic, ok}
这些函数在由 AttributeName
定义的字段上创建或删除表索引。为了说明这一点,在表定义 (employee, {emp_no, name, salary, sex, phone, room_no})
上添加索引,这是来自 Company
数据库的示例表。在元素 salary
上添加索引的函数可以表示为 mnesia:add_table_index(employee, salary)
。
Mnesia
的索引功能与以下三个函数一起使用,这些函数基于数据库中的索引条目检索和匹配记录
mnesia:index_read(Tab, SecondaryKey, AttributeName) -> transaction abort | RecordList
通过在索引中查找SecondaryKey
来查找主键,从而避免了对整个表的详尽搜索。mnesia:index_match_object(Pattern, AttributeName) -> transaction abort | RecordList
通过在索引中查找辅助键来查找主键,从而避免了对整个表的详尽搜索。辅助键在Pattern
的字段AttributeName
中找到。辅助键必须绑定。mnesia:match_object(Pattern) -> transaction abort | RecordList
使用索引来避免对整个表的详尽搜索。与之前的函数不同,只要辅助键已绑定,此函数就可以使用任何索引。
这些函数在 模式匹配 中有更详细的描述和示例。
分布式和容错
Mnesia
是一个分布式、容错的 DBMS。表可以通过多种方式在不同的 Erlang 节点上复制。Mnesia
程序员不需要说明不同表的位置,只需要在程序代码中指定不同表的名称。这被称为“位置透明性”,是一个重要的概念。特别是
程序的工作方式与数据的位置无关。数据位于本地节点还是远程节点没有区别。
请注意,如果数据位于远程节点上,则程序运行速度会较慢。
可以重新配置数据库,并且可以在节点之间移动表。这些操作不会影响用户程序。
之前已经展示了每个表都有许多系统属性,例如 index
和 type
。
表属性在创建表时指定。例如,以下函数创建一个具有两个 RAM 副本的表
mnesia:create_table(foo,
[{ram_copies, [N1, N2]},
{attributes, record_info(fields, foo)}]).
表还可以具有以下属性,其中每个属性都将 Erlang 节点列表作为其值
ram_copies
。节点列表的值是 Erlang 节点列表,并且该表的 RAM 副本驻留在列表中的每个节点上。请注意,当程序对这些副本执行写入操作时,不会执行磁盘操作。但是,如果需要永久 RAM 副本,则可以使用以下替代方法
- 函数
mnesia:dump_tables/1
可用于将 RAM 表副本转储到磁盘。 - 表副本可以从 RAM 或磁盘进行备份,如果使用此函数转储到磁盘。
- 函数
disc_copies
。该属性的值是 Erlang 节点列表,并且该表的副本同时驻留在列表中的每个节点的 RAM 和磁盘上。寻址到表的写入操作会寻址到表的 RAM 和磁盘副本。disc_only_copies
。该属性的值是 Erlang 节点列表,并且该表的副本仅作为磁盘副本驻留在列表中的每个节点上。这种类型的表副本的主要缺点是访问速度。主要优点是表不会占用内存空间。
此外,可以设置和更改表属性。有关详细信息,请参阅 定义模式。
使用多个表副本主要有两个原因:容错和速度。请注意,表复制为这两个系统要求提供了解决方案。
如果有两个活动的表副本,如果一个副本发生故障,所有信息仍然可用。这在许多应用程序中可能是一个重要的属性。此外,如果表副本存在于两个特定节点上,则在这些节点中的任一节点上执行的应用程序可以从表中读取数据,而无需访问网络。网络操作比本地操作慢得多,并且消耗更多资源。
对于经常读取数据但很少写入数据的分布式应用程序,创建表副本以在本地节点上实现快速读取操作可能是有利的。复制的主要缺点是写入数据的时间增加。如果一个表有两个副本,则每个写入操作都必须访问两个表副本。由于这些写入操作之一必须是网络操作,因此对复制表执行写入操作比对非复制表执行写入操作要昂贵得多。
表分片
概念
为了处理大型表,引入了表分片的概念。其思想是将表拆分为几个可管理的分片。每个分片都实现为一个一流的 Mnesia
表,并且可以像任何其他表一样进行复制、拥有索引等等。但是这些表不能具有 local_content
或激活 snmp
连接。
为了能够访问分片表中的记录,Mnesia
必须确定实际记录属于哪个分片。这是由模块 mnesia_frag
完成的,该模块实现了 mnesia_access
回调行为。建议阅读关于函数 mnesia:activity/4
的文档,以了解如何将 mnesia_frag
用作 mnesia_access
回调模块。
在每次记录访问时,mnesia_frag
首先从记录键计算哈希值。其次,从哈希值确定表分片的名称。最后,实际的表访问由与非分片表相同的函数执行。当键事先未知时,会搜索所有分片以查找匹配的记录。
请注意,在 ordered_set
表中,记录是按分片排序的,并且 select
和 match_object
返回的结果以及 first
、next
、prev
和 last
返回的结果中的顺序是未定义的。
以下代码说明了如何将 Mnesia
表转换为分片表,以及以后如何添加更多分片
Eshell V4.7.3.3 (abort with ^G)
(a@sam)1> mnesia:start().
ok
(a@sam)2> mnesia:system_info(running_db_nodes).
[b@sam,c@sam,a@sam]
(a@sam)3> Tab = dictionary.
dictionary
(a@sam)4> mnesia:create_table(Tab, [{ram_copies, [a@sam, b@sam]}]).
{atomic,ok}
(a@sam)5> Write = fun(Keys) -> [mnesia:write({Tab,K,-K}) || K <- Keys], ok end.
#Fun<erl_eval>
(a@sam)6> mnesia:activity(sync_dirty, Write, [lists:seq(1, 256)], mnesia_frag).
ok
(a@sam)7> mnesia:change_table_frag(Tab, {activate, []}).
{atomic,ok}
(a@sam)8> mnesia:table_info(Tab, frag_properties).
[{base_table,dictionary},
{foreign_key,undefined},
{n_doubles,0},
{n_fragments,1},
{next_n_to_split,1},
{node_pool,[a@sam,b@sam,c@sam]}]
(a@sam)9> Info = fun(Item) -> mnesia:table_info(Tab, Item) end.
#Fun<erl_eval>
(a@sam)10> Dist = mnesia:activity(sync_dirty, Info, [frag_dist], mnesia_frag).
[{c@sam,0},{a@sam,1},{b@sam,1}]
(a@sam)11> mnesia:change_table_frag(Tab, {add_frag, Dist}).
{atomic,ok}
(a@sam)12> Dist2 = mnesia:activity(sync_dirty, Info, [frag_dist], mnesia_frag).
[{b@sam,1},{c@sam,1},{a@sam,2}]
(a@sam)13> mnesia:change_table_frag(Tab, {add_frag, Dist2}).
{atomic,ok}
(a@sam)14> Dist3 = mnesia:activity(sync_dirty, Info, [frag_dist], mnesia_frag).
[{a@sam,2},{b@sam,2},{c@sam,2}]
(a@sam)15> mnesia:change_table_frag(Tab, {add_frag, Dist3}).
{atomic,ok}
(a@sam)16> Read = fun(Key) -> mnesia:read({Tab, Key}) end.
#Fun<erl_eval>
(a@sam)17> mnesia:activity(transaction, Read, [12], mnesia_frag).
[{dictionary,12,-12}]
(a@sam)18> mnesia:activity(sync_dirty, Info, [frag_size], mnesia_frag).
[{dictionary,64},
{dictionary_frag2,64},
{dictionary_frag3,64},
{dictionary_frag4,64}]
(a@sam)19>
分片属性
可以使用函数 mnesia:table_info(Tab, frag_properties)
读取表属性 frag_properties
。分片属性是具有元数为 2 的标记元组列表。默认情况下,该列表为空,但是当它非空时,它会触发 Mnesia
将表视为已分片。分片属性如下
{n_fragments, Int}
-n_fragments
调节表当前拥有的分片数量。此属性可以在表创建时显式设置,并在以后使用{add_frag, NodesOrDist}
或del_frag
更改。n_fragments
默认为1
。{node_pool, List}
- 节点池包含一个节点列表,可以在表创建时显式设置,并在以后使用{add_node, Node}
或{del_node, Node}
更改。在表创建时,Mnesia
尝试将每个分片的副本均匀地分布在节点池中的所有节点上。希望所有节点最终都具有相同数量的副本。node_pool
默认为函数mnesia:system_info(db_nodes)
的返回值。{n_ram_copies, Int}
- 调节每个分片要拥有的ram_copies
副本的数量。此属性可以在表创建时显式设置。默认为0
,但是如果n_disc_copies
和n_disc_only_copies
也为0
,则n_ram_copies
默认为1
。{n_disc_copies, Int}
- 规定每个分片拥有的disc_copies
副本数量。此属性可以在创建表时显式设置。默认为0
。{n_disc_only_copies, Int}
- 规定每个分片拥有的disc_only_copies
副本数量。此属性可以在创建表时显式设置。默认为0
。{foreign_key, ForeignKey}
-ForeignKey
可以是原子undefined
,也可以是元组{ForeignTab, Attr}
,其中Attr
表示另一个名为ForeignTab
的分片表中要解释为键的属性。Mnesia
确保此表和外键表中的分片数量始终相同。当添加或删除分片时,
Mnesia
会自动将操作传播到所有具有引用此表的外键的分片表。不使用记录键来确定要访问哪个分片,而是使用字段Attr
的值。此功能使得可以将不同表中的记录自动共置到同一节点。foreign_key
默认为undefined
。但是,如果将外键设置为其他值,则会导致其他分片属性的默认值与外键表的实际分片属性相同。{hash_module, Atom}
- 允许定义替代哈希方案。该模块必须实现mnesia_frag_hash
回调行为。此属性可以在创建表时显式设置。默认为mnesia_frag_hash
。{hash_state, Term}
- 允许对通用哈希模块进行特定于表的参数化。此属性可以在创建表时显式设置。默认为undefined
。Eshell V4.7.3.3 (abort with ^G) (a@sam)1> mnesia:start(). ok (a@sam)2> PrimProps = [{n_fragments, 7}, {node_pool, [node()]}]. [{n_fragments,7},{node_pool,[a@sam]}] (a@sam)3> mnesia:create_table(prim_dict, [{frag_properties, PrimProps}, {attributes,[prim_key,prim_val]}]). {atomic,ok} (a@sam)4> SecProps = [{foreign_key, {prim_dict, sec_val}}]. [{foreign_key,{prim_dict,sec_val}}] (a@sam)5> mnesia:create_table(sec_dict, [{frag_properties, SecProps}, (a@sam)5> {attributes, [sec_key, sec_val]}]). {atomic,ok} (a@sam)6> Write = fun(Rec) -> mnesia:write(Rec) end. #Fun<erl_eval> (a@sam)7> PrimKey = 11. 11 (a@sam)8> SecKey = 42. 42 (a@sam)9> mnesia:activity(sync_dirty, Write, [{prim_dict, PrimKey, -11}], mnesia_frag). ok (a@sam)10> mnesia:activity(sync_dirty, Write, [{sec_dict, SecKey, PrimKey}], mnesia_frag). ok (a@sam)11> mnesia:change_table_frag(prim_dict, {add_frag, [node()]}). {atomic,ok} (a@sam)12> SecRead = fun(PrimKey, SecKey) -> mnesia:read({sec_dict, PrimKey}, SecKey, read) end. #Fun<erl_eval> (a@sam)13> mnesia:activity(transaction, SecRead, [PrimKey, SecKey], mnesia_frag). [{sec_dict,42,11}] (a@sam)14> Info = fun(Tab, Item) -> mnesia:table_info(Tab, Item) end. #Fun<erl_eval> (a@sam)15> mnesia:activity(sync_dirty, Info, [prim_dict, frag_size], mnesia_frag). [{prim_dict,0}, {prim_dict_frag2,0}, {prim_dict_frag3,0}, {prim_dict_frag4,1}, {prim_dict_frag5,0}, {prim_dict_frag6,0}, {prim_dict_frag7,0}, {prim_dict_frag8,0}] (a@sam)16> mnesia:activity(sync_dirty, Info, [sec_dict, frag_size], mnesia_frag). [{sec_dict,0}, {sec_dict_frag2,0}, {sec_dict_frag3,0}, {sec_dict_frag4,1}, {sec_dict_frag5,0}, {sec_dict_frag6,0}, {sec_dict_frag7,0}, {sec_dict_frag8,0}] (a@sam)17>
分片表管理
函数 mnesia:change_table_frag(Tab, Change)
用于重新配置分片表。参数 Change
必须具有以下值之一
{activate, FragProps}
- 激活现有表的分片属性。FragProps
必须包含{node_pool, Nodes}
或为空。deactivate
- 停用表的分片属性。分片数量必须为1
。没有任何其他表可以在其外键中引用此表。{add_frag, NodesOrDist}
- 向分片表添加一个分片。一个旧分片中的所有记录都将被重新哈希,大约一半的记录将被移动到新的(最后一个)分片。所有其他在其外键中引用此表的分片表也会自动获得一个新的分片。此外,它们的记录也会以与主表相同的方式动态地重新哈希。参数
NodesOrDist
可以是节点列表,也可以是函数mnesia:table_info(Tab, frag_dist)
的结果。假设参数NodesOrDist
是一个排序列表,其中最佳节点优先位于列表中以托管新副本。新分片获得的副本数量与第一个分片相同(请参阅n_ram_copies
、n_disc_copies
和n_disc_only_copies
)。NodesOrDist
列表必须至少包含一个元素,用于需要分配的每个副本。del_frag
- 从分片表中删除一个分片。最后一个分片中的所有记录都将被移动到其他分片之一。所有其他在其外键中引用此表的分片表也会自动丢失它们的最后一个分片。此外,它们的记录也会以与主表相同的方式动态地重新哈希。{add_node, Node}
- 向node_pool
添加一个节点。新的节点池会影响从函数mnesia:table_info(Tab, frag_dist)
返回的列表。{del_node, Node}
- 从node_pool
删除一个节点。新的节点池会影响从函数mnesia:table_info(Tab, frag_dist)
返回的列表。
现有函数的扩展
通过将表属性 frag_properties
设置为某些适当的值,函数 mnesia:create_table/2
创建一个全新的分片表。
函数 mnesia:delete_table/1
删除一个分片表,包括其所有分片。但是,不得存在任何其他在其外键中引用此表的分片表。
函数 mnesia:table_info/2
现在理解项 frag_properties
。
如果函数 mnesia:table_info/2
在模块 mnesia_frag
的活动上下文中启动,则可以获取有关几个新项的信息
base_table
- 分片表的名称n_fragments
- 分片的实际数量node_pool
- 节点池n_ram_copies
n_disc_copies
n_disc_only_copies
- 存储类型分别为ram_copies
、disc_copies
和disc_only_copies
的副本数量。实际值是从第一个分片动态派生的。第一个分片用作原型。当需要计算实际值时(例如,添加新分片时),它们是通过计算每个存储类型的每个副本数量来确定的。这意味着当函数mnesia:add_table_copy/3
、mnesia:del_table_copy/2
和mnesia:change_table_copy_type/2
应用于第一个分片时,会影响n_ram_copies
、n_disc_copies
和n_disc_only_copies
的设置。foreign_key
- 外键foreigners
- 所有其他在其外键中引用此表的表frag_names
- 所有分片的名称frag_dist
- 按Count
升序排序的{Node, Count}
元组的排序列表。Count
是此分片表在每个Node
上托管的副本总数。该列表始终至少包含node_pool
中的所有节点。即使其Count
较低,也不属于node_pool
的节点也会放在列表的最后。frag_size
-{Name, Size}
元组的列表,其中Name
是分片Name
,Size
是它包含的记录数frag_memory
-{Name, Memory}
元组的列表,其中Name
是分片Name
,Memory
是它占用的内存量size
- 所有分片的总大小memory
- 所有分片的总内存
负载均衡
有几种算法可以将分片表中的记录均匀地分布在节点池中。没有哪一个是最好的,这取决于应用程序的需求。以下情况示例需要注意
节点的永久更改
。当引入或删除新的永久db_node
时,可能是时候更改节点池并将副本均匀地重新分布到新的节点池中。也可能是在重新分发副本之前添加或删除分片的时候。大小/内存阈值
。当分片表(或单个分片)的总大小或总内存超过某些特定于应用程序的阈值时,可能是时候动态添加新分片以获得更好的记录分布。临时节点故障
。当某个节点暂时关闭时,可能是时候使用新副本补偿某些分片以保持所需的冗余级别。当节点再次启动时,可能是时候删除多余的副本。过载阈值
。当某些节点的负载超过某些特定于应用程序的阈值时,可能是时候添加或移动某些分片副本到负载较低的节点。如果表与某些其他表具有外键关系,则要格外小心。为避免严重的性能损失,必须对所有相关表执行相同的重新分发。
使用函数 mnesia:change_table_frag/2
添加新分片,并在每个分片上应用常规的模式操作函数(例如 mnesia:add_table_copy/3
、mnesia:del_table_copy/2
和 mnesia:change_table_copy_type/2
)以执行实际的重新分发。
本地内容表
复制表在复制它们的所有节点上具有相同的内容。但是,有时最好拥有表,但在不同节点上拥有不同的内容。
如果在创建表时指定属性 {local_content, true}
,则该表会驻留在您指定该表存在的节点上,但对该表的写入操作仅在本地副本上执行。
此外,当表在启动时初始化时,该表仅在本地初始化,并且不会从其他节点复制表内容。
无磁盘节点
Mnesia
可以在没有磁盘的节点上运行。在此类节点上,不可能使用 disc_copies
或 disc_only_copies
的副本。这对于 schema
表尤其麻烦,因为 Mnesia
需要模式来初始化自身。
与其他表一样,模式表可以驻留在一个或多个节点上。模式表的存储类型可以是 disc_copies
或 ram_copies
(但不能是 disc_only_copies
)。在启动时,Mnesia
使用其模式来确定要尝试建立连接的节点。如果任何其他节点已启动,则启动节点将其表定义与从其他节点带来的表定义合并。这也适用于模式表本身的定义。应用程序参数 extra_db_nodes
包含一个节点列表,Mnesia
除了在模式中找到的节点之外,还要与这些节点建立连接。默认值为 []
(空列表)。
因此,当无盘节点需要从网络上的远程节点查找模式定义时,必须通过应用程序参数 -mnesia extra_db_nodes NodeList
提供此信息。如果没有设置此配置参数,Mnesia
将作为单节点系统启动。此外,函数 mnesia:change_config/2
可以用来为 extra_db_nodes
赋值,并在 Mnesia
启动后强制建立连接,即 mnesia:change_config(extra_db_nodes, NodeList)
。
应用程序参数 schema_location
控制 Mnesia
在哪里搜索其模式。该参数可以是以下原子之一
disc
- 强制磁盘。假设模式位于Mnesia
目录中。如果找不到模式,Mnesia
将拒绝启动。ram
- 强制 RAM。模式仅驻留在 RAM 中。在启动时,会生成一个微小的新的模式。此默认模式仅包含模式表的定义,并且仅驻留在本地节点上。由于在默认模式中没有找到其他节点,因此必须使用配置参数extra_db_nodes
,以使该节点与其他节点共享其表定义。(参数extra_db_nodes
也可在磁盘完整节点上使用。)opt_disc
- 可选磁盘。模式可以驻留在磁盘或 RAM 上。如果在磁盘上找到模式,Mnesia
将作为磁盘完整节点启动(模式表的存储类型为 disc_copies)。如果在磁盘上没有找到模式,Mnesia
将作为无盘节点启动(模式表的存储类型为ram_copies
)。应用程序参数的默认值为opt_disc
。
当 schema_location
设置为 opt_disc
时,可以使用函数 mnesia:change_table_copy_type/3
更改模式的存储类型。如下所示
1> mnesia:start().
ok
2> mnesia:change_table_copy_type(schema, node(), disc_copies).
{atomic, ok}
假设调用 mnesia:start/0
没有在磁盘上找到任何要读取的模式,Mnesia
将作为无盘节点启动,然后将其更改为使用磁盘在本地存储模式的节点。
关于模式管理的更多信息
可以将节点添加到 Mnesia
系统中或从中删除。这可以通过将模式的副本添加到这些节点来完成。
函数 mnesia:add_table_copy/3
和 mnesia:del_table_copy/2
可用于添加和删除模式表的副本。将节点添加到模式复制的节点列表会影响以下方面
- 它允许将其他表复制到此节点。
- 它会导致
Mnesia
在磁盘完整节点启动时尝试联系该节点。
函数调用 mnesia:del_table_copy(schema, mynode@host)
从 Mnesia
系统中删除节点 mynode@host
。如果 Mnesia
在 mynode@host
上运行,则调用将失败。其他 Mnesia
节点将永远不会再尝试连接到该节点。请注意,如果节点 mynode@host
上有磁盘驻留模式,则必须删除整个 Mnesia
目录。这可以使用函数 mnesia:delete_schema/1
完成。如果 Mnesia
在节点 mynode@host
上再次启动并且目录尚未清除,则 Mnesia
的行为是未定义的。
如果模式的存储类型是 ram_copies
,即无盘节点,则 Mnesia
不会使用该特定节点上的磁盘。通过将表 schema
的存储类型更改为 disc_copies
来启用磁盘使用。
使用函数 mnesia:create_schema/1
显式创建新模式,或者通过启动没有磁盘驻留模式的 Mnesia
隐式创建新模式。每当创建表(包括模式表)时,都会为其分配自己的唯一 cookie。模式表不是像普通表那样使用函数 mnesia:create_table/2
创建的。
在启动时,Mnesia
将不同的节点相互连接,然后它们相互交换表定义,并且合并表定义。在合并过程中,Mnesia
执行健全性测试,以确保表定义彼此兼容。如果一个表存在于多个节点上,则 cookie 必须相同,否则 Mnesia
将关闭其中一个节点。如果在两个节点断开连接时,它们彼此独立地创建了一个表,则会发生这种不幸的情况。要解决此问题,必须删除其中一个表(由于 cookie 不同,即使它们具有相同的名称,也被视为两个不同的表)。
合并不同版本的模式表并不总是要求 cookie 相同。如果模式表的存储类型为 disc_copies
,则 cookie 是不可变的,所有其他 db_nodes
必须具有相同的 cookie。当模式存储为类型 ram_copies
时,其 cookie 可以替换为来自另一个节点(ram_copies
或 disc_copies
)的 cookie。每次 RAM 节点连接到另一个节点时,都会执行 cookie 替换(在合并模式表定义期间)。
此外,以下内容适用
mnesia:system_info(schema_location)
和mnesia:system_info(extra_db_nodes)
可分别用于确定schema_location
和extra_db_nodes
的实际值。mnesia:system_info(use_dir)
可用于确定Mnesia
是否实际使用Mnesia
目录。- 即使在
Mnesia
启动之前,也可以确定use_dir
。
现在可以使用函数 mnesia:info/0
在 Mnesia
启动之前打印一些系统信息。当 Mnesia
启动时,该函数会打印更多信息。
更新表定义的事务要求在模式存储类型为 disc_copies
的所有节点上启动 Mnesia
。还必须加载这些节点上该表的所有副本。这些可用性规则有一些例外情况
- 可以在不启动所有磁盘完整节点的情况下创建表并添加新的副本。
- 可以在加载该表的所有其他副本之前添加新的副本,前提是至少有一个其他副本处于活动状态。
Mnesia 事件处理
系统事件和表事件是 Mnesia
在各种情况下生成的两个事件类别。
用户进程可以订阅 Mnesia
生成的事件。提供以下两个函数
mnesia:subscribe(Event-Category)
- 确保将Event-Category
类型的所有事件的副本发送到调用进程mnesia:unsubscribe(Event-Category)
- 删除对Event-Category
类型事件的订阅
Event-Category
可以是以下之一
- 原子
system
- 原子
activity
- 元组
{table, Tab, simple}
- 元组
{table, Tab, detailed}
旧的事件类别 {table, Tab}
与事件类别 {table, Tab, simple}
相同。
订阅函数激活事件的订阅。事件作为消息传递到评估函数 mnesia:subscribe/1
的进程。语法如下
{mnesia_system_event, Event}
用于系统事件{mnesia_activity_event, Event}
用于活动事件{mnesia_table_event, Event}
用于表事件
事件类型将在下一节中描述。
所有系统事件都由 Mnesia
gen_event
处理程序订阅。默认的 gen_event
处理程序是 mnesia_event
,但可以通过使用应用程序参数 event_module
来更改。此参数的值必须是实现完整处理程序的模块的名称,如 STDLIB
中的 gen_event
模块所指定。
mnesia:system_info(subscribers)
和 mnesia:table_info(Tab, subscribers)
可用于确定哪些进程订阅了各种事件。
系统事件
系统事件如下
{mnesia_up, Node}
- 在节点上启动 Mnesia。Node
是节点名称。默认情况下,此事件将被忽略。{mnesia_down, Node}
- 在节点上停止 Mnesia。Node
是节点名称。默认情况下,此事件将被忽略。{mnesia_checkpoint_activated, Checkpoint}
- 激活名称为Checkpoint
的检查点,并且当前节点参与该检查点。可以通过函数mnesia:activate_checkpoint/1
显式激活检查点,或者在备份、添加表副本、节点之间内部传输数据时隐式激活。默认情况下,此事件将被忽略。{mnesia_checkpoint_deactivated, Checkpoint}
- 停用名称为Checkpoint
的检查点,并且当前节点参与该检查点。可以通过函数mnesia:deactivate/1
显式停用检查点,或者当表的最后一个副本(参与检查点)变得不可用时隐式停用,例如在节点关闭时。默认情况下,此事件将被忽略。{mnesia_overload, Details}
- 当前节点的Mnesia
过载,订阅者需要采取行动。当应用程序对磁盘驻留表执行的更新操作超过
Mnesia
的处理能力时,通常会发生过载情况。忽略这种过载可能会导致磁盘空间耗尽的情况(无论存储在磁盘上的表有多大)。每次更新都会追加到事务日志中,并偶尔(取决于配置方式)转储到表文件中。表文件的存储比事务日志存储更紧凑,特别是当同一个记录被重复更新时。如果在上一次转储完成之前达到转储事务日志的阈值,则会触发过载事件。
另一种典型的过载情况是事务管理器无法以与应用程序更新磁盘驻留表相同的速度提交事务。当发生这种情况时,事务管理器的消息队列会持续增长,直到内存耗尽或负载降低。
脏更新也可能发生同样的问题。过载是在当前节点本地检测到的,但其原因可能在另一个节点上。如果任何表驻留在另一个节点上(无论是否复制),应用程序进程都可能导致高负载。默认情况下,此事件会报告给
error_logger
。{inconsistent_database, Context, Node}
-Mnesia
认为数据库可能不一致,并让其应用程序有机会从不一致中恢复。例如,通过安装一致的备份作为后备,然后重新启动系统。另一种方法是从mnesia:system_info(db_nodes)
中选择一个MasterNode
,然后调用mnesia:set_master_nodes([MasterNode])
。默认情况下,会向error_logger
报告错误。{mnesia_fatal, Format, Args, BinaryCore}
-Mnesia
检测到致命错误,即将终止。故障原因在Format
和Args
中解释,它们可以作为io:format/2
的输入或发送到error_logger
。默认情况下,它会发送到error_logger
。BinaryCore
是一个二进制文件,其中包含检测到致命错误时Mnesia
内部状态的摘要。默认情况下,该二进制文件会写入当前目录下的唯一文件名。在 RAM 节点上,该核心会被忽略。{mnesia_info, Format, Args}
-Mnesia
检测到一些在调试系统时可能感兴趣的内容。这在Format
和Args
中解释,它们可以作为io:format/2
的输入或发送到error_logger
。默认情况下,此事件会使用io:format/2
打印。{mnesia_error, Format, Args}
-Mnesia
检测到错误。故障原因在Format
和Args
中解释,它们可以作为io:format/2
的输入或发送到error_logger
。默认情况下,此事件会报告给error_logger
。{mnesia_user, Event}
- 应用程序启动了函数mnesia:report_event(Event)
。Event
可以是任何 Erlang 数据结构。当跟踪Mnesia
应用程序的系统时,能够将Mnesia
自身的事件与提供应用程序上下文信息的应用程序相关事件交错排列会很有用。每当应用程序开始新的且要求苛刻的Mnesia
活动,或者进入其执行的新且有趣的阶段时,使用mnesia:report_event/1
都是一个好主意。
活动事件
目前,只有一种类型的活动事件
{complete, ActivityID}
- 当导致数据库修改的事务完成时,会发生此事件。它对于确定何时发送由给定活动引起的一组表事件(请参阅下一节)很有用。一旦收到此事件,就可以保证不会再收到具有相同ActivityID
的其他表事件。请注意,即使没有收到具有相应ActivityID
的表事件,仍然可能会收到此事件,这取决于接收进程订阅的表。脏操作始终只包含一个更新,因此不会发送活动事件。
表事件
表事件是与表更新相关的事件。表事件有两种类型:简单型和详细型。
简单表事件 是形如 {Oper, Record, ActivityId}
的元组,其中:
Oper
是执行的操作。Record
是操作中涉及的记录。ActivityId
是执行操作的事务的标识。
请注意,即使 record_name
有其他设置,记录名称也是表名称。
可能发生的与表相关的事件如下:
{write, NewRecord, ActivityId}
- 已写入新记录。NewRecord
包含新的记录值。{delete_object, OldRecord, ActivityId}
- 可能已使用mnesia:delete_object/1
删除记录。OldRecord
包含旧记录的值,由应用程序作为参数给出。请注意,如果表类型为bag
,则具有相同键的其他记录可以保留在该表中。{delete, {Tab, Key}, ActivityId}
- 可能已删除一个或多个记录。已删除表Tab
中所有键为Key
的记录。
详细表事件 是形如 {Oper, Table, Data, [OldRecs], ActivityId}
的元组,其中:
Oper
是执行的操作。Table
是操作中涉及的表。Data
是写入/删除的记录/OID。OldRecs
是操作之前的内容。ActivityId
是执行操作的事务的标识。
可能发生的与表相关的事件如下:
{write, Table, NewRecord, [OldRecords], ActivityId}
- 已写入新记录。NewRecord
包含新的记录值,OldRecords
包含执行操作之前的记录。请注意,新内容取决于表类型。{delete, Table, What, [OldRecords], ActivityId}
- 可能已删除记录。What
是{Table, Key}
或已删除的记录{RecordName, Key, ...}
。请注意,新内容取决于表类型。
调试 Mnesia 应用程序
由于各种原因,调试 Mnesia
应用程序可能很困难,主要与难以理解事务和表加载机制的工作方式有关。另一个困惑的来源可能是嵌套事务的语义。
Mnesia
的调试级别通过调用函数 mnesia:set_debug_level(Level)
设置,其中 Level
是以下之一:
none
- 没有跟踪输出。这是默认值。verbose
- 激活重要调试事件的跟踪。这些事件会生成{mnesia_info, Format, Args}
系统事件。进程可以使用函数mnesia:subscribe/1
订阅这些事件。这些事件始终会发送到Mnesia
事件处理程序。debug
- 激活 verbose 级别的所有事件,并跟踪所有调试事件。这些调试事件会生成{mnesia_info, Format, Args}
系统事件。进程可以使用mnesia:subscribe/1
订阅这些事件。这些事件始终会发送到Mnesia
事件处理程序。在此调试级别,Mnesia
事件处理程序开始订阅架构表中的更新。trace
- 激活 debug 级别的所有事件。在此级别,Mnesia
事件处理程序开始订阅所有Mnesia
表中的更新。此级别仅适用于调试小型玩具系统,因为可能会生成许多大型事件。false
- none 的别名。true
- debug 的别名。
Mnesia
自身的调试级别也是一个应用程序参数,这使得可以通过使用以下代码在初始启动阶段启动 Erlang 系统以打开 Mnesia
调试:
% erl -mnesia debug verbose
Mnesia 中的并发进程
并发 Erlang 系统的编程是另一本书的主题。但是,值得注意的是以下功能,这些功能允许并发进程存在于 Mnesia
系统中:
可以在事务中调用一组函数或进程。事务可以包括从 DBMS 读取、写入或删除数据的语句。许多这样的事务可以并发运行,程序员无需显式同步操作数据的进程。
所有通过事务系统访问数据库的程序都可以编写得好像它们具有对数据的独占访问权。这是一个理想的属性,因为所有同步都由事务处理程序负责。如果程序读取或写入数据,则系统会确保没有其他程序同时尝试操作相同的数据。
表可以移动或删除,并且表的布局可以通过多种方式重新配置。这些功能实现的一个重要方面是,用户程序可以在表被重新配置时继续使用该表。例如,可以移动表并同时对该表执行写操作。这对于许多需要持续可用服务的应用程序非常重要。有关更多信息,请参阅事务和其他访问上下文。
原型设计
当您想要启动和操作 Mnesia
时,通常更容易将定义和数据写入普通文本文件。最初,不存在任何表和数据,或者哪些表是必需的。在原型设计的初始阶段,谨慎的做法是将所有数据写入一个文件,处理该文件,并将文件中的数据插入到数据库中。Mnesia
可以使用从文本文件读取的数据进行初始化。以下两个函数可用于处理文本文件。
mnesia:load_textfile(Filename)
将文件中找到的一系列本地表定义和数据加载到Mnesia
中。此函数还会启动Mnesia
,并可能创建一个新的模式。该函数仅在本地节点上运行。mnesia:dump_to_textfile(Filename)
将Mnesia
系统的所有本地表转储到一个文本文件中,该文件可以编辑(使用普通文本编辑器)并在以后重新加载。
这些函数比 Mnesia
的普通存储和加载函数慢得多。但是,这主要用于小型实验和初始原型设计。这些函数的主要优点是它们易于使用。
文本文件的格式如下:
{tables, [{Typename, [Options]},
{Typename2 ......}]}.
{Typename, Attribute1, Attribute2 ....}.
{Typename, Attribute1, Attribute2 ....}.
Options
是一个 {Key,Value}
元组列表,符合您可以提供给 mnesia:create_table/2
的选项。
例如,要开始使用一个小型健康食品数据库,请将以下数据输入到文件 FRUITS
中:
{tables,
[{fruit, [{attributes, [name, color, taste]}]},
{vegetable, [{attributes, [name, color, taste, price]}]}]}.
{fruit, orange, orange, sweet}.
{fruit, apple, green, sweet}.
{vegetable, carrot, orange, carrotish, 2.55}.
{vegetable, potato, yellow, none, 0.45}.
以下 Erlang shell 会话显示如何加载 FRUITS
数据库:
% erl
Erlang (BEAM) emulator version 4.9
Eshell V4.9 (abort with ^G)
1> mnesia:load_textfile("FRUITS").
New table fruit
New table vegetable
{atomic,ok}
2> mnesia:info().
---> Processes holding locks <---
---> Processes waiting for locks <---
---> Pending (remote) transactions <---
---> Active (local) transactions <---
---> Uncertain transactions <---
---> Active tables <---
vegetable : with 2 records occuping 299 words of mem
fruit : with 2 records occuping 291 words of mem
schema : with 3 records occuping 401 words of mem
===> System info in version "1.1", debug level = none <===
opt_disc. Directory "/var/tmp/Mnesia.nonode@nohost" is used.
use fallback at restart = false
running db nodes = [nonode@nohost]
stopped db nodes = []
remote = []
ram_copies = [fruit,vegetable]
disc_copies = [schema]
disc_only_copies = []
[{nonode@nohost,disc_copies}] = [schema]
[{nonode@nohost,ram_copies}] = [fruit,vegetable]
3 transactions committed, 0 aborted, 0 restarted, 2 logged to disc
0 held locks, 0 in queue; 0 local transactions, 0 remote
0 transactions waits for other nodes: []
ok
3>
可以看出,DBMS 是从一个常规文本文件初始化的。
使用 Mnesia 的面向对象编程
在入门中介绍的 Company
数据库有三个表存储记录(employee
、dept
、project
),以及三个表存储关系(manager
、at_dep
、in_proj
)。这是一个规范化的数据模型,它比非规范化的数据模型有一些优势。
在规范化的数据库中进行通用搜索更有效。在规范化的数据模型上执行某些操作也更容易。例如,可以轻松删除一个项目,如下例所示:
remove_proj(ProjName) ->
F = fun() ->
Ip = qlc:e(qlc:q([X || X <- mnesia:table(in_proj),
X#in_proj.proj_name == ProjName]
)),
mnesia:delete({project, ProjName}),
del_in_projs(Ip)
end,
mnesia:transaction(F).
del_in_projs([Ip|Tail]) ->
mnesia:delete_object(Ip),
del_in_projs(Tail);
del_in_projs([]) ->
done.
实际上,数据模型很少完全规范化。规范化数据库模型的现实替代方案将是甚至不属于第一范式的数据模型。Mnesia
适用于电信等应用程序,因为它很容易以灵活的方式组织数据。Mnesia
数据库始终组织为一组表。每个表都填充了行、对象和记录。 Mnesia
的独特之处在于,记录中的各个字段可以包含任何类型的复合数据结构。记录中的单个字段可以包含列表、元组、函数,甚至记录代码。
许多电信应用程序对某些类型的记录的查找时间有独特的要求。如果 Company
数据库是电信系统的一部分,则可以最小化员工及其正在参与的项目列表的查找时间。如果是这种情况,可以选择一种完全不同的没有直接关系的数据模型。那么您将只有记录本身,不同的记录可以包含对其他记录的直接引用,或者包含不属于 Mnesia
模式的其他记录。
可以创建以下记录定义:
-record(employee, {emp_no,
name,
salary,
sex,
phone,
room_no,
dept,
projects,
manager}).
-record(dept, {id,
name}).
-record(project, {name,
number,
location}).
描述员工的记录可以如下所示:
Me = #employee{emp_no = 104732,
name = klacke,
salary = 7,
sex = male,
phone = 99586,
room_no = {221, 015},
dept = 'B/SFR',
projects = [erlang, mnesia, otp],
manager = 114872},
此模型只有三个不同的表,并且员工记录包含对其他记录的引用。该记录具有以下引用:
'B/SFR'
指向一个dept
记录。[erlang, mnesia, otp]
是对三个不同projects
记录的三个直接引用的列表。114872
指向另一个员工记录。
Mnesia
记录标识符({Tab, Key}
)也可以用作引用。在这种情况下,属性 dept
将设置为值 {dept, 'B/SFR'}
而不是 'B/SFR'
。
使用此数据模型,某些操作的执行速度比 Company
数据库中规范化数据模型的执行速度快得多。但是,某些其他操作会变得更加复杂。特别是,更难确保记录不包含指向其他不存在或已删除记录的悬挂指针。
以下代码示例说明了使用非规范化数据模型的搜索。要查找部门 Dep
中工资高于 Salary
的所有员工,请使用以下代码:
get_emps(Salary, Dep) ->
Q = qlc:q(
[E || E <- mnesia:table(employee),
E#employee.salary > Salary,
E#employee.dept == Dep]
),
F = fun() -> qlc:e(Q) end,
transaction(F).
此代码更易于编写和理解,并且执行速度也快得多。
如果使用非规范化的数据模型而不是规范化的模型,则很容易展示执行速度更快的代码示例。主要原因是需要的表更少。因此,可以更轻松地在连接操作中组合来自不同表的数据。在前面的示例中,函数 get_emps/2
从连接操作转换为简单的查询,该查询由对单个表的选择和投影组成。