erlang学习笔记--------mnesia数据库

2022-04-11 00:00:00 函数 操作 事务 节点 记录

事务及其它访问

  • 事务属性,包括原子性,一致性,隔离性,持久性
  • 脏操作
  • 记录名字与表名字
  • 活动概念与访问上下文
  • 嵌套事务
  • 模式匹配
  • Iteratoin

1、事务属性

Mnesia事务就是将一系列数据库操作封装在一个函数块中。函数块作为一个事务进行运行所有叫作函数对象。保作将影响到所有相关节点上。

Mnesia提供了如下重要属性:

  • 事务函数内部不涉及操作在其它事务中,当它在执行一系列表操作时
  • 事务保证了要么在所有节点上操作成功,要么失败但没有在任何节点上产生负作用
  • 提供了Atomicity(原子性),Consistency(一致性),Isolation(隔离性),Durability(持久性).ACID.

2、锁

Mnesia使用5种锁

  • 读锁, 在复制记录读前加读锁
  • 写锁, 在事务写记录前,会在指定记录的所有复件上添加写锁
  • 读表锁,如果一个事务遍历整个表搜索指定条件记录,低效是设置记录锁,同时也是大量内存消耗。些时可以指定一个读表锁。
  • 写表锁,如果一个事务要大量写入一个表,好你用写表锁
  • 粘锁,当一个事务操作完成后,锁依然存在。

Mnesia采用动态策略应对如mnesia:read/1,自动添加和释放锁。程序员无须考虑。

Mnesia不用担心死锁问题,当系统怀疑某个锁死锁时,它会释放该锁,然后再执行一遍, 多个相同事务不能保证顺序执行,但可以保证都执行。程序员不能设定某个事务的优先级。

切不可执行代码带有事务副作用。如在receive语句在事务,可能产生系统假死等。

当一个事务异常终止时,Mnesia会自动的释放其持有的所有锁。

Mnesia函数:

mnesia:transaction(Fun) -> {aborted, Reason} | {atomic, Value},该函数执行一个带函数Fun的事务。

mnesia:read({Tab, Key}) -> transaction abort | RecordList, 返回所有带Key的记录

mnesia:wread({Tab, Key}),该函数和上一函数相同,除由读锁改为写锁,如果执行一个读记录,修改, 写入记录,那么直接用写锁效率更高。

mnesia:write(Record).写入一条记录到数据库
mnesia:delete({Tab, Key}),删除指定表键的所有记录

mnesia:delete_object(Record)用Record的OID删除对应记录

粘锁:

普通情况下mnesia每次写入操作的时候,都会锁住所有复件。如果针对一个大量写入到在一个复件的情况下, 那么粘锁就可以派上用场了。在没有其它复件存在的情况下,粘锁和普通锁差不多,没有什么特别影响。

粘锁在次使用后, 并不立即释放。下次我们使用粘锁在同一节点的同一记录上,那么这个粘锁就已经设置好了。所有效率更高。多用于一主多从库, 对于有用到两复件间交互则消耗较大。

表锁:

如果我们在表上进行大量记录的读写操作, 那么设置表锁定会更加高效,但是会阻塞其它并行事务。我用以下函数进行设置:

mnesia:read_lock_table(Tab) 在表上设置一个读锁

mnesia:write_lock_table(Tab)在表上设置一个写锁

或者

mnesia:lock({table,Tab},read)

mnesia:lock({table,Tab},write)

 

全局锁

通常情况下,写锁都会锁住所有活动节点的复件。读锁仅需要一个节点。函数mnesia:lock/2可以锁住所有复件表

mnesia:lock({global, GlobalKey, Node}, LockKind)

LockKind ::= read | write | ...

3、脏操作

大量的事务开锁可能导致低效,可以引入脏操作。可以应用在报文路由类应用,使用mnesia函数不带事务,这就是脏操作,需要权衡失去了mnesia的原子性和隔离性。主要优势就是执行更快。

脏操作也是保证了记录操作的一致性的。每一个单一读写操作都是原子操作。所有操作失败返回({aborted, Reason})。具体脏操作函数如下:

mnesia:dirty_read({Tab, Key}),

mnesia:dirty_write(Record)

mnesia:dirty_delete({Tab,Key})

mnesia:dirty_delete_object(Record)

mnesia:dirty_firest(Tab)

mnesia:dirty_next(Tab, key)

mnesia:dirty_last(Tab)

mnesia:dirty_prev(Tab, Key)

mnesia:dirty_slot(Tab, Slot)

mnesia:dirty_update_counter({Tab, Key}, Val)

mnesia:dirty_match_object(Pat)

mnesia:dirty_index_match_object(Pat, Pos)

mnesia:dirty_all_key(Tab)
4、记录名与表名

在Mnesia中,在一个表中所有记录名字必须相同。所有记录必须是同一记录类型。但是记录名字可以与表名字不同。

 mnesia:create_table(subscriber, [])

 TabDef = [{record_name, subscriber}],
 mnesia:create_table(my_subscriber, TabDef),
 mnesia:create_table(your_subscriber, TabDef).

 

mnesia:write(subscriber, #subscriber{}, write)
mnesia:write(my_subscriber, #subscriber{}, sticky_write
mnesia:write(your_subscriber, #subscriber{}, write)

 5、活动概念与访问上下文
如下函数对象都可以作为事务函数mnesia:tansaction的参数:

mnesia:write/3 (write/1, s_write/1)
mnesia:delete/3 (delete/1, s_delete/1)

mnesia:delete_object/3 (delete_object/1, s_delete_object/1)
mnesia:read/3 (read/1, wread/1)
mnesia:match_object/2 (match_object/1)
mnesia:select/3 (select/2)
mnesia:foldl/3 (foldl/4, foldr/3, foldr/4)
mnesia:all_keys/1
mnesia:index_match_object/4 (index_match_object/2)
mnesia:index_read/3
mnesia:lock/2 (read_lock_table/1, write_lock_table/1)
mnesia:table_info/2

这些函数能够运行在事务活动中,也可以运行在如下活动中:

  • transaction(事务)
  • sync_transaction(同步事务)
  • async_dirty(脏异步)
  • sync_dirty(脏同步)
  • ets

sync_transaction会等待所有活动复件上的事务操作完成后才返回。

sync_dirty会等待所有活动复件上的脏操作完成后返回

存储类型为RAM_copies , disc_copies的Mnesia表内部是以“ets-tables"实现的。它允许用户直接访问表,mnesia:ets/2将按照非常原始的上下文进行处理,它会假设这些表存储类型为RAM_copies ,没有复件在其它节点 ,没有订阅触发,没有检查点更新。

6、嵌套事务

事务可以进行嵌套, 子事务必须和父事务运行在同一进程,当子事务中止时,子事务的调用者会收到{aborted, Reason},任何子事务操作都会回滚。如果子事务提交, 由子事务写入的记录将会产生在父事务。

所有锁的释放是在上层事务终止后。

   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).

 7、模式匹配

当使用mnesia:read/3不能满足需求的时候, mnesia提供了以下函数用于匹配记录:

    mnesia:select(Tab, MatchSpecification, LockKind) ->
        transaction abort | [ObjectList]
    mnesia:select(Tab, MatchSpecification, NObjects, Lock) ->  
        transaction abort | {[Object],Continuation} | 'endoftable′mnesia:select(Cont)−>transactionabort|[Object],Continuation|′endoftable′mnesia:select(Cont)−>transactionabort|[Object],Continuation|′end_of_table'
    mnesia:match_object(Tab, Pattern, LockKind) ->
        transaction abort | RecordList
  表中记录以hash保存,或者ordered_set都是有提升查询效率。在数据匹配中,‘_'表示任意数据结构,匹配元组的个元素必须为记录名字。‘$<number>'作为Erlang变量。

 如下:

   Wildpattern = mnesia:table_info(employee, wild_pattern), 
  %% Or use
  Wildpattern = #employee{_ = '_'},意义如  {employee, '_', '_', '_', '_', '_',' _'}.

 又如:

 Pat = #employee{sex = female, _ = '_'},
 F = fun() -> mnesia:match_object(Pat) end,
 Females = mnesia:transaction(F).

 假如我们要找房号与职员号是相同的职员:

  Pat = #employee{emp_no = '1′,roomno=′1′,roomno=′1', _ = '_'},
  F = fun() -> mnesia:match_object(Pat) end,
  Odd = mnesia:transaction(F).

 假如我们找在二楼的男性职员:

  MatchHead = #employee{name='1', sex=male, room_no={'$2', '_'}, _='_'},   Guard = [{'>=', '$2', 220},{'<', '$2', 230}],   Result = '1', sex=male, room_no={'$2', '_'}, _='_'},   Guard = [{'>=', '$2', 220},{'<', '$2', 230}],   Result = '1',
  mnesia:select(employee,[{MatchHead, Guard, [Result]}])

 8、Iteration (迭代)

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
 这些函数会将Fun遍历应用到表Tab表上,并把结构放入累计集Acc0中,可以按需要指定锁类型。Fun有两个参数,个是从表中取出的记录,第二个是累计集。

 例如要查找薪资级别在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级,返回所有涨薪和:

      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).
在遍历中可以做很多事情,但是要特别留意性能和内存消耗。

在调用这些函数时,如果表在另外一个节点上,会消耗很不必要的网络traffic。

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} | endOfTable
其中函数mnesia:first/1和last/1只对 ordered_set有效。当搜索到'$end_of_table'就退出。

在用mnesia:fold遍历时,进行删除或者写入操作都会创建一个本地拷贝进行修改。所有会占用大量内存。可能会降低性能,所有尽量避免。

在脏操作上下文中,修改记录不保存在本地拷贝,每条件记录都是分别更新。如果表复件存在其它节点中,会产生大量网络开销。特别是mnesai:first/1 , mnesia:next/2,同理dirty_first和dirty_next.不要在遍历的时候进行写操作。

相关文章