查看源码 事务和其他访问上下文
本节介绍 Mnesia 事务系统以及使 Mnesia 成为容错分布式数据库管理系统 (DBMS) 的事务属性。
本节还介绍锁定功能,包括表锁和粘性锁,以及为提高速度和减少开销而绕过事务系统的替代功能。 这些功能称为“脏操作”。 此外还描述了嵌套事务的使用。 包括以下主题
- 事务属性,包括原子性、一致性、隔离性和持久性
- 锁定
- 脏操作
- 记录名称与表名称
- 活动概念和各种访问上下文
- 嵌套事务
- 模式匹配
- 迭代
事务属性
在设计容错分布式系统时,事务至关重要。Mnesia 事务是一种机制,通过该机制可以将一系列数据库操作作为一个功能块执行。 作为事务运行的功能块称为函数对象 (Fun),此代码可以读取、写入和删除 Mnesia 记录。 Fun 将被评估为一个事务,该事务要么提交,要么终止。 如果事务成功执行 Fun,它会在所有涉及的节点上复制该操作,如果发生错误,则终止。
以下示例显示了一个提高某些员工编号的工资的事务
raise(Eno, Raise) ->
F = fun() ->
[E] = mnesia:read(employee, Eno, write),
Salary = E#employee.salary + Raise,
New = E#employee{salary = Salary},
mnesia:write(New)
end,
mnesia:transaction(F).函数 raise/2 包含一个由四行代码组成的 Fun。 此 Fun 由语句 mnesia:transaction(F) 调用并返回一个值。
Mnesia 事务系统通过提供以下重要属性来促进可靠的分布式系统的构建
- 事务处理程序确保当 Fun 在事务内部执行时,在表上执行一系列操作时,不会干扰嵌入在其他事务中的操作。
- 事务处理程序确保事务中的所有操作要么在所有节点上以原子方式成功执行,要么事务失败而不会对任何节点产生永久影响。
Mnesia事务具有四个重要属性,称为原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation) 和持久性 (Durability) (ACID)。 这些属性将在以下部分中描述。
原子性
原子性是指事务执行的数据库更改在所有涉及的节点上生效,或者在所有节点上均不生效。 也就是说,事务要么完全成功,要么完全失败。
当需要在同一事务中原子地写入多个记录时,原子性非常重要。 前面示例中显示的函数 raise/2 仅写入一条记录。 在入门中的程序清单中显示的函数 insert_emp/3 将记录 employee 以及员工关系(例如 at_dep 和 in_proj)写入数据库。 如果此后一段代码在事务内运行,则事务处理程序将确保事务要么完全成功,要么完全不成功。
Mnesia 是一个分布式 DBMS,其中的数据可以在多个节点上复制。 在许多应用程序中,重要的是一系列写入操作在事务内部以原子方式执行。 原子性属性确保事务在所有节点上生效,或在所有节点上均不生效。
一致性
一致性属性确保事务始终将 DBMS 保持在一致状态。 例如,Mnesia 确保如果在写入操作正在进行时 Erlang、Mnesia 或计算机崩溃,则不会发生不一致的情况。
隔离性
隔离性属性确保在网络中不同节点上执行、访问和操作相同数据记录的事务不会互相干扰。 隔离性属性使得并发执行函数 raise/2 成为可能。 并发控制理论中的一个经典问题是“丢失更新问题”。
如果发生以下情况,隔离性属性特别有用,其中一名员工(员工编号为 123)和两个进程(P1 和 P2)同时尝试提高该员工的工资
- 步骤 1:员工工资的初始值例如为 5。 进程 P1 开始执行,读取员工记录,并将工资加 2。
- 步骤 2:进程 P1 由于某种原因被抢占,并且进程 P2 有机会运行。
- 步骤 3:进程 P2 读取记录,将工资加 3,最后写入一条新的员工记录,并将工资设置为 8。
- 步骤 4:进程 P1 再次开始运行,并写入其员工记录,并将工资设置为 7,从而有效地覆盖并撤销了进程 P2 执行的工作。 P2 执行的更新丢失。
事务系统使得可以并发执行两个或多个操作同一记录的进程。 程序员无需检查更新是否同步; 这由事务处理程序监督。 所有通过事务系统访问数据库的程序都可以编写得好像它们对数据拥有独占访问权一样。
持久性
持久性属性确保事务对 DBMS 所做的更改是永久的。 提交事务后,对数据库所做的所有更改都是持久的,即它们被安全地写入磁盘,不会损坏也不会消失。
注意
所描述的持久性功能并不完全适用于
Mnesia配置为“纯”主存数据库的情况。
锁定
不同的事务管理器采用不同的策略来满足隔离性属性。Mnesia 使用两阶段锁定的标准技术。 也就是说,在读取或写入记录之前,先在记录上设置锁。Mnesia 使用以下锁类型
- 读锁。 在读取记录的副本之前,会在该副本上设置读锁。
- 写锁。 每当事务写入记录时,首先会在该特定记录的所有副本上设置写锁。
- 读表锁。 如果事务遍历整个表以搜索满足特定属性的记录,则逐个在记录上设置读锁效率最低。 这也很消耗内存,因为如果表很大,读锁本身会占用相当大的空间。 因此,
Mnesia可以在整个表上设置读锁。 - 写表锁。 如果事务将许多记录写入一个表,则可以在整个表上设置写锁。
- 粘性锁。 这些是在启动锁的事务终止后,仍保留在节点上的写锁。
Mnesia 采用一种策略,其中诸如mnesia:read/1之类的函数会在事务执行时动态获取必要的锁。 Mnesia 自动设置和释放锁,程序员无需编写这些操作。
当并发进程在同一记录上设置和释放锁时,可能会发生死锁。Mnesia 采用“等待-死亡”策略来解决这些情况。 如果 Mnesia 怀疑当事务尝试设置锁时可能会发生死锁,则会强制该事务释放其所有锁并休眠一段时间。 事务中的 Fun 将被再次评估。
因此,传递给mnesia:transaction/1的 Fun 内部的代码必须是纯粹的。 如果例如,事务 Fun 发送消息,则可能会发生一些奇怪的结果。 以下示例说明了这种情况
bad_raise(Eno, Raise) ->
F = fun() ->
[E] = mnesia:read({employee, Eno}),
Salary = E#employee.salary + Raise,
New = E#employee{salary = Salary},
io:format("Trying to write ... ~n", []),
mnesia:write(New)
end,
mnesia:transaction(F).此事务可以向终端写入文本 "尝试写入..." 1000 次。 但是,Mnesia 保证每个事务最终都会运行。 因此,Mnesia 不仅没有死锁,而且没有活锁。
Mnesia 程序员无法优先执行某个特定事务,使其在其他等待执行的事务之前执行。 因此,Mnesia DBMS 事务系统不适合硬实时应用程序。 但是,Mnesia 包含具有实时属性的其他功能。
Mnesia 在事务执行时动态设置和释放锁。 因此,执行具有事务副作用的代码是危险的。 特别是,事务内部的 receive 语句可能会导致事务挂起且永不返回,这反过来会导致锁无法释放。 这种情况可能会导致整个系统停滞不前,因为在其他进程或其他节点中执行的其他事务会被迫等待有缺陷的事务。
如果事务异常终止,Mnesia 会自动释放该事务持有的锁。
到目前为止,已经展示了许多可以在事务中使用的函数的示例。以下列表显示了与事务一起工作的最简单的 Mnesia 函数。请注意,这些函数必须嵌入在事务中。如果没有封闭的事务(或其他封闭的 Mnesia 活动),它们都会失败。
mnesia:transaction(Fun) -> {aborted, Reason} | {atomic, Value}执行一个事务,其中函数对象Fun作为唯一参数。mnesia:read({Tab, Key}) -> transaction abort | RecordList从表Tab中读取所有以Key为键的记录。无论Table的位置如何,此函数都具有相同的语义。如果表类型为bag,则read({Tab, Key})可以返回任意长度的列表。如果表类型为set,则列表的长度为 1 或[]。mnesia:wread({Tab, Key}) -> transaction abort | RecordList的行为与前面列出的函数read/1相同,不同之处在于它获取的是写锁而不是读锁。要执行读取记录、修改记录然后写入记录的事务,立即设置写锁会稍微更有效率。当发出mnesia:read/1,然后执行mnesia:write/1时,在执行写操作时,必须将第一个读锁升级为写锁。mnesia:write(Record) -> transaction abort | ok将记录写入数据库。参数Record是记录的实例。如果发生错误,该函数返回ok或终止事务。mnesia:delete({Tab, Key}) -> transaction abort | ok删除所有具有给定键的记录。mnesia:delete_object(Record) -> transaction abort | ok删除具有 OIDRecord的记录。使用此函数仅删除类型为bag的表中的某些记录。
粘性锁
如前所述,Mnesia 使用的锁定策略是在读取记录时锁定一条记录,并在写入记录时锁定记录的所有副本。但是,某些应用程序主要使用 Mnesia 的容错特性。这些应用程序可以配置为由一个节点完成所有繁重的工作,并在主节点发生故障时,由备用节点准备接管。此类应用程序可以从使用粘性锁而不是正常的锁定方案中受益。
粘性锁是在首次获取锁的事务终止后,仍然保留在节点上的锁。为了说明这一点,假设执行以下事务
F = fun() ->
mnesia:write(#foo{a = kalle})
end,
mnesia:transaction(F).foo 表在两个节点 N1 和 N2 上复制。
正常的锁定需要以下操作
- 一个网络 RPC(两条消息)来获取写锁
- 三个网络消息来执行两阶段提交协议
如果使用粘性锁,则必须首先按如下方式更改代码
F = fun() ->
mnesia:s_write(#foo{a = kalle})
end,
mnesia:transaction(F).此代码使用函数 s_write/1 而不是函数 write/1。函数 s_write/1 设置粘性锁而不是普通锁。如果表未复制,则粘性锁没有特殊效果。如果表已复制,并且在节点 N1 上设置了粘性锁,则该锁将保留在节点 N1 上。下次尝试在节点 N1 上为同一记录设置粘性锁时,Mnesia 会检测到该锁已设置,并且不会执行任何网络操作来获取该锁。
设置本地锁比设置网络锁更有效。因此,对于使用复制表并在其中一个节点上执行大部分工作的应用程序,粘性锁可能会有所帮助。
如果记录粘在节点 N1 上,并且您尝试在节点 N2 上为该记录设置粘性锁,则必须取消粘性。此操作代价很高,并且会降低性能。如果在 N2 上发出 s_write/1 请求,则会自动执行取消粘性操作。
表锁
作为单条记录上正常锁的补充,Mnesia 支持对整个表的读写锁。如前所述,Mnesia 会自动设置和释放锁,程序员无需编写这些操作的代码。但是,如果事务通过在此表上设置表锁来启动,则在特定表中读取和写入许多记录的事务会更有效率。这会阻止其他并发事务访问该表。以下两个函数用于为读取和写入操作设置显式表锁
mnesia:read_lock_table(Tab)在表Tab上设置读锁。mnesia:write_lock_table(Tab)在表Tab上设置写锁。
获取表锁的替代语法如下
mnesia:lock({table, Tab}, read)
mnesia:lock({table, Tab}, write)Mnesia 中的匹配操作可以锁定整个表,也可以只锁定单个记录(当键绑定在模式中时)。
全局锁
写锁通常在表的副本所在(并且处于活动状态)的所有节点上获取。读锁在一个节点(如果存在本地副本,则为本地节点)上获取。
函数 mnesia:lock/2 旨在支持表锁(如前所述),也适用于需要获取锁的情况,而无论表是如何复制的
mnesia:lock({global, GlobalKey, Nodes}, LockKind)
LockKind ::= read | write | ...在节点列表中的所有节点上的 LockItem 上获取锁。
脏操作
在许多应用程序中,处理事务的开销可能会导致性能损失。脏操作是绕过大部分处理并提高事务速度的捷径。
脏操作通常很有用,例如,在数据报路由应用程序中,Mnesia 存储路由表,并且每次收到数据包都启动整个事务非常耗时。因此,Mnesia 具有在不使用事务的情况下操作表的函数。这种替代处理方法称为脏操作。但是,请注意避免事务处理开销的权衡
Mnesia的原子性和隔离属性会丢失。- 隔离属性会被破坏,因为如果同时使用脏操作从同一表中读取和写入记录,则其他使用事务来操作数据的 Erlang 进程无法获得隔离的好处。
脏操作的主要优点是,它们的执行速度比在事务中作为函数对象处理的等效操作快得多。
如果脏操作是在 disc_copies 类型或 disc_only_copies 类型的表上执行的,则会将它们写入磁盘。Mnesia 还确保如果对表执行脏写操作,则会更新表的所有副本。
脏操作可确保一定程度的一致性。例如,脏操作不会返回乱码记录。因此,每个单独的读取或写入操作都以原子方式执行。
所有脏函数在失败时都会执行对 exit({aborted, Reason}) 的调用。即使在事务内部执行以下函数,也不会获取任何锁。以下函数可用
mnesia:dirty_read({Tab, Key})从Mnesia中读取一个或多个记录。mnesia:dirty_write(Record)写入记录Record。mnesia:dirty_delete({Tab, Key})删除一个或多个具有键Key的记录。mnesia:dirty_delete_object(Record)是函数delete_object/1的脏操作替代方法。mnesia:dirty_first(Tab)返回表Tab中的“第一个”键。set或bag表中的记录未排序。但是,存在用户未知的记录顺序。这意味着可以通过此函数和函数mnesia:dirty_next/2遍历表。如果表中没有记录,则此函数返回原子
'$end_of_table'。不建议将此原子用作任何用户记录的键。mnesia:dirty_next(Tab, Key)返回表Tab中的“下一个”键。此函数使得可以遍历表并对表中的所有记录执行一些操作。当到达表的末尾时,将返回特殊键'$end_of_table'。否则,该函数返回一个可用于读取实际记录的键。如果在遍历表时,任何进程都对表执行写操作,则行为未定义,且函数
dirty_next/2。这是因为对Mnesia表的write操作可能会导致表本身的内部重组。这是一个实现细节,但请记住,脏函数是底层函数。mnesia:dirty_last(Tab)的工作方式与mnesia:dirty_first/1完全相同,但返回表类型为ordered_set的 Erlang 项顺序中的最后一个对象。对于所有其他表类型,mnesia:dirty_first/1和mnesia:dirty_last/1是同义词。mnesia:dirty_prev(Tab, Key)的工作方式与mnesia:dirty_next/2完全相同,但返回表类型为ordered_set的 Erlang 项顺序中的前一个对象。对于所有其他表类型,mnesia:dirty_next/2和mnesia:dirty_prev/2是同义词。如果在遍历表时对表进行写入,则此函数的行为未定义。可以使用函数
mnesia:read_lock_table(Tab)来确保在迭代期间不执行任何事务保护的写入。mnesia:dirty_update_counter({Tab, Key}, Val)。计数器是正整数,其值大于或等于零。更新计数器将添加Val和计数器,其中Val是正整数或负整数。Mnesia没有特殊的计数器记录。但是,可以使用{TabName, Key, Integer}形式的记录作为计数器,并且可以持久化。无法对计数器记录进行受事务保护的更新。
使用此函数而不是读取记录、执行算术运算和写入记录时,存在两个显着差异
- 效率更高。
- 函数
dirty_update_counter/2虽然不受事务保护,但其执行是原子操作。因此,如果两个进程同时执行函数dirty_update_counter/2,则不会丢失表更新。
mnesia:dirty_match_object(Pat)是mnesia:match_object/1的非事务等价函数。mnesia:dirty_select(Tab, Pat)是mnesia:select/2的非事务等价函数。mnesia:dirty_index_match_object(Pat, Pos)是mnesia:index_match_object/2的非事务等价函数。mnesia:dirty_index_read(Tab, SecondaryKey, Pos)是mnesia:index_read/3的非事务等价函数。mnesia:dirty_all_keys(Tab)是mnesia:all_keys/1的非事务等价函数。
记录名称与表名称
在 Mnesia 中,表中所有记录必须具有相同的名称。所有记录必须是相同记录类型的实例。但是,记录名称不一定必须与表名称相同,尽管在本用户指南的大多数示例中都是这种情况。如果创建表时没有属性 record_name,则以下代码可确保表中所有记录的名称与表名称相同
mnesia:create_table(subscriber, [])但是,如果像以下示例中所示,使用显式记录名称作为参数创建表,则可以将订阅者记录存储在两个表中,而与表名称无关
TabDef = [{record_name, subscriber}],
mnesia:create_table(my_subscriber, TabDef),
mnesia:create_table(your_subscriber, TabDef).要访问此类表,不能使用简化的访问函数(如前所述)。例如,将订阅者记录写入表需要函数 mnesia:write/3,而不是简化的函数 mnesia:write/1 和 mnesia:s_write/1
mnesia:write(subscriber, #subscriber{}, write)
mnesia:write(my_subscriber, #subscriber{}, sticky_write)
mnesia:write(your_subscriber, #subscriber{}, write)以下简单代码说明了大多数示例中使用的简化访问函数与其更灵活的对应函数之间的关系
mnesia:dirty_write(Record) ->
Tab = element(1, Record),
mnesia:dirty_write(Tab, Record).
mnesia:dirty_delete({Tab, Key}) ->
mnesia:dirty_delete(Tab, Key).
mnesia:dirty_delete_object(Record) ->
Tab = element(1, Record),
mnesia:dirty_delete_object(Tab, Record)
mnesia:dirty_update_counter({Tab, Key}, Incr) ->
mnesia:dirty_update_counter(Tab, Key, Incr).
mnesia:dirty_read({Tab, Key}) ->
Tab = element(1, Record),
mnesia:dirty_read(Tab, Key).
mnesia:dirty_match_object(Pattern) ->
Tab = element(1, Pattern),
mnesia:dirty_match_object(Tab, Pattern).
mnesia:dirty_index_match_object(Pattern, Attr)
Tab = element(1, Pattern),
mnesia:dirty_index_match_object(Tab, Pattern, Attr).
mnesia:write(Record) ->
Tab = element(1, Record),
mnesia:write(Tab, Record, write).
mnesia:s_write(Record) ->
Tab = element(1, Record),
mnesia:write(Tab, Record, sticky_write).
mnesia:delete({Tab, Key}) ->
mnesia:delete(Tab, Key, write).
mnesia:s_delete({Tab, Key}) ->
mnesia:delete(Tab, Key, sticky_write).
mnesia:delete_object(Record) ->
Tab = element(1, Record),
mnesia:delete_object(Tab, Record, write).
mnesia:s_delete_object(Record) ->
Tab = element(1, Record),
mnesia:delete_object(Tab, Record, sticky_write).
mnesia:read({Tab, Key}) ->
mnesia:read(Tab, Key, read).
mnesia:wread({Tab, Key}) ->
mnesia:read(Tab, Key, write).
mnesia:match_object(Pattern) ->
Tab = element(1, Pattern),
mnesia:match_object(Tab, Pattern, read).
mnesia:index_match_object(Pattern, Attr) ->
Tab = element(1, Pattern),
mnesia:index_match_object(Tab, Pattern, Attr, read).活动概念和各种访问上下文
如前所述,执行此处列出的表访问操作的函数对象 (Fun) 可以作为参数传递给函数 mnesia:transaction/1,2,3
mnesia:write/3(write/1,s_write/1)mnesia:delete/3(mnesia:delete/1,mnesia:s_delete/1)mnesia:delete_object/3(mnesia:delete_object/1,mnesia:s_delete_object/1)mnesia:read/3(mnesia:read/1,mnesia:wread/1)mnesia:match_object/2(mnesia:match_object/1)mnesia:select/3(mnesia:select/2)mnesia:foldl/3(mnesia:foldl/4,mnesia:foldr/3,mnesia:foldr/4)mnesia:all_keys/1mnesia:index_match_object/4(mnesia:index_match_object/2)mnesia:index_read/3mnesia:lock/2(mnesia:read_lock_table/1,mnesia:write_lock_table/1)mnesia:table_info/2
这些函数在事务上下文中执行,其中涉及锁定、日志记录、复制、检查点、订阅和提交协议等机制。但是,相同的函数也可以在其他活动上下文中求值。
目前支持以下活动访问上下文
transactionsync_transactionasync_dirtysync_dirtyets
通过将相同的 “fun” 作为参数传递给函数 mnesia:sync_transaction(Fun [, Args]),它将在同步事务上下文中执行。同步事务会等待所有活动的副本都提交事务(到磁盘)后,才从 mnesia:sync_transaction 调用返回。在以下情况下使用 sync_transaction 非常有用
- 当应用程序在多个节点上执行,并希望确保在生成远程进程或将消息发送到远程进程之前,更新已在远程节点上执行。
- 当组合事务使用“脏读”进行写入时,即使用函数
dirty_match_object、dirty_read、dirty_index_read、dirty_select等。 - 当应用程序执行频繁或大量的更新,可能会使其他节点上的
Mnesia过载时。
通过将相同的 “fun” 作为参数传递给函数 mnesia:async_dirty(Fun [, Args]),它将在脏上下文中执行。函数调用映射到相应的脏函数。这仍然涉及日志记录、复制和订阅,但不涉及锁定、本地事务存储或提交协议。检查点保留器会被更新,但会以“脏”方式更新。因此,它们会异步更新。函数等待操作在一个节点上执行,而不是其他节点上执行。如果表驻留在本地,则不会发生等待。
通过将相同的 “fun” 作为参数传递给函数 mnesia:sync_dirty(Fun [, Args]),它将在与函数 mnesia:async_dirty/1,2 几乎相同的上下文中执行。区别在于操作是同步执行的。调用方会等待所有活动副本都执行更新。在以下情况下使用 mnesia:sync_dirty/1,2 非常有用
- 当应用程序在多个节点上执行,并希望确保在生成远程进程或将消息发送到远程进程之前,更新已在远程节点上执行。
- 当应用程序执行频繁或大量的更新,可能会使节点上的
Mnesia过载时。
要检查您的代码是否在事务中执行,请使用函数 mnesia:is_transaction/0。当在事务上下文中调用时,它返回 true,否则返回 false。
存储类型为 RAM_copies 和 disc_copies 的 Mnesia 表在内部实现为 ets 表。应用程序可以直接访问这些表。仅当权衡了所有选项并了解了可能的结果时,才建议这样做。通过将前面提到的 “fun” 传递给函数 mnesia:ets(Fun [, Args]),它将以原始上下文执行。这些操作直接在本地 ets 表上执行,假设本地存储类型为 RAM_copies 并且该表未复制到其他节点。
不会触发订阅,也不会更新任何检查点,但此操作速度极快。磁盘驻留表不能使用 ets 函数进行更新,因为磁盘不会更新。
Fun 也可以作为参数传递给函数 mnesia:activity/2,3,4,这允许使用自定义的活动访问回调模块。可以通过将模块名称直接声明为参数来获得,也可以通过使用配置参数 access_module 隐式获得。自定义的回调模块可用于多种目的,例如提供触发器、完整性约束、运行时统计信息或虚拟表。
回调模块不必访问真实的 Mnesia 表,只要满足回调接口,它就可以自由地执行任何操作。
附录 B,活动访问回调接口 提供了备用实现之一的源代码 mnesia_frag.erl。上下文相关的函数 mnesia:table_info/2 可用于提供有关表的虚拟信息。其中一个用途是在具有自定义回调模块的活动上下文中执行 QLC 查询。通过提供有关表索引和其他 QLC 要求的信息,可以将 QLC 用作访问虚拟表的通用查询语言。
可以在所有这些活动上下文(transaction、sync_transaction、async_dirty、sync_dirty 和 ets)中执行 QLC 查询。仅当表没有索引时,ets 活动才有效。
注意
函数
mnesia:dirty_*始终以async_dirty语义执行,无论启动的是哪个活动访问上下文。它甚至可以在没有任何封闭活动访问上下文的情况下启动上下文。
嵌套事务
事务可以以任意方式嵌套。子事务必须在其父事务所在的同一进程中运行。当子事务终止时,子事务的调用者会获得返回值 {aborted, Reason},并且子事务执行的任何工作都将被擦除。如果子事务提交,则子事务写入的记录会传播到父事务。
当子事务终止时,不会释放任何锁。由一系列嵌套事务创建的锁会一直保留,直到最顶层的事务终止。此外,嵌套事务执行的任何更新都仅以父事务能看到更新的方式进行传播。在顶层事务终止之前,不会进行任何最终提交。因此,即使嵌套事务返回 {atomic, Val},如果封闭的父事务终止,则整个嵌套操作也会终止。
能够拥有与顶层事务具有相同语义的嵌套事务,使得编写操作 Mnesia 表的库函数变得更加容易。
考虑一个将订阅者添加到电话系统的函数
add_subscriber(S) ->
mnesia:transaction(fun() ->
case mnesia:read( ..........此函数需要作为事务调用。假设您希望编写一个函数,该函数既调用 add_subscriber/1 函数,又本身受到事务上下文的保护。通过在另一个事务中调用 add_subscriber/1,将创建一个嵌套事务。
此外,在嵌套时可以混合不同的活动访问上下文。但是,脏操作(async_dirty、sync_dirty 和 ets)如果它们在事务内部调用,则会继承事务语义,从而获取锁并使用两阶段或三阶段提交。
示例
add_subscriber(S) ->
mnesia:transaction(fun() ->
%% Transaction context
mnesia:read({some_tab, some_data}),
mnesia:sync_dirty(fun() ->
%% Still in a transaction context.
case mnesia:read( ..) ..end), end).
add_subscriber2(S) ->
mnesia:sync_dirty(fun() ->
%% In dirty context
mnesia:read({some_tab, some_data}),
mnesia:transaction(fun() ->
%% In a transaction context.
case mnesia:read( ..) ..end), end).模式匹配
当无法使用 mnesia:read/3 函数时,Mnesia 为程序员提供了几个函数,用于根据模式匹配记录。最有用的函数如下:
mnesia:select(Tab, MatchSpecification, LockKind) ->
transaction abort | [ObjectList]
mnesia:select(Tab, MatchSpecification, NObjects, Lock) ->
transaction abort | {[Object],Continuation} | '$end_of_table'
mnesia:select(Cont) ->
transaction abort | {[Object],Continuation} | '$end_of_table'
mnesia:match_object(Tab, Pattern, LockKind) ->
transaction abort | RecordList这些函数将 Pattern 与表 Tab 中的所有记录进行匹配。在 mnesia:select 调用中,Pattern 是下面描述的 MatchSpecification 的一部分。它不一定是对整个表执行穷举搜索。通过在模式的键中使用索引和绑定值,函数实际完成的工作可以压缩为少量哈希查找。如果键是部分绑定的,使用 ordered_set 表可以减少搜索空间。
提供给函数的模式必须是有效的记录,并且所提供的元组的第一个元素必须是表的 record_name。特殊元素 '_' 匹配 Erlang 中的任何数据结构(也称为 Erlang 项)。特殊元素 '$<number>' 的行为类似于 Erlang 变量,也就是说,它们匹配任何内容,绑定第一次出现的值,并将该变量的后续出现与绑定的值进行匹配。
使用函数 mnesia:table_info(Tab, wild_pattern) 获取匹配表中所有记录的基本模式,或使用记录创建中的默认值。不要硬编码模式,因为这会使代码更容易受到记录定义的未来更改的影响。
示例
Wildpattern = mnesia:table_info(employee, wild_pattern),
%% Or use
Wildpattern = #employee{_ = '_'},对于 employee 表,通配符模式如下所示:
{employee, '_', '_', '_', '_', '_',' _'}.要约束匹配,需要替换一些 '_' 元素。以下代码用于匹配所有女性员工:
Pat = #employee{sex = female, _ = '_'},
F = fun() -> mnesia:match_object(Pat) end,
Females = mnesia:transaction(F).匹配函数还可以用于检查不同属性的相等性。例如,查找所有员工编号等于其房间号的员工:
Pat = #employee{emp_no = '$1', room_no = '$1', _ = '_'},
F = fun() -> mnesia:match_object(Pat) end,
Odd = mnesia:transaction(F).函数 mnesia:match_object/3 缺少 mnesia:select/3 具有的一些重要功能。例如,mnesia:match_object/3 只能返回匹配的记录,并且它不能表达除相等性之外的约束。要查找二楼男性员工的姓名:
MatchHead = #employee{name='$1', sex=male, room_no={'$2', '_'}, _='_'},
Guard = [{'>=', '$2', 220},{'<', '$2', 230}],
Result = '$1',
mnesia:select(employee,[{MatchHead, Guard, [Result]}])可以使用 select 函数添加更多约束并创建使用 mnesia:match_object/3 无法实现的输出。
select 的第二个参数是 MatchSpecification。MatchSpecification 是 MatchFunction 的列表,其中每个 MatchFunction 由包含 {MatchHead, MatchCondition, MatchBody} 的元组组成。
MatchHead与前面描述的mnesia:match_object/3中使用的模式相同。MatchCondition是应用于每个记录的额外约束列表。MatchBody构建返回值。
有关匹配规范的详细信息,请参阅 ERTS 用户指南中的“Erlang 中的匹配规范”。有关更多信息,请参阅 STDLIB 中的 ets 和 dets 手册页。
函数 select/4 和 select/1 用于获取有限数量的结果,其中 Continuation 获取下一块结果。Mnesia 仅将 NObjects 作为建议。因此,结果列表中返回的结果可能比 NObjects 指定的结果多或少,即使有更多结果要收集,也可能返回空列表。
警告
在同一事务中对表执行任何修改操作后使用
mnesia:select/1,2,3,4会严重影响性能。也就是说,避免在同一事务中的mnesia:select之前使用mnesia:write/1或mnesia:delete/1。
如果键属性在模式中绑定,则匹配操作是高效的。但是,如果模式中的键属性被给定为 '_' 或 '$1',则必须搜索整个 employee 表以查找匹配的记录。因此,如果表很大,这可能会成为一个耗时的操作,但是如果使用函数 mnesia:match_object,则可以使用索引来解决这个问题(请参阅 索引)。
QLC 查询也可用于搜索 Mnesia 表。通过使用函数 mnesia:table/1,2 作为 QLC 查询中的生成器,您可以让查询在 Mnesia 表上运行。mnesia:table/2 的 Mnesia 特定选项是 {lock, Lock}、{n_objects, Integer} 和 {traverse, SelMethod}。
lock指定Mnesia是否要在表上获取读锁或写锁。n_objects指定要返回给 QLC 的每个块中的结果数。traverse指定Mnesia要用于遍历表的函数。默认使用select,但是通过使用{traverse, {select, MatchSpecification}}作为mnesia:table/2的选项,用户可以指定其自己的表视图。
如果未指定任何选项,则获取读锁,每个块返回 100 个结果,并且使用 select 遍历表,即:
mnesia:table(Tab) ->
mnesia:table(Tab, [{n_objects, 100},{lock, read}, {traverse, select}]).函数 mnesia:all_keys(Tab) 返回表中所有键。
迭代
Mnesia 提供了以下函数,用于迭代表中的所有记录:
mnesia:foldl(Fun, Acc0, Tab) -> NewAcc | transaction abort
mnesia:foldr(Fun, Acc0, Tab) -> NewAcc | transaction abort
mnesia:foldl(Fun, Acc0, Tab, LockType) -> NewAcc | transaction abort
mnesia:foldr(Fun, Acc0, Tab, LockType) -> NewAcc | transaction abort这些函数迭代 Mnesia 表 Tab,并将函数 Fun 应用于每个记录。Fun 接受两个参数,第一个是表中的记录,第二个是累加器。Fun 返回一个新的累加器。
第一次应用 Fun 时,Acc0 是第二个参数。下一次调用 Fun 时,前一次调用的返回值将用作第二个参数。最后一次调用 Fun 返回的项是函数 mnesia:foldl/3 或 mnesia:foldr/3 的返回值。
这些函数之间的区别在于访问 ordered_set 表的顺序。对于其他表类型,这些函数是等效的。
LockType 指定为迭代获取的锁类型,默认为 read。如果在迭代期间写入或删除记录,则要获取写锁。
当无法为函数 mnesia:match_object/3 编写约束时,或者当您想对某些记录执行某些操作时,可以使用这些函数在表中查找记录。
例如,查找所有工资低于 10 的员工,可以如下所示:
find_low_salaries() ->
Constraint =
fun(Emp, Acc) when Emp#employee.salary < 10 ->
[Emp | Acc];
(_, Acc) ->
Acc
end,
Find = fun() -> mnesia:foldl(Constraint, [], employee) end,
mnesia:transaction(Find).要将所有工资低于 10 的员工的工资提高到 10,并返回所有加薪的总和:
increase_low_salaries() ->
Increase =
fun(Emp, Acc) when Emp#employee.salary < 10 ->
OldS = Emp#employee.salary,
ok = mnesia:write(Emp#employee{salary = 10}),
Acc + 10 - OldS;
(_, Acc) ->
Acc
end,
IncLow = fun() -> mnesia:foldl(Increase, 0, employee, write) end,
mnesia:transaction(IncLow).迭代器函数可以完成很多有用的事情,但对于大型表,请注意性能和内存使用情况。
在包含表副本的节点上调用这些迭代函数。每次调用函数 Fun 都会访问表,如果该表位于另一个节点上,则会产生许多不必要的网络流量。
Mnesia 还提供了一些函数,使用户可以迭代表。如果表不是 ordered_set 类型,则迭代顺序是不确定的。
mnesia:first(Tab) -> Key | transaction abort
mnesia:last(Tab) -> Key | transaction abort
mnesia:next(Tab,Key) -> Key | transaction abort
mnesia:prev(Tab,Key) -> Key | transaction abort
mnesia:snmp_get_next_index(Tab,Index) -> {ok, NextIndex} | endOfTablefirst/last 和 next/prev 的顺序仅对 ordered_set 表有效,它们是其他表的同义词。当到达表的末尾时,将返回特殊键 '$end_of_table'。
如果在遍历期间写入和删除记录,请使用 write 锁调用函数 mnesia:foldl/3 或 mnesia:foldr/3。或者在使用 first 和 next 时使用函数 mnesia:write_lock_table/1。
在事务上下文中写入或删除会创建每个修改记录的本地副本。因此,修改大型表中的每个记录会占用大量内存。Mnesia 会补偿在事务上下文中迭代期间写入或删除的每个记录,这可能会降低性能。如果可能,请避免在遍历表之前在同一事务中写入或删除记录。
在脏上下文中,即 sync_dirty 或 async_dirty,修改后的记录不会存储在本地副本中;相反,每个记录都会单独更新。如果表在另一个节点上有副本,则会生成大量的网络流量,并具有脏操作的所有其他缺点。特别是对于命令 mnesia:first/1 和 mnesia:next/2,与之前描述的 mnesia:dirty_first/1 和 mnesia:dirty_next/2 应用的缺点相同,也就是说,在迭代期间不能对表进行任何写入操作。