讲述内容包括锁(table lock和sticky lock)、如何绕开事务(dirty operation)、嵌套事务(nested transaction):
1)事务属性,包括原子性,一致性,隔离性,持久性(ACID)
2)锁
3)脏操作
4)Record名与Table名
5)activity概念和多种访问上下文
6)嵌套事务
7)模式匹配
8)迭代
1,事务属性
在设计容错和分布式系统时事务是一个重要的工具
Mnesia事务是一种机制,通过它,一些数据库操作可以作为一个功能块来执行
作为一个事务来运行的功能块称为一个Functional Object(Fun),它可以对Mnesia记录进行读、写和删除
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(Eno, Raise)包括一个Fun,这个Fun由mnesia:transaction(F)调用并返回一个值
Mnesia事务系统通过提供以下重要特性来方便构建可信任的、分布式的系统:
1)事务handler保证在一个事务中的Fun在对一些表执行一系列的操作时不会干预在其他事物中的操作
2)事务handler保证事务中的操作要么在所有节点上完全原子的成功执行,要么失败并且对所有节点没有任何影响
3)Mnesia事务有四大特性,原子性,一致性,隔离性和持久性,ACID
原子性
原子性意味着事务要么完全成功,要么完全失败
当我们希望在事务里写多条record时,原子性就特别重要
Mnesia是一个分布式DBMS,数据可以在多个服务器节点上备份
原子性保证一个事务要么对所有的节点都生效,要么一个节点也不生效
一致性
一致性保证了事务一直让DBMS处于一个一致的状态
例如,当Erlang或者Mnesia或者computer崩溃时如果有一个写操作正在运行,Mnesia保证了不会出现数据不一致的情况
隔离性
隔离性保证了当事务在网络中不同节点上执行时,对同样的数据record的访问和操作不会互相干扰
这意味着可以并发的执行raise/2方法,在并发控制理论里的一个经典问题是“更新丢失问题(lost update problem)”
如果两个进程想同时给一个employee加薪,这时隔离性就特别有用
例如,employee的初始薪水是5,这时P1开始执行,它读取employee的记录,并给薪水加2
同时,P1由于某种原因暂停了,P2开始运行,读取薪水为5,添加3,这样最终薪水为8
现在P1开始继续运行,结果薪水最终被改为7,P2的工作被擦除了,P2的更新丢失了
而一个事务系统保证同步执行多个操作相同数据的进程是可能的
程序员不需要保证更新是同步的,事务handler会监督这一点,对程序员是透明的
所有通过事务系统访问数据库的程序都可以当作自己是对数据有唯一访问权限的
(译者注:俺发现了数据库的王者!有机会宣扬一下真正好用的数据库是什么样的。“球不是这样踢滴!~”)
持久性
持久性保证事务对DBMS的操作是永久性的
一旦一个事务提交,所有对数据库的更改都必须是持久的——它们被安全的写入硬盘,不会消失或者被损坏
注意,如果将Mnesia配置为纯内存数据库则持久性不起作用
2,锁
不同的事务管理器使用不同的策略来满足隔离属性
Mnesia使用两阶段锁(two-phase locking)标准技术
这意味着记录在读写之前被加锁,Mnesia使用5种不同的锁
1)读锁
在record能被读取之前设置读锁
2)写锁
当事务写一条record时,首先在这条record的所有备份上设置写锁
3)读表锁
如果一个事务扫描整张表来搜索一条record,那么对表里的记录一条一条的加锁效率很低也很耗内存(如果表很大,读锁本身会消耗很多空间)
因此,Mnesia支持对整张表加读锁
4)写表锁
如果事务对表写入大量数据,则可以对整张表设置写锁
5)Sticky锁
位于一个节点上,直到事务结束
当事务执行时,Mnesia使用一个策略来动态获得必要的锁
Mnesia自动加锁和解锁,程序员不用在代码里考虑这些操作
当并发进程对同样的数据操作时会出现死锁的情况
Mnesia使用“等待死亡(wait-die)”策略来解决这种问题
当某个事务尝试加锁时,如果Mnesia怀疑它可能出现死锁,那么该事务就会被强制释放所有锁并休眠一会,然后事务将被再次执行
因此,在事务里的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).
这个事务可能会将“Trying to write ...”写一千次到终端
尽管如此,Mnesia会保证每个事务最终会运行
结果,Mnesia不仅仅释放死锁,也释放活锁
Mnesia程序员不能区分事务的优先级,所以Mnesia DBMS事务系统不适合硬实时应用
但是Mnesia包括其他软实时特性
当事务执行时Mnesia动态加锁和解锁,所以执行有事务副作用的代码是很危险的
特别是一个事务中含有receive语句时容易导致事务一直悬挂着而不会返回,这样就导致锁不被释放
这种场景会导致整个系统停止,因为其他节点的其他进程的其他事务会强制等待这个sb事务
如果事务异常终止,Mnesia将自动释放该事务的锁
如果上面这些例子中的方法不包含在事务中,它们将失败
1)mnesia:transaction(Fun) -> {aborted, Reason} | {atomic, Value}
该方法在一个事务中执行参数Fun这个functional object
2)mnesia:read({Tab, Key}) -> transaction abort | RecordList
该方法使用Key来从Tab中读取record
不管表在哪里,该方法有同样的语义
如果表类型是bag,则read({Tab, Key})可能返回一个非常长的list
如果表类型是set,则list长度为1或者list为[]
3)mnesia:wread({Tab, Key}) -> transaction abort | RecordList
该方法和前面的read/1方法的行为一样,除了它会获取一个写锁而不是一个读锁
如果我们执行的事务中读取一个record,然后修改该record,然后写更新,那么立即设置一个写锁会更高效
如果我们先调用mnesia:read/1,然后调用mnesia:write/1,那么当写操作执行时会马上将读锁升级到写锁
4)mnesia:write(Record) -> transaction abort | ok
该方法写record到数据库
Record参数是一个record实例,该方法返回abort或者ok
5)mnesia:delete({Tab, Key}) -> transaction abort | ok
该方法删除Key对应的所有record
6)mnesia:delete_object(Record) -> transaction abort | ok
该方法删除oid为Record的记录
该方法用来删除一些表类型为bag的记录
Sticky Lock
前面说到,Mnesia读一条record时锁住该record,写一条record时锁住所有该record的备份
尽管如此,有些应用使用Mnesia主要用来保障容错性
这些应用可能配置为一个节点做所有繁重的任务,而另一个备用节点在主节点失败时接替它
这样的应用使用sticky lock可能比使用普通的锁更有用
当首先获取一个锁的事务终止时sticky lock会停留在一个节点上,如以下事务:
F = fun() -> mnesia:write(#foo{a = kalle}) end, mnesia:transaction(F).
foo表备份在N1和N2两个节点上
普通的锁需要:
1)1个网络rpc(2个消息)来获取写锁
2)3个网络消息来执行两阶段提交协议
如果我们使用sticky lock,我们必须首先将代码改为如下:
F = fun() -> mnesia:s_write(#foo{a = kalle}) end, mneisa:transaction(F).
这段代码使用s_write/1方法而不是write/1方法
s_write/1方法设置sticky锁而不是普通锁
如果表没有备份,sticky锁没有任何特殊的效果
如果表示备份的,则我们在N1节点的一条record上上设置sticky锁,Mnesia将会发现锁已经准备好,而不是用网络操作来获得锁
设置本地锁比设置网络锁更高效,因此sticky锁对于使用备份表并且大部分工作都只在其中一个节点上操作的应用更有利
如果N1节点上的一条record打开了sticky锁,这时我们对N2节点上相同的record设置sticky锁,则N1上的record会自动释放sticky锁
这种操作开销很大很消耗性能
表锁
Mnesia支持整表的读锁和写锁来作为单挑record锁的补充
Mnesia自动开锁和解锁,程序员不用对这些操作写代码
但是,如果我们在表上设置表锁时,对某一特定表的大量record的读和写会更高效
这样做会对该表的同步事务阻隔,下面的两个方法是读写操作时显式的对表加锁:
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, Nodes}, LockKind) LockKind ::= read | write | ...
该方法对节点list中所有节点的全局资源GlobalKey上加锁
脏操作
在许多应用里,事务的过度开销可能导致性能丢失
脏操作是绕开事务处理并增加事务速度的捷径
脏操作用在许多场景,例如数据报路由应用里,Mnesia存储路由表,每次接受一个包时期的整个事务非常耗时
因此,Mnesia支持处理表时不加事务的方法,这种方式称为脏操作
但是,意识到避免事务处理的过度开销所带来的交换非常重要:
1)原子性和隔离性丢失了
2)如果我们同步使用脏操作来从同一个表读和写record的话隔离性妥协了,因为使用事务来处理数据的Erlang进程没有从隔离性中得到好处
脏操作的优点是它们比相应的事务处理快的多
如果脏操作处理disc_copies、disc_only_copies类型的表,则写到磁盘
如果脏写操作执行在这种表上,Mnesia也保证所有的表备份一起更新
脏操作将保证某种级别的一致性,例如脏操作不可能返回混乱的record,这样每个单独的读或写操作以一个原子的行为执行
所有的脏操作在失败时调用exit({aborted, Reason}),即使在事务中执行以下方法也不会获取锁:
mnesia:dirty_read({Tab, Key}) mnesia:dirty_write(Record) mnesia:dirty_delete({Tab, Key}) mnesia:dirty_delete_object(Record) mnesia:dirty_first(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_select(Tab, Pat) mnesia:dirty_index_match_oject(Pat, Pos) mnesia:dirty_index_read(Tab, SecondaryKey, Pos) mnesia:dirty_all_keys(Tab)
4,Record名与Table名
Mnesia里表的所有record必须有一样的名字,所有的record必须为相同record类型的实例
但是record名没有必要和table名一样
如果表创建时没有指定record_name属性,那么表里所有的record会有和表名一样的record名
如果多个表创建时指定了相同的record_name属性,那么相同名字的record可以存储在多个表中
访问这些指定了record_name的表时不能简单的使用mnesia:write/1和mnesia:s_write/1,而应该使用mnesia:write/3:
mnesia:write(subscriber, #subscriber{}, write) mnesia:write(my_subscriber, #subscriber{}, sticky_write) mnesia:write(your_subscriber, #subscriber{}, write)
5,Activity概念和多种访问上下文
前面说到,mnesia:transaction/1,2,3的参数functional object(Fun)执行以下表操作:
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
这些操作将在一个涉及到锁、日志、备份、checkpoint、subscription、提交协议等等的事务上下文里执行
但是,这些方法也可以在其他activity上下文里执行
Mnesia支持以下activity访问上下文:
transaction sync_transaciton async_dirty sync_dirty ets
Fun作为参数传给mnesia:sync_transaction(Fun, [,Args])时会在同步事务上下文里执行
同步事务等待所有活动备份提交事务到硬盘才返回
多于在多个节点上执行并且希望确定远程节点更新已被执行的应用,以及联合事务写和dirty_read的应用,sync_transaction特别有用
sync_transaction对于需要频繁和大量可能对其他节点的Mnesia负载过重的更新的应用也有用
6,嵌套事务
事务可以任意嵌套
一个子事务必须运行在和父亲相同的进程里
当子事务终止时,子事务的调用者会得到返回值{aborted, Reason}并且子事务执行的任何任务都会擦除
如果子事务提交,则子事务写入的record会传播到父事务
当子事务终止时不会释放锁
一个嵌套事务序列创建的锁会一直保持直到最顶端的事务终止
而且,嵌套事务所做的更新只传播到父亲可见
不到最顶端事务结束,不会做最终的提交
这样,即使一个嵌套的事务返回{atomic, Val},如果外层的父事务失败,那么嵌套子事务也失败
具有嵌套事务与顶级事务相同语义的能力会让写操作Mnesia表的库函数更容易
add_subscriber(S) -> mnesia:transaction(fun() -> case mneisa:read( ......
该方法需要作为一个事务来调用
现在假设我们希望写一个方法,该方法调用add_subscriber/1方法,并且本身由一个事务上下文保护
通过在另一个事务里简单的调用add_subscriber/1,则创建了一个嵌套事务
7,模式匹配
当不能使用mnesia:read/3方法时,Mnesia提供了一些方法来对record进行模式匹配:
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表的所有记录匹配一个Pattern
没有必要对整张表执行昂贵的搜索,通过使用索引和pattern的key的绑定值,该方法所做真正的工作可能压缩为一些哈希查询
如果key部分绑定,则使用ordered_set表可能减少搜索空间
Pat = #employee{sex = female, _ = '_'}, F = fun() -> mnesia:match_object(Pat) end, Females = mnesia:transaction(F).
8,迭代
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 mneisa:foldr(Fun, Acc0, Tab, LockType) -> NewAcc | transaction abort
这些方法使用Fun方法来对每条record迭代Tab表
Fun有两个参数,第一个是表的一条记录,第二个参数是accumulator,返回值是一个新的accumulator
如找出所有salary小于10的employee:
find_low_salaries() -> Constraint = fun(Emp, Acc) when Emp#employee.salary < 10 -> [Emp | Acc]; (_, Acc) -> Acc end, Find = fun() -> mnesia:foldl(Contraint, [], employee) end, mnesia:transaction(Find).
(译者注:太多了,api的讲解太过冗繁,所以忽略一部分内容)