首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

6.杂项Mnesia功能 | 6. Miscellaneous Mnesia Features

前面的部分介绍了如何开始Mnesia以及如何构建Mnesia数据库。本节介绍构建分布式容错Mnesia数据库时可用的更高级功能。包括以下主题:

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

6.1索引

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

以下两个函数处理现有表上的索引:

  • mnesia:add_table_index(Tab, AttributeName) -> {aborted, R} |{atomic, ok}
  • mnesia:del_table_index(Tab, AttributeName) -> {aborted, R} |{atomic, ok}

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

索引功能Mnesia与以下三种功能一起使用,它们根据数据库中的索引条目来检索和匹配记录:

mnesia:index_read(Tab,SecondaryKey,AttributeName) - > transaction abort | 通过在索引中查找SecondaryKey来查找主键,RecordList避免了对整个表的详尽搜索。

mnesia:index_match_object(Pattern,AttributeName) - > transaction abort | 通过查找索引中的辅助键来查找主键,RecordList避免了对整个表的详尽搜索。 辅助键在Pattern的AttributeName字段中找到。 辅助键必须绑定。

mnesia:match_object(Pattern) - > transaction abort | RecordList使用索引来避免整个表的详尽搜索。 与以前的函数不同,只要绑定了辅助键,此函数就可以使用任何索引。

这些功能将在以下进一步描述和举例说明Pattern Matching

6.2 分发和容错

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副本,则可以使用以下替代方法:
- The function [`mnesia:dump_tables/1`](mnesia#dump_tables-1) can be used to dump RAM table replicas to disc. 
- The table replicas can be backed up, either from RAM, or from disc if dumped there with this function. 
  • disc_copies。该属性的值是一个Erlang节点的列表,该表的一个副本位于列表中每个节点的RAM和光盘上。写入该表的地址写入操作将表格的RAM和光盘副本同时写入。
  • disc_only_copies。该属性的值是Erlang节点的列表,并且该表的副本仅作为列表中每个节点上的光盘副本。这种表副本的主要缺点是访问速度。主要优点是表格不占用内存空间。

另外,表格属性可以被设置和改变。有关详情,请参阅Define a Schema

基本上有两个使用多个表副本的原因:容错和速度。请注意,表复制为这两种系统需求提供了解决方案。

如果有两个活动表副本,如果一个副本失败,则所有信息仍可用。这可能是许多应用程序中的重要属性。此外,如果在两个特定节点上存在表副本,那么在这些节点上执行的应用程序都可以在不访问网络的情况下从表中读取数据。网络运营速度相对较慢,比本地运营消耗更多资源。

为经常读取数据但很少写入数据的分布式应用程序创建表副本以实现本地节点上的快速读取操作可能更为有利。复制的主要缺点是增加了写入数据的时间。如果一个表有两个副本,则每个写入操作都必须访问两个表副本。由于这些写入操作之一必须是网络操作,因此对复制表执行写操作要比对非复制表写入操作要昂贵得多。

6.3 表碎片

概念

引入了表碎片的概念来处理大型表格。 这个想法是将一张表分成几个可管理的片段。 每个片段都被实现为第一类Mnesia表,并且可以像任何其他表一样被复制,具有索引等等。 但是这些表不能有local_content或者snmp连接被激活。

为了能够访问分段表中的记录,Mnesia必须确定实际记录属于哪个片段。 这由模块mnesia_frag完成,该模块实现了mnesia_access回调行为。 建议阅读有关函数mnesia:activity / 4的文档,以了解mnesia_frag如何用作mnesia_access回调模块。

在每次记录访问时,mnesia_frag首先从记录键计算一个散列值。其次,表格片段的名称由散列值确定。最后,实际的表访问由与非分片表相同的功能执行。当事先不知道密钥时,将搜索所有片段的匹配记录。

请注意,在ordered_set表中,记录按每个片段排序,并且select和match_object返回的结果中的顺序未定义。

以下代码演示了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。 碎片属性是一个带有arity 2的标记元组的列表。默认情况下,列表是空的,但是当它不是空的时,它会触发Mnesia将表视为碎片。 碎片属性如下所示:

{n_fragments, Int}

n_fragments规定表格当前有多少片段。该属性可以在创建表格时明确设置,以后可以用{add_frag, NodesOrDist}或更改del_fragn_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也有0n_ram_copies默认为1

{n_disc_copies, Int}

disc_copies规定每个片段有多少副本。该属性可以在创建表格时明确设置。默认是0

{n_disc_only_copies, Int}

disc_only_copies规定每个片段有多少副本。该属性可以在创建表格时明确设置。默认值是0

{foreign_key, ForeignKey}

ForeignKey可以是未定义的原子或元组{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)

现有功能的扩展

该函数mnesia:create_table/2通过将table属性设置frag_properties为一些适当的值来创建一个全新的碎片表。

该函数mnesia:delete_table/1删除包含其所有片段的碎片表。但是,必须不存在任何其他在其外键中引用此表的碎片表。

该功能mnesia:table_info/2现在可以理解项目frag_properties

如果该功能mnesia:table_info/2是在模块的活动上下文中启动的,则mnesia_frag可以获得几个新项目的信息:

base_table分片tablen_fragments的名称实际分片数node_pool节点池n_ram_copiesn_disc_copiesn_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

按递增计数顺序排序的{Node,Count}元组的排序列表。 计数是此分段表在每个节点上承载的副本总数。 该列表始终至少包含node_pool中的所有节点。 即使计数较低,也不会将属于node_pool的节点放在列表中。

frag_size

{Name, Size}元组列表,其中Name是片段Name,以及Size它包含的记录数

frag_memory

{Name, Memory}元组列表,其中Name是一个片段NameMemory它是多少内存占用

size

所有片段的总大小

memory

所有片段的总内存

负载均衡

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

  • permanent change of nodes。当db_node引入或丢弃新的永久性永久磁盘时,可能需要更改节点池并将副本均匀地重新分配到新的节点池中。在重新分配副本之前,也可以添加或删除片段。
  • size/memory threshold。当碎片表(或单个碎片)的总大小或总内存超过某个特定于应用程序的阈值时,可以动态添加新碎片以获得更好的记录分布。
  • temporary node down。当一个节点暂时关闭时,可能需要时间用新副本来补偿某些片段,以保持所需的冗余级别。当节点再次出现时,它可能是删除多余副本的时候了。
  • overload threshold。当某个节点上的负载超过某个特定于应用程序的阈值时,可以将时间添加或移动一些片段副本到负载较低的节点。如果表格与其他表格有外键关系,请格外小心。为避免严重的性能损失,必须对所有相关表执行相同的重新分配。

使用该功能mnesia:change_table_frag/2,以增加新的片段和应用通常的模式操作函数(例如mnesia:add_table_copy/3mnesia:del_table_copy/2mnesia:change_table_copy_type/2在每个片段)来执行实际的重新分配。

6.4 本地内容表

复制表在其所复制的所有节点上具有相同的内容。但是,在不同的节点上使用表格有时会有所不同,但内容不同。

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

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

6.5 Disc-Less节点

Mnesia可以在没有硬盘的节点上运行。 disc_copies或disc_only_copies的副本在这些节点上是不可能的。 这对于模式表格尤其麻烦,因为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中。在启动时,会生成一个小小的新模式。此默认架构仅包含架构表的定义,并且仅驻留在本地节点上。由于在默认模式中找不到其他节点,因此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作为无盘节点启动,然后将其更改为使用光盘在本地存储模式的节点。

6.6 更多关于模式管理

节点可以添加到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完成的。 如果在节点mynode @ host上再次启动Mnesia并且目录尚未清除,则Mnesia的行为未定义。

如果模式的存储类型是ram_copies,即无盘节点,则Mnesia不会在该特定节点上使用该盘。 通过将表架构的存储类型更改为disc_copies来启用光盘使用。

新的模式通过函数mnesia:create_schema / 1显式创建,或者隐式启动Mnesia而不使用硬盘驻留模式。 无论何时创建表(包括模式表),都会为其分配其自己的唯一Cookie。 模式表格不是使用mnesia:create_table / 2作为普通表格创建的。

启动时,Mnesia将不同节点彼此连接,然后互相交换表定义,并合并表定义。在合并过程中,Mnesia执行完整性测试以确保表定义彼此兼容。如果某个表存在于多个节点上,则Cookie必须相同,否则Mnesia关闭其中一个节点。如果在断开连接时在两个节点上独立创建表,则会出现此不幸情况。为了解决这个问题,必须删除其中一个表(因为Cookie不同,即使它们具有相同的名称,它也被认为是两个不同的表)。

合并不同版本的模式表并不总是要求cookies相同。 如果模式表的存储类型是disc_copies,则该cookie是不可变的,并且所有其他数据库节点必须具有相同的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启动之前,现在可以使用mnesia:info / 0函数来打印一些系统信息。 当Mnesia启动时,该功能会打印更多信息。

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

  • 可以创建表并在不启动所有硬盘满节点的情况下添加新副本。
  • 如果至少有一个其他副本处于活动状态,则可以在加载表的所有其他副本之前添加新副本。

6.7 Mnesia事件处理

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

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

mnesia:subscribe(Event-Category)确保将类型为Event-Category的所有事件的副本发送给调用processmnesia: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。该参数的值必须是实现完整处理程序的模块的名称,如gen_event模块所指定的STDLIB

mnesia:system_info(subscribers)mnesia:table_info(Tab, subscribers)可以用来确定哪些进程订阅的各种事件。

系统事件

系统事件如下:

{mnesia_up,Node} Mnesia在节点上启动。 节点是节点名称。 默认情况下,该事件被忽略。 {mnesia_down,Node} Mnesia在节点上停止。 节点是节点名称。 默认情况下,该事件被忽略。 {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_node([MasterNode])。 默认情况下,错误会报告给error_logger。{mnesia_fatal, Format, Args, BinaryCore}

Mnesia发现了一个致命的错误,并很快终止。 格式和参数解释了故障原因,可以将其作为输入提供给io:format / 2或发送到error_logger。 默认情况下,它被发送到error_logger。

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

{mnesia_info,Format,Args}在调试系统时,Mnesia发现了一些可能有用的东西。 这在格式和参数中进行了说明,格式和参数可以作为输入显示为io:format / 2或发送到error_logger。 默认情况下,这个事件是用io:format / 2打印的。 {mnesia_error,格式,参数} Mnesia检测到错误。 格式和参数解释了故障原因,可以将其作为输入提供给io:format / 2或发送到error_logger。 默认情况下,这个事件被报告给error_logger。 {mnesia_user,Event}应用程序启动了函数mnesia:report_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,其他具有相同密钥的记录可保留在表中。 {删除,{Tab,Key},ActivityId}一条或多条记录可能已被删除。 表格标签中的所有带有关键字的记录都已被删除。

详细表事件都是这样的元组{Oper, Table, Data, [OldRecs], ActivityId},其中:

  • Oper 是执行的操作。
  • Table 是操作中涉及的表格。
  • Data 是写/删除的记录/ OID。
  • OldRecs 是操作前的内容。
  • ActivityId 是执行操作的交易的身份。

可能发生的与表格相关的事件如下所示:

{write,Table,NewRecord,[OldRecords],ActivityId}写了一条新记录。 NewRecord包含新的记录值,OldRecords包含操作执行前的记录。 请注意,新内容取决于表格类型。 {删除,表,什么,[OldRecords],ActivityId}记录可能已被删除。 什么是已删除的{Table,Key}或记录{RecordName,Key,...}。 请注意,新内容取决于表格类型。

6.8 调试Mnesia应用程序

Mnesia由于各种原因,调试应用程序可能很困难,主要与理解事务和表加载机制如何工作的困难有关。混淆的另一个来源可能是嵌套事务的语义。

Mnesia的调试级别通过调用函数mnesia:set_debug_level(Level)来设置,其中Levelis是以下之一:

noneNo跟踪输出。 这是默认设置。 verbose激活重要调试事件的跟踪。 这些事件生成{mnesia_info,Format,Args}系统事件。 进程可以使用函数mnesia:subscribe / 1来订阅这些事件。 事件总是发送给Mnesia事件处理程序。 debugActivate详细级别的所有事件以及所有调试事件的跟踪。 这些调试事件生成{mnesia_info,Format,Args}系统事件。 进程可以使用mnesia订阅这些事件:subscribe / 1。 事件总是发送给Mnesia事件处理程序。 在这个调试级别上,Mnesia事件处理程序开始订阅架构表中的更新。 trace在调试级别激活所有事件。 在这个层面上,Mnesia事件处理程序开始订阅所有Mnesia表上的更新。 这个级别仅用于调试小型玩具系统,因为可以生成许多大型事件。 false没有别名。 true调试的别名。

Mnesia它本身的调试级别也是一个应用程序参数,可以Mnesia通过使用以下代码启动Erlang系统以在初始启动阶段打开调试:

% erl -mnesia debug verbose

6.9 Mnesia中的并发进程

编程并发Erlang系统是另一本书的主题。但是,值得注意以下功能,这些功能允许Mnesia系统中存在并发进程:

  • 一组函数或进程可以在一个事务中调用。事务可以包含从DBMS读取,写入或删除数据的语句。许多这样的事务可以并发运行,程序员不需要显式同步操作数据的进程。所有通过交易系统访问数据库的程序都可以编写,就好像它们可以单独访问数据一样。这是一个理想的属性,因为所有同步都由事务处理程序来处理。如果程序读取或写入数据,系统会确保没有其他程序尝试同时处理相同的数据。
  • 可以移动或删除表格,并且可以通过各种方式重新配置表格的布局。实现这些功能的一个重要方面是,用户程序可以在重新配置时继续使用表格。例如,可以同时移动一个表并对表执行写入操作。这对于许多需要连续可用服务的应用程序非常重要。有关更多信息,请参阅Transactions and Other Access Contexts

6.10 原型

如果并且当你想开始和操作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是从一个普通的文本文件启动的。

6.11使用Mnesia进行基于对象的编程

入门指南中介绍的公司数据库有三个存储记录(雇员,部门,项目)和三个存储关系的表(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不同的是,记录中的单个字段可以包含任何类型的复合数据结构。 记录中的单个字段可以包含列表,元组,函数,甚至记录代码。

许多电信应用程序对某些类型的记录的查找时间有独特的要求。 如果公司数据库是电信系统的一部分,那么可以将员工的查询时间和员工正在进行的项目列表最小化。 如果是这种情况,可以选择一个没有直接关系的完全不同的数据模型。 然后您将只拥有记录本身,而不同的记录可能包含对其他记录的直接引用,或者包含不属于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'是指部门记录。
  • [erlang,mnesia,otp]是三个直接引用三个不同项目记录的列表。
  • 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从连接操作转换为简单查询,该查询由单个表上的选择和投影组成。

扫码关注腾讯云开发者

领取腾讯云代金券