查看源码 Mnesia 的其他特性

前面的章节描述了如何开始使用 Mnesia 以及如何构建 Mnesia 数据库。本节描述了在构建分布式、容错的 Mnesia 数据库时可用的更高级功能。包括以下主题

  • 索引
  • 分布式和容错
  • 表分片
  • 本地内容表
  • 无盘节点
  • 关于模式管理的更多信息
  • Mnesia 事件处理
  • 调试 Mnesia 应用程序
  • Mnesia 中的并发进程
  • 原型设计
  • 使用 Mnesia 进行面向对象编程

索引

如果记录的键已知,则可以高效地执行数据检索和匹配。相反,如果键未知,则必须搜索表中的所有记录。表越大,耗时就越长。为了解决这个问题,Mnesia 索引功能用于改进数据的检索和记录的匹配。

以下两个函数操作现有表上的索引

这些函数在由 AttributeName 定义的字段上创建或删除表索引。为了说明这一点,在表定义 (employee, {emp_no, name, salary, sex, phone, room_no}) 上添加索引,这是来自 Company 数据库的示例表。在元素 salary 上添加索引的函数可以表示为 mnesia:add_table_index(employee, salary)

Mnesia 的索引功能与以下三个函数一起使用,这些函数基于数据库中的索引条目检索和匹配记录

这些函数在 模式匹配 中有更详细的描述和示例。

分布式和容错

Mnesia 是一个分布式、容错的 DBMS。表可以通过多种方式在不同的 Erlang 节点上复制。Mnesia 程序员不需要说明不同表的位置,只需要在程序代码中指定不同表的名称。这被称为“位置透明性”,是一个重要的概念。特别是

  • 程序的工作方式与数据的位置无关。数据位于本地节点还是远程节点没有区别。

    请注意,如果数据位于远程节点上,则程序运行速度会较慢。

  • 可以重新配置数据库,并且可以在节点之间移动表。这些操作不会影响用户程序。

之前已经展示了每个表都有许多系统属性,例如 indextype

表属性在创建表时指定。例如,以下函数创建一个具有两个 RAM 副本的表

mnesia:create_table(foo,
                    [{ram_copies, [N1, N2]},
                     {attributes, record_info(fields, foo)}]).

表还可以具有以下属性,其中每个属性都将 Erlang 节点列表作为其值

  • ram_copies。节点列表的值是 Erlang 节点列表,并且该表的 RAM 副本驻留在列表中的每个节点上。

    请注意,当程序对这些副本执行写入操作时,不会执行磁盘操作。但是,如果需要永久 RAM 副本,则可以使用以下替代方法

    1. 函数 mnesia:dump_tables/1 可用于将 RAM 表副本转储到磁盘。
    2. 表副本可以从 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 表中,记录是按分片排序的,并且 selectmatch_object 返回的结果以及 firstnextprevlast 返回的结果中的顺序是未定义的。

以下代码说明了如何将 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_copiesn_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_copiesn_disc_copiesn_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_copiesdisc_copiesdisc_only_copies 的副本数量。实际值是从第一个分片动态派生的。第一个分片用作原型。当需要计算实际值时(例如,添加新分片时),它们是通过计算每个存储类型的每个副本数量来确定的。这意味着当函数 mnesia:add_table_copy/3mnesia:del_table_copy/2mnesia:change_table_copy_type/2 应用于第一个分片时,会影响 n_ram_copiesn_disc_copiesn_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 是分片 NameSize 是它包含的记录数

  • frag_memory - {Name, Memory} 元组的列表,其中 Name 是分片 NameMemory 是它占用的内存量

  • size - 所有分片的总大小

  • memory - 所有分片的总内存

负载均衡

有几种算法可以将分片表中的记录均匀地分布在节点池中。没有哪一个是最好的,这取决于应用程序的需求。以下情况示例需要注意

  • 节点的永久更改。当引入或删除新的永久 db_node 时,可能是时候更改节点池并将副本均匀地重新分布到新的节点池中。也可能是在重新分发副本之前添加或删除分片的时候。
  • 大小/内存阈值。当分片表(或单个分片)的总大小或总内存超过某些特定于应用程序的阈值时,可能是时候动态添加新分片以获得更好的记录分布。
  • 临时节点故障。当某个节点暂时关闭时,可能是时候使用新副本补偿某些分片以保持所需的冗余级别。当节点再次启动时,可能是时候删除多余的副本。
  • 过载阈值。当某些节点的负载超过某些特定于应用程序的阈值时,可能是时候添加或移动某些分片副本到负载较低的节点。如果表与某些其他表具有外键关系,则要格外小心。为避免严重的性能损失,必须对所有相关表执行相同的重新分发。

使用函数 mnesia:change_table_frag/2 添加新分片,并在每个分片上应用常规的模式操作函数(例如 mnesia:add_table_copy/3mnesia:del_table_copy/2mnesia:change_table_copy_type/2)以执行实际的重新分发。

本地内容表

复制表在复制它们的所有节点上具有相同的内容。但是,有时最好拥有表,但在不同节点上拥有不同的内容。

如果在创建表时指定属性 {local_content, true},则该表会驻留在您指定该表存在的节点上,但对该表的写入操作仅在本地副本上执行。

此外,当表在启动时初始化时,该表仅在本地初始化,并且不会从其他节点复制表内容。

无磁盘节点

Mnesia 可以在没有磁盘的节点上运行。在此类节点上,不可能使用 disc_copiesdisc_only_copies 的副本。这对于 schema 表尤其麻烦,因为 Mnesia 需要模式来初始化自身。

与其他表一样,模式表可以驻留在一个或多个节点上。模式表的存储类型可以是 disc_copiesram_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/3mnesia:del_table_copy/2 可用于添加和删除模式表的副本。将节点添加到模式复制的节点列表会影响以下方面

  • 它允许将其他表复制到此节点。
  • 它会导致 Mnesia 在磁盘完整节点启动时尝试联系该节点。

函数调用 mnesia:del_table_copy(schema, mynode@host)Mnesia 系统中删除节点 mynode@host。如果 Mnesiamynode@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_copiesdisc_copies)的 cookie。每次 RAM 节点连接到另一个节点时,都会执行 cookie 替换(在合并模式表定义期间)。

此外,以下内容适用

现在可以使用函数 mnesia:info/0Mnesia 启动之前打印一些系统信息。当 Mnesia 启动时,该函数会打印更多信息。

更新表定义的事务要求在模式存储类型为 disc_copies 的所有节点上启动 Mnesia。还必须加载这些节点上该表的所有副本。这些可用性规则有一些例外情况

  • 可以在不启动所有磁盘完整节点的情况下创建表并添加新的副本。
  • 可以在加载该表的所有其他副本之前添加新的副本,前提是至少有一个其他副本处于活动状态。

Mnesia 事件处理

系统事件和表事件是 Mnesia 在各种情况下生成的两个事件类别。

用户进程可以订阅 Mnesia 生成的事件。提供以下两个函数

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 检测到致命错误,即将终止。故障原因在 FormatArgs 中解释,它们可以作为 io:format/2 的输入或发送到 error_logger。默认情况下,它会发送到 error_logger

    BinaryCore 是一个二进制文件,其中包含检测到致命错误时 Mnesia 内部状态的摘要。默认情况下,该二进制文件会写入当前目录下的唯一文件名。在 RAM 节点上,该核心会被忽略。

  • {mnesia_info, Format, Args} - Mnesia 检测到一些在调试系统时可能感兴趣的内容。这在 FormatArgs 中解释,它们可以作为 io:format/2 的输入或发送到 error_logger。默认情况下,此事件会使用 io:format/2 打印。

  • {mnesia_error, Format, Args} - Mnesia 检测到错误。故障原因在 FormatArgs 中解释,它们可以作为 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 数据库有三个表存储记录(employeedeptproject),以及三个表存储关系(managerat_depin_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 从连接操作转换为简单的查询,该查询由对单个表的选择和投影组成。