Mnesia:如何同时锁定多行,以便我可以写/读一组“一致”的记录

时间:2011-10-30 02:05:31

标签: erlang mnesia

我如何希望我已经开始提出我的问题

拿一张带有26个键的表,a-z并让它们具有整数值。 创建一个过程,哎呀,一遍又一遍地做两件事

  1. 在一个事务中,为 a b c 写入随机值,使这些值始终总和为10
  2. 在另一个事务中,读取 a b c 的值,并在其值不等于10时抱怨
  3. 如果您启动其中一些流程,您会很快看到 a b c 处于某种状态他们的值不总和为10.我相信没有办法要求mnesia“在开始写入(或读取)之前锁定这3条记录”,只能让mnesia将记录锁定为它到达了他们(可以这么说),它允许记录集的值违反我的“必须加总到10”的约束。

    如果我是对的,这个问题的解决方案包括

    1. 在写入(或读取)3条记录之前锁定整个表格 - 我讨厌锁定整个表格以进行3次记录,
    2. 创建一个流程,跟踪谁正在读取或写入哪些密钥,并保护批量操作与其他任何人一起编写或读取,直到操作完成。当然我必须确保所有进程都使用了这个...废话,我想这意味着将我自己的AccessMod写为activity / 4的第四个参数,这似乎是一项非常重要的练习
    3. 其他一些我不够聪明的事情。
    4. 想法?

      好的,我是一个雄心勃勃的Erlang newbee,很抱歉,如果这是一个愚蠢的问题,但是

      我正在构建一个特定于应用程序的内存分布式缓存,我需要能够在一个事务中编写 Key,Value 对的集合,并在一个事务中检索值集。换句话说,我需要 1)将40个键,值对写入高速缓存,并确保在此多键写入操作期间没有其他人可以读取或写入这40个键中的任何一个;和, 2)在一次操作中读取40个键并获得40个值,知道从读取操作开始到结束的所有40个值都没有变化。

      我能想到这样做的唯一方法是将整个表锁定在fetch_keylist([ListOfKeys])的开头或write_keylist的开头([KeyValuePairs],但我不想这样做因为我有很多进程同时进行自己的multi_key读写操作,而且我不想在任何进程需要读取/写入相对较小的记录子集时锁定整个表。

      帮助?

      试图更清楚:我不认为这只是关于使用香草交易

      我问的是一个比这更微妙的问题。想象一下,我有一个进程,在一个事务中,迭代10个记录,锁定它们。现在假设这个过程开始但在它迭代到第3个记录之前另一个过程更新了第3个记录。就事务而言,这将是正常的,因为第一个进程尚未锁定第三个记录(尚未),而OTHER进程修改它并在第一个进程到达之前将其释放。我想要的是保证,一旦我的第一个进程启动,在第一个进程完成之前,没有其他进程可以触及10条记录。

      问题已解决 - 我是个傻瓜......我猜... 谢谢你的病人,特别是Hynek -Pichi- Vychodil! 我准备了我的测试代码以显示问题,而我可能实际上重现了这个问题。然后我简化了代码的可读性,问题就消失了。我无法再次重现这个问题。这对我来说既尴尬又神秘,因为我有几天这个问题。 mnesia也从不抱怨我在事务之外执行操作而且我的代码中没有任何脏事务,我不知道我是如何将这个bug引入我的代码的!

      我已经将隔离的概念强加到我的头脑中,并且不会怀疑它是否会再次存在。

      感谢您的教育。

      实际上,事实证明问题是在事务中使用围绕mnesia操作的try / catch。有关详情,请参阅here

3 个答案:

答案 0 :(得分:2)

Mnesia交易将为您完成这件事。除非你做脏操作,否则这是什么交易。因此,只需将您的写入和读取操作放在一个事务中,mnesia就会休息。一个事务中的所有操作都是作为一个原子操作完成的。 Mnesia事务隔离级别有时被称为“可序列化”,即最强的隔离级别。

编辑:

似乎你错过了关于Erlang中并发进程的一个重要观点。 (公平地说,它不仅适用于Erlang,而且适用于任何真正并发的环境,当有人争论其他并不是真正的并发环境时。)除非进行同步,否则无法区分哪个动作首先发生,哪个动作发生。只有这样才能实现此同步是使用消息传递。你只保证Erlang中的消息,从一个进程发送到其他进程的消息的排序。这意味着当您从流程M1向流程M2发送两条消息AB时,它们会以相同的顺序发送。但是,如果您从M1A发送消息B,并从M2C发送消息B,则可以按任何顺序发送消息。只是因为你甚至可以告诉你先发送的是哪条消息?如果您从M1A发送消息B,然后从M2A发送C以及M2,则会更糟} CM3发送C B M1 B M3 fun -module(transactions). -export([start/2, sum/0, write/0]). start(W, R) -> mnesia:start(), {atomic, ok} = mnesia:create_table(test, [{ram_copies,[node()]}]), F = fun() -> ok = mnesia:write({test, a, 10}), [ ok = mnesia:write({test, X, 0}) || X <- [b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z]], ok end, {atomic, ok} = mnesia:transaction(F), F2 = fun() -> S = self(), erlang:send_after(1000, S, show), [ spawn_link(fun() -> writer(S) end) || _ <- lists:seq(1,W) ], [ spawn_link(fun() -> reader(S) end) || _ <- lists:seq(1,R) ], collect(0,0) end, spawn(F2). collect(R, W) -> receive read -> collect(R+1, W); write -> collect(R, W+1); show -> erlang:send_after(1000, self(), show), io:format("R: ~p, W: ~p~n", [R,W]), collect(R, W) end. keys() -> element(random:uniform(6), {[a,b,c],[a,c,b],[b,a,c],[b,c,a],[c,a,b],[c,b,a]}). sum() -> F = fun() -> lists:sum([X || K<-keys(), {test, _, X} <- mnesia:read(test, K)]) end, {atomic, S} = mnesia:transaction(F), S. write() -> F = fun() -> [A, B ] = L = [ random:uniform(10) || _ <- [1,2] ], [ok = mnesia:write({test, K, V}) || {K, V} <- lists:zip(keys(), [10-A-B|L])], ok end, {atomic, ok} = mnesia:transaction(F), ok. reader(P) -> case sum() of 10 -> P ! read, reader(P); _ -> io:format("ERROR!!!~n",[]), exit(error) end. writer(P) -> ok = write(), P ! write, writer(P). {{1}} {{1}} {{1}} }。即使它将在当前实现中的一个VM中发生。但是你不能依赖它,因为它不能得到保证,即使在下一版本的VM中也可以改变,因为不同调度程序之间的消息传递实现了。

它说明了并发进程中事件排序的问题。现在回到mnesia交易。 Mnesia交易必须是免费的副作用{{1}}。这意味着可能没有任何消息从交易发送到外部。因此,您无法确定哪个事务首先启动,何时启动。如果交易成功并且他们订购您只能通过其效果确定,那么您可以判断。当你考虑到这一点时,你的微妙澄清毫无意义。一个事务将读取原子操作中的所有键,即使它实现为在事务实现中逐个读取一个键,并且您的写操作也将作为原子操作执行。在第一次交易中读取第一个密钥后,您无法判断第二个交易中的第四个密钥是否发生,因为从外部无法观察到。两个事务都将按特定顺序执行,作为单独的原子操作。从外部的角度来看,所有按键都将在同一时间点读取,并且强制它的是mnesia的工作。如果您从事务内部发送消息,则违反了mnesia事务属性,您不会感到惊讶,它会表现得很奇怪。具体而言,此消息可以多次发送。

EDIT2:

  

如果您启动这些过程中的一些,您会看到它   快速a,b和c处于其值不等于10的状态。

我很好奇为什么你认为它会发生或你测试过它?告诉我你的测试用例,我会告诉我的:

{{1}}

如果它不起作用,那将是非常严重的问题。有严肃的应用程序,包括依赖它的支付系统。如果您的测试用例显示已损坏,请在erlang-bugs@erlang.org上报告错误

答案 1 :(得分:1)

您是否尝试过 mnesia Events ?您可以让读者订阅mnesia的Table Events 特别write事件,以免中断写入过程。通过这种方式,mnesia只是不断地将实时写入的内容的副本发送给另一个进程,该进程在任何时候检查值是什么。看看这个:

subscriber()->
    mnesia:subscribe({table,YOUR_TABLE_NAME,simple}),
    %% OR mnesia:subscribe({table,YOUR_TABLE_NAME,detailed}),
    wait_events().
wait_events()-> receive %% For simple events {mnesia_table_event,{write, NewRecord, ActivityId}} -> %% Analyse the written record as you wish wait_events(); %% For detailed events {mnesia_table_event,{write, YOUR_TABLE, NewRecord, [OldRecords], ActivityId}} -> %% Analyse the written record as you wish wait_events(); _Any -> wait_events() end.
现在,您将分析器生成为如下过程:

spawn(?MODULE,subscriber,[]).

这使得整个过程在没有任何进程被阻止的情况下运行,mnesia不需要锁定任何表格或记录,因为现在你拥有的是writer进程和analyser进程。整件事将实时运行。请记住,如果您希望在订阅者wait_events()接收正文中通过模式匹配它们,可以使用许多其他事件。
可以构建heavy duty gen_server或完整application,用于接收和分析您的所有mnesia事件。拥有一个有能力的用户通常比许多失败的事件用户更好。如果我理解你的问题,这个unblocking解决方案符合您的要求。

答案 2 :(得分:-1)

mnesia:带有写锁的读/ 3似乎已经足够了。

Mnesia的事务是通过读写锁实现的,并且锁是格式良好的(保持锁定直到事务结束)。因此隔离级别是可序列化的。

只要您通过主键访问,锁的粒度就是每条记录。