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

5.交易和其他访问上下文 | 5. Transactions and Other Access Contexts

本节介绍Mnesia构成Mnesia容错分布式数据库管理系统(DBMS)的事务系统和事务属性。

本节还介绍了锁定功能,包括表锁和粘滞锁,以及绕过交易系统的替代功能,以提高速度并降低开销。这些功能被称为“脏操作”。还描述了嵌套事务的使用。包括以下主题:

  • 事务属性,包括原子性,一致性,隔离性和持久性
  • 锁定
  • 脏操作
  • 记录名称与表名
  • 活动概念和各种访问上下文
  • 嵌套事务
  • 模式匹配
  • 迭代

5.1 交易属性

在设计容错分布式系统时,事务很重要。甲Mnesia交易是通过该一系列的数据库操作可以作为一个功能块执行的机制。作为事务运行的功能块称为功能对象(Fun),此代码可以读取,写入和删除Mnesia记录。Fun被评估为提交或终止的事务。如果事务成功执行Fun,它将在所涉及的所有节点上复制该操作,或者在发生错误时终止操作。

以下示例显示提高某些员工编号的薪水的交易:

代码语言:javascript
复制
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交易有四个重要属性,称为原子性,一致性,隔离性和耐久性(ACID)。 这些属性在以下各节中进行介绍。

原子性

原子性意味着由事务执行的数据库更改会在所涉及的所有节点上或在任何节点上生效。也就是说,交易要么完全成功,要么完全失败。

当需要在同一事务中原子写入多条记录时,原子性非常重要。 上例中显示的函数raise / 2仅写入一条记录。 Insert_emp / 3(入门指南中的程序清单中显示)将记录员工以及员工关系(如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配置为“纯”主存储器数据库的情况。

5.2 锁定

不同的交易管理者采用不同的策略来满足隔离属性。 Mnesia使用两相锁定的标准技术。 也就是说,在读取或写入之前,会在记录上设置锁。 Mnesia使用以下锁定类型:

  • 读锁。读取锁定在记录的一个副本上设置,然后才能读取。
  • 写锁。每当事务写入记录时,首先在该特定记录的所有副本上设置写锁。
  • 阅读表锁。如果事务遍历整个表以搜索满足某些特定属性的记录,则逐个设置记录上的读锁是最为低效的。这也是内存消耗,因为如果表格很大,读取锁定本身会占用相当大的空间。因此,Mnesia可以在整个表上设置读锁。
  • 写表锁。如果事务将很多记录写入一个表中,则可以在整个表上设置写入锁定。
  • 粘滞的锁。这些是在启动锁的事务终止后,在节点上保留原位的写入锁。

Mnesia采用一种策略来实现功能,例如mnesia:read/1在交易执行时动态获取必要的锁。Mnesia自动设置和释放锁,程序员不需要编写这些操作。

当并发进程在同一记录上设置和释放锁时,会发生死锁。Mnesia采用“等待死亡”战略来解决这些情况。如果Mnesia怀疑事务尝试设置锁定时可能发生死锁,则该事务将被迫释放其所有锁并休眠一段时间。交易中的乐趣再次被评估。

因此,赋予Fun的代码mnesia:transaction/1是纯粹的,这一点很重要。例如,如果消息由事务Fun发送,则会出现一些奇怪的结果。以下示例说明了这种情况:

代码语言:javascript
复制
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在事务执行时动态设置和释放锁。 因此,执行具有交易副作用的代码是很危险的。 特别是,事务内部的接收语句会导致事务处于挂起状态而永远不会返回,从而导致锁定不能释放。 这种情况会导致整个系统陷于停顿,因为在其他进程或其他节点上执行的其他事务被迫等待有缺陷的事务。

如果事务异常终止,则Mnesia自动释放事务持有的锁。

到目前为止,已经展示了可以在事务中使用的许多函数的例子。以下列表显示了使用事务的最简单的 Mnesia函数。注意这些函数必须嵌入到事务中。如果不存在封闭事务(或其他封闭Mnesia活动),则它们全部失败。

mnesia:transaction(Fun) - > {aborted,Reason} | {atomic,Value}用函数对象Fun作为单个参数执行一个事务。

mnesia:read({Tab,Key}) - >事务中止| RecordList使用Key从表Tab中读取所有具有Key的记录。无论Table的位置如何,该函数都具有相同的语义。如果表的类型是bag,则读({Tab,Key})可以返回一个任意长的列表。如果该表的类型是set,则列表的长度为1或[]。

mnesia:wread({Tab,Key}) - >事务中止| RecordList的行为方式与前面列出的函数read / 1的行为相同,只是它获取了写入锁而不是读取锁。要执行读取记录的事务,修改记录,然后写入记录,立即设置写入锁定会稍微高效。当发出mnesia:read / 1时,后跟mnesia:write / 1,写操作执行时,必须将第一个读锁升级为写锁。

mnesia:写(记录) - >事务中止| ok将一条记录写入数据库。参数记录是记录的一个实例。函数返回ok,或者在发生错误时终止事务。

mnesia:delete({Tab,Key}) - >事务中止|确定使用给定的键删除所有记录。

mnesia:delete_object(Record) - > transaction abort |确定使用OID记录删除记录。使用此功能只删除袋式表格中的一些记录。

粘滞锁

如前所述,Mnesia使用的锁定策略是在读取记录时锁定一条记录,并在写入记录时锁定记录的所有副本。 但是,有些应用程序主要使用Mnesia的容错特性。 这些应用程序可以配置为一个节点执行所有繁重的工作,并且备用节点可以在主节点发生故障时准备好接管。 这些应用程序可以使用粘性锁代替正常的锁定方案。

粘滞锁是在第一次获取锁的事务终止之后,在节点上保持就位的锁。为了说明这一点,假设执行以下事务:

代码语言:javascript
复制
F = fun() ->
      mnesia:write(#foo{a = kalle})
    end,
mnesia:transaction(F).

foo表被复制到两个节点N1N2

正常锁定需要以下内容:

  • 一个网络RPC(两条消息)获取写入锁定
  • 三条网络消息执行两阶段提交协议

如果使用粘滞锁,则代码必须首先更改如下:

代码语言:javascript
复制
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

获取表锁的其他语法如下:

代码语言:javascript
复制
mnesia:lock({table, Tab}, read)
mnesia:lock({table, Tab}, write)

匹配操作Mnesia可以锁定整个表或只锁定一个记录(当键在模式中绑定时)。

Global Locks

写锁通常在表的副本所在的节点上(并处于活动状态)获取。读取锁定是在一个节点上获得的(本地副本如果存在本地副本)。

该函数mnesia:lock/2旨在支持表锁(如前所述),但也适用于无论表如何被复制都需要获取锁的情况:

代码语言:javascript
复制
mnesia:lock({global, GlobalKey, Nodes}, LockKind)

LockKind ::= read | write | ...

LockItem在节点列表中的所有节点上获取。

5.3脏操作

在许多应用程序中,处理事务的开销可能会导致性能下降。肮脏的操作是绕过大部分处理并提高交易速度的捷径。

例如,在Mnesia存储路由表的数据报路由应用程序中,脏操作通常很有用,并且每次接收到数据包时启动整个事务都很耗时。 因此,Mnesia具有不使用事务操作表的功能。 这种处理方式被称为肮脏操作。 但是,请注意避免交易处理开销的权衡:

  • 原子性和隔离性Mnesia丧失。
  • 隔离属性会受到影响,因为如果使用脏操作同时读取和写入同一个表中的记录,则使用事务操作数据的其他Erlang进程不会受益于隔离。

脏操作的主要优点是它们的执行速度要比在事务中作为功能对象处理的等效操作快得多。

如果在disc_copies类型的表上执行脏操作或将其写入disc_only_copies,则会将其写入光盘。 Mnesia还确保如果在表上执行脏写入操作,则会更新表的所有副本。

肮脏的操作确保了一定程度的一致性。例如,脏操作不能返回乱码记录。因此,每个单独的读取或写入操作以原子方式执行。

所有脏函数执行一个调用来退出({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中的“第一个”键。

套餐或手提袋里的记录没有排序。但是,用户不知道有记录的订单。这意味着可以通过函数mnesia:dirty_next / 2来遍历一个表。

如果表中没有记录,则该函数返回原子'$ end_of_table'。不建议将此原子用作任何用户记录的关键字。

mnesia:dirty_next(Tab,Key)返回表格Tab中的“下一个”键。该函数可以遍历一个表并对表中的所有记录执行一些操作。当到达表的末尾时,返回特殊键“$ end_of_table”。否则,该函数返回一个可用于读取实际记录的密钥。

如果任何进程在使用函数dirty_next / 2遍历表时执行对表的写操作,则行为是未定义的这是因为对Mnesia表的写操作可能导致表本身的内部重组。这是一个实现细节,但请记住脏函数是低级函数。

mnesia:dirty_last(Tab)的工作原理与mnesia:dirty_first / 1完全相同,但返回Erlang词汇顺序中的最后一个对象,用于表类型ordered_set。对于所有其他表类型,mnesia:dirty_first / 1和mnesia:dirty_last / 1是同义词。

mnesia:dirty_prev(Tab,Key)的工作方式与mnesia:dirty_next / 2完全相同,但返回Erlang术语顺序中的先前对象,用于表类型ordered_set。对于所有其他表类型,mnesia:dirty_next / 2和mnesia:dirty_prev / 2是同义词。

mnesia:dirty_slot(Tab,Slot)返回表中与Slot关联的记录列表。它可以用类似于函数dirty_next / 2的方式遍历一个表。一个表格有多个从零到一些未知上限的插槽。当达到表的末尾时,函数dirty_slot / 2返回特殊原子'$ end_of_table'。

如果在遍历期间写入表,则此函数的行为是不确定的。函数mnesia:read_lock_table(Tab)可用于确保在迭代过程中不执行受事务保护的写操作。

mnesia:dirty_update_counter({Tab,Key},Val)。计数器是具有大于或等于零的值的正整数。更新计数器会添加Val和计数器,其中Val是正整数或负整数。

Mnesia没有特殊的柜台记录。但是,{TabName,Key,Integer}形式的记录可以用作计数器,并且可以是持久性的。

计数器记录的事务保护更新是不可能的。

使用这个函数有两个显着的区别,而不是读取记录,执行算术和写入记录:

代码语言:txt
复制
- It is much more efficient. 
- The funcion [`dirty_update_counter/2`](mnesia#dirty_update_counter-2) is performed as an atomic operation although it is not protected by a transaction. Therfore no table update is lost if two processes simultaneously execute the function `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的肮脏等价物。

5.4记录名称与表名称

Mnesia,表中的所有记录必须具有相同的名称。所有记录必须是相同记录类型的实例。但是,记录名称并不一定必须与表名相同,不过,在本用户指南的大多数示例中都是如此。如果创建的表没有属性record_name,则以下代码确保表中的所有记录与表的名称相同:

代码语言:javascript
复制
mnesia:create_table(subscriber, [])

但是,如果使用显式记录名称作为参数来创建表,则无论表名是什么,订户记录都可以存储在两个表中:

代码语言:javascript
复制
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:

代码语言:javascript
复制
mnesia:write(subscriber, #subscriber{}, write)
mnesia:write(my_subscriber, #subscriber{}, sticky_write)
mnesia:write(your_subscriber, #subscriber{}, write)

以下简单代码说明了大多数示例中使用的简化访问函数与其更灵活的对应函数之间的关系:

代码语言:javascript
复制
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).

5.5 活动概念和各种访问上下文

如前所述,执行表访问操作的函数对象(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/1
  • mnesia:index_match_object/4 (mnesia:index_match_object/2)
  • mnesia:index_read/3
  • mnesia:lock/2 (mnesia:read_lock_table/1, mnesia:write_lock_table/1)
  • mnesia:table_info/2

这些函数在涉及机制的事务上下文中执行,例如锁定,日志记录,复制,检查点,预订和提交协议。但是,也可以在其他活动环境中评估相同的功能。

目前支持以下活动访问上下文:

  • transaction
  • sync_transaction
  • async_dirty
  • sync_dirty
  • ets

通过将相同的“fun”作为参数传递给函数mnesia:sync_transaction(Fun [,Args]),它将在同步的事务上下文中执行。 同步事务等待,直到所有活动副本在从mnesia:sync_transaction调用返回之前提交事务(到光盘)。 在以下情况下使用sync_transaction很有用:

  • 当某个应用程序在多个节点上执行时,并且希望在生成远程进程或将消息发送到远程进程之前确保在远程节点上执行更新。
  • 当组合事务使用“dirty_reads”写入时,即,函数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]),它会在原始上下文中执行。 假设本地存储类型为RAM_copies,并且该表不在其他节点上复制,则操作直接在本地ets表上执行。

订阅不会被触发,也不会更新检查点,但此操作非常快速。由于光盘没有更新,因此光盘常驻表不能用该ets功能更新。

Fun也可以作为参数传递给函数mnesia:activity / 2,3,4,它可以使用定制的活动访问回调模块。 它可以通过将模块名称指定为参数直接获取,也可以通过使用配置参数access_module隐式获取。 自定义回调模块可用于多种用途,例如提供触发器,完整性约束,运行时统计信息或虚拟表。

回调模块不必访问真正的Mnesia表格,只要满足回调接口,它就可以自由地做任何事情。

附录B,活动访问回调接口提供了一个替代实现的源代码mnesia_frag.erl。 上下文敏感函数mnesia:table_info / 2可用于提供有关表的虚拟信息。 这样做的一个用途是在具有自定义回调模块的活动上下文中执行QLC查询。 通过提供有关表索引和其他QLC需求的表信息,QLC可以用作通用查询语言来访问虚拟表。

QLC查询可以在所有这些活动上下文(事务,sync_transaction,async_dirty,sync_dirty和ets)中执行。 只有当表没有索引时,ets活动才起作用。

注意

函数mnesia:dirty_ *总是使用async_dirty语义执行,而不管启动了哪些活动访问上下文。 它甚至可以启动上下文而不需要任何封闭的活动访问上下文。

5.6 嵌套事务

事务可以以任意方式嵌套。小孩交易必须在与其父母相同的过程中运行。当一个子事务终止时,子事务的调用者获得返回值,{aborted, Reason}与子任务执行的任何工作都将被清除。如果一个子交易提交,那么由孩子写的记录会传播给父母。

当子交易终止时,不会释放锁。 由一系列嵌套事务创建的锁保持到顶层事务终止。 此外,嵌套事务执行的任何更新只会以这种方式进行传播,以便嵌套事务的父代会看到更新。 在顶级交易终止之前,不会做出最终承诺。 因此,虽然嵌套事务返回{atomic,Val},但如果封闭父事务终止,则整个嵌套操作终止。

使用与顶级事务相同的语义嵌套事务的能力使编写操作Mnesia表的库函数变得更加容易。

考虑一个将用户添加到电话系统的功能:

代码语言:javascript
复制
add_subscriber(S) ->
    mnesia:transaction(fun() ->
        case mnesia:read( ..........

这个功能需要被称为事务。假设您希望编写一个既调用函数add_subscriber/1又受事务上下文保护的函数。通过add_subscriber/1从另一个事务中调用,创建嵌套事务。

而且,嵌套时可以混合不同的活动访问上下文。 但是,如果在事务内部调用了这些内容(async_dirty,sync_dirty和ets),它们会继承事务语义,从而获取锁并使用两个或三个阶段提交。

例:

代码语言:javascript
复制
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).

5.7 模式匹配

当mnesia:read / 3函数无法使用时,Mnesia向编程人员提供了几种函数来匹配模式的记录。 最有用的是以下几点:

代码语言:javascript
复制
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

这些函数将一个模式与表格Tab中的所有记录进行匹配。 在mnesia:select调用中,Pattern是以下所述的MatchSpecification的一部分。 它不一定是作为整个表的详尽搜索来执行的。 通过在模式的关键字中使用索引和绑定值,函数完成的实际工作可以被压缩为几个哈希查找。 如果键被部分绑定,使用ordered_set表可以减少搜索空间。

提供给函数的模式必须是有效的记录,并且提供的元组的第一个元素必须是表的record_name。 特殊元素'_'匹配Erlang中的任何数据结构(也称为Erlang术语)。 特殊元素'$ <number>'表现为Erlang变量,即它们匹配任何内容,绑定第一个匹配项,并将该变量出现的次数与绑定值进行匹配。

使用函数mnesia:table_info(Tab,wild_pattern)获得一个基本模式,该模式匹配表中的所有记录,或使用记录创建中的默认值。 不要使模式变为硬编码,因为这会使代码更易受到未来记录定义更改的影响。

例:

代码语言:javascript
复制
Wildpattern = mnesia:table_info(employee, wild_pattern), 
%% Or use
Wildpattern = #employee{_ = '_'},

对于员工表,模式如下所示:

代码语言:javascript
复制
{employee, '_', '_', '_', '_', '_',' _'}.

为了限制匹配,需要替换一些'_'元素。用于匹配所有女性员工的代码如下所示:

代码语言:javascript
复制
Pat = #employee{sex = female, _ = '_'},
F = fun() -> mnesia:match_object(Pat) end,
Females = mnesia:transaction(F).

匹配函数也可以用来检查不同属性的相等性。例如,要查找员工编号等于其房间号的所有员工:

代码语言:javascript
复制
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只能返回匹配的记录,并且不能表达除平等以外的约束。 要找到二楼男性员工的姓名:

代码语言:javascript
复制
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

要选择的第二个参数是MatchSpecification。 MatchSpecification是一个MatchFunctions列表,其中每个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之前使用mnesia:write / 1或mnesia:delete / 1:在同一个事务中选择。

如果键属性绑定在一个模式中,则匹配操作是有效的。 但是,如果模式中的键属性以'_'或'$ 1'给出,则必须搜索整个员工表以查找匹配的记录。 因此,如果表很大,这可能会变成一个耗时的操作,但如果使用函数mnesia:match_object,则可以通过索引来修复它(请参阅索引)。

QLC查询也可以用来搜索Mnesia表。 通过在QLC查询中使用函数mnesia:table / [1 | 2]作为生成器,可以让查询在Mnesia表上运行。 Mnesia的具体选项:table / 2是{lock,Lock},{n_objects,Integer}和{遍历,SelMethod}:

锁指定Mnesia是否要获取表上的读取或写入锁定。

n_objects指定将每个块返回到QLC的结果数量。

遍历指定Mnesia将使用哪个函数遍历表。 使用默认选择,但通过使用{traverse,{select,MatchSpecification}}作为mnesia:table / 2的选项,用户可以指定它自己的表视图。

如果未指定选项,则会获取读取锁定,每个块中返回100个结果,并select用于遍历表,即:

代码语言:javascript
复制
mnesia:table(Tab) ->
    mnesia:table(Tab, [{n_objects,100},{lock, read}, {traverse, select}]).

该函数mnesia:all_keys(Tab)返回表中的所有键。

5.8 迭代

Mnesia 提供了迭代表中所有记录的以下函数:

代码语言:javascript
复制
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指定要为迭代获取哪种类型的锁,默认为读取。 如果在迭代期间写入或删除记录,则会获取写入锁定。

当不可能为函数编写约束mnesia:match_object/3时,或者当您想对某些记录执行某些操作时,可以使用这些函数在表中查找记录。

例如,找到所有薪金低于10美元的员工可以看起来如下:

代码语言:javascript
复制
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,并返回所有加薪的总和:

代码语言:javascript
复制
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

代码语言:javascript
复制
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} | endOfTable

first / last和next / prev的顺序仅对ordered_set表有效,它们是其他表的同义词。 当到达表的末尾时,返回特殊键“$ end_of_table”。

如果在遍历过程中写入和删除记录,则使用写入锁定函数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所述的相同缺点也适用,也就是说,在迭代期间不需要写入表格。

扫码关注腾讯云开发者

领取腾讯云代金券