例如,当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里传递消息,可能会产生一些很奇怪的结果:
Java代码
? bad_raise(Eno, Raise) ->
?? F = fun() ->
?? [E] = mnesia:read({employee, Eno}), ?? Salary = E#employee.salary + Raise, ?? New = E#employee{salary = Salary}, ?? io:format(\, []), ?? mnesia:write(New) ?? end,
?? mnesia:transaction(F).
bad_raise(Eno, Raise) -> F = fun() ->
[E] = mnesia:read({employee, Eno}), Salary = E#employee.salary + Raise, New = E#employee{salary = Salary}, io:format(\ 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会停留在一个节点上,如以下事务:
Java代码
?? F = fun() ->
?? mnesia:write(#foo{a = kalle}) ?? end,
?? mnesia:transaction(F).
F = fun() ->
mnesia:write(#foo{a = kalle}) end,
mnesia:transaction(F).
foo表备份在N1和N2两个节点上 普通的锁需要:
1)1个网络rpc(2个消息)来获取写锁 2)3个网络消息来执行两阶段提交协议
如果我们使用sticky lock,我们必须首先将代码改为如下:
Java代码
?? F = fun() ->
?? mnesia:s_write(#foo{a = kalle}) ?? end,
?? mneisa:transaction(F).
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的读和写会更高效 这样做会对该表的同步事务阻隔,下面的两个方法是读写操作时显式的对表加锁:
Java代码
?? mnesia:read_lock_table(Tab) ?? mnesia:write_lock_table(Tab)
mnesia:read_lock_table(Tab) mnesia:write_lock_table(Tab)
另外的获得表锁的方法:
Java代码
?? mnesia:lock({table, Tab}, read) ?? mnesia:lock({table, Tab}, write)
mnesia:lock({table, Tab}, read) mnesia:lock({table, Tab}, write) 全局锁
如果表有备份,那么写锁一般会需要在所有节点上打开,而读锁则在一个节点上 mnesia:lock/2用来支持表所,但也可以用在不管表如何备份的场景:
Java代码
?? mnesia:lock({global, GlobalKey, Nodes}, LockKind) ?? LockKind ::= read | write | ...
mnesia:lock({global, GlobalKey, Nodes}, LockKind) LockKind ::= read | write | ...
该方法对节点list中所有节点的全局资源GlobalKey上加锁 脏操作
在许多应用里,事务的过度开销可能导致性能丢失 脏操作是绕开事务处理并增加事务速度的捷径
脏操作用在许多场景,例如数据报路由应用里,Mnesia存储路由表,每次接受一个包时期的整个事务非常耗时
因此,Mnesia支持处理表时不加事务的方法,这种方式称为脏操作 但是,意识到避免事务处理的过度开销所带来的交换非常重要: 1)原子性和隔离性丢失了
2)如果我们同步使用脏操作来从同一个表读和写record的话隔离性妥协了,因为使用事务来处理