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

Posted

技术标签:

【中文标题】Mnesia:如何同时锁定多行,以便我可以写入/读取一组“一致”的记录【英文标题】:Mnesia: How to lock multiple rows simultaneously so that I can write/read a "consistent" set of of records 【发布时间】:2011-12-18 01:51:31 【问题描述】:

我希望我的问题是如何开始的

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

    在一个事务中,为 abc 写入随机值,使这些值总是总和为 10 在另一个事务中,读取 abc 的值,如果它们的值之和不等于 10,则抱怨

如果您启动其中的几个进程,您会很快看到 abc 处于一种状态它们的值总和不等于 10。我相信没有办法要求 mnesia“在开始写入(或读取)之前锁定这 3 条记录”,只能让 mnesia 将记录锁定为它到达了它们(可以这么说),这允许记录集的值违反我的“必须总和为 10”约束。

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

    在写入(或读取)这组 3 条记录之前锁定整个表 -- 我讨厌为 3 条记录锁定整个表, 创建一个进程来跟踪谁在读取或写入哪些密钥,并保护批量操作不被其他任何人写入或读取,直到操作完成。当然,我必须确保所有进程都使用这个......废话,我想这意味着编写我自己的 AccessMod 作为 activity/4 的第四个参数,这似乎是一个不平凡的练习 我还不够聪明,无法弄清楚的其他一些事情。

想法?

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

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

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

帮助?

想更清楚一点:我不认为这只是使用普通交易

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

问题已解决 - 我是个白痴...我猜... 感谢所有患者,尤其是 Hynek -Pichi- Vychodil! 我准备了我的测试代码来显示问题,我可以事实上重现了这个问题。然后我简化了代码以提高可读性,问题就消失了。我无法再次重现该问题。这对我来说既尴尬又神秘,因为我有这个问题好几天了。此外,mnesia 从未抱怨我在事务之外执行操作,并且我的代码中没有任何脏事务,我不知道我是如何将这个错误引入我的代码中的!

我已经将隔离的概念牢牢地印在了我的脑海中,并且不会怀疑它再次存在。

感谢您的教育。

实际上,问题在于在事务 中使用try/catch 来处理mnesia 操作。请参阅here 了解更多信息。

【问题讨论】:

一个 mnesia 事务应该这样做。有什么不适合你的理由吗? 因为如果我有一个包含 10 个要迭代的键的列表,事务系统可以在我迭代时锁定第一条记录,然后锁定第二条,但有人可以更改第 10 个键在此代码到达之前。从本质上讲,我需要一种方法来锁定组中我试图以原子方式更改的所有记录。 【参考方案1】:

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

编辑:

您似乎错过了有关 Erlang 中并发进程的重要一点。 (公平地说,这不仅在 Erlang 中如此,而且在任何真正的并发环境中都是如此,当有人争辩说它不是真正的并发环境时。)除非您进行一些同步,否则您无法区分哪个动作首先发生,哪个动作随后发生。进行这种同步的唯一方法是使用消息传递。你只保证了 Erlang 中消息的一件事,即从一个进程发送到另一个进程的消息的排序。这意味着当您从进程A 发送两条消息M1M2 到进程B 时,它们以相同的顺序到达。但是,如果您将消息M1A 发送到B,并将消息M2C 发送到B,它们可以按任何顺序到达。仅仅因为你怎么知道你先发送了哪条消息?如果您将消息M1A 发送到B,然后将M2A 发送到C,并且当M2 到达C 时从M3 发送M3,则更糟。到B 你没有保证M1M3 之前到达B。即使它会在当前实现中发生在一个 VM 中。但是你不能依赖它,因为它不能保证,即使在下一版本的 VM 中也可以改变,只是由于不同调度程序之间的消息传递实现。

它说明了并发进程中的事件排序问题。现在回到 mnesia 事务。 Mnesia 交易必须是无副作用的fun。这意味着可能没有任何消息从事务外部发送。所以你无法分辨哪个事务先开始,什么时候开始。只有您可以判断交易是否成功并且他们命令您只能通过其效果来确定。当您考虑到这一点时,您的微妙澄清毫无意义。一个事务将在原子操作中读取所有键,即使它在事务实现中实现为一个键一个读,并且您的写操作也将作为原子操作执行。在您读取第一个事务中的第一个键之后,您无法判断是否写入了第二个事务中的第 4 个键,因为从外部无法观察到它。两个事务都将作为单独的原子操作以特定顺序执行。从外部的角度来看,所有的键都将在同一时间点被读取,这是 mnesia 的工作来强制它。如果您从交易内部发送消息,则违反了 mnesia 交易属性,您不会感到惊讶,它的行为会很奇怪。具体来说,这条消息可以发送多次。

编辑2:

如果您启动其中的几个流程,您会发现非常 很快 a、b 和 c 就处于它们的值总和不等于 10 的状态。

我很好奇你为什么认为它会发生或者你测试了它?给我看你的测试用例,我就给我看:

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

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

【讨论】:

我想我问的是一个比这更微妙的问题。我可能很密集,但请参阅上面我修改后的描述。 感谢您的回复。是的,我明白这一点。但是,我可以在更新记录集的过程开始时锁定表。这行得通,但这样做是“昂贵的”。一些数据库有一个“带锁选择”命令,可以满足我的要求。我想我可以在我的记录中添加一个所有者字段并编写一个函数,该函数遍历集合中的每条记录并尝试用唯一的 ref 标记它,如果它到达某个其他进程拥有的记录并不断地中止事务重试此操作,直到它可以在更新之前将它们全部标记。我认为这会奏效, 看看我对 shino 的回答。一旦您在事务中读取记录,该记录就会被读锁锁定。然后任何人都可以阅读此记录,但没有人可以更改。 mnesia事务中的所有select操作都是带锁的,除了脏操作。 可序列化级别还是可重复读取级别? mneisa 有范围锁定吗? en.wikipedia.org/wiki/Isolation_(database_systems)#Serializable 根据erlang.org/doc/man/mnesia.html#index_read-3,似乎Mnesia在执行range search时会持有表锁。所以我认为它是Searializable Level。【参考方案2】:

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

订户()-> mnesia:subscribe(table,YOUR_TABLE_NAME,simple), %% OR mnesia:subscribe(table,YOUR_TABLE_NAME,detailed), wait_events(). 等待事件()-> 收到 %% 用于简单事件 mnesia_table_event,write, NewRecord, ActivityId -> %% 随心所欲分析书面记录 等待事件(); %% 详细事件 mnesia_table_event,write, YOUR_TABLE, NewRecord, [OldRecords], ActivityId -> %% 随心所欲分析书面记录 等待事件(); _Any -> wait_events() 结尾。 现在您将分析器生成为如下过程: 产卵(?模块,订阅者,[])。

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

【讨论】:

【参考方案3】:

mnesia:read/3 with write locks 似乎就足够了。

Mnesia 的事务是通过读写锁实现的,并且锁的格式是良构的(持有锁直到事务结束)。所以隔离级别是可序列化的。

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

【讨论】:

这里获取写锁没有任何意义。一旦你读了一些记录,你就获得了这条记录的读锁。当您保持读锁时,没有人可以写入此记录,这足以防止任何数据更改。除非你愿意写这条记录,否则获取写锁纯属浪费资源。 感谢指出我的错误。我考虑写偏斜(c.f. wiki.postgresql.org/wiki/SSI#Simple_Write_Skew),它会影响一些不变性,例如一些记录的总和。但这与读写锁事务实现无关。

以上是关于Mnesia:如何同时锁定多行,以便我可以写入/读取一组“一致”的记录的主要内容,如果未能解决你的问题,请参考以下文章

如何将 mnesia 节点添加到现有集群

在 Elixir/Erlang 中的(本地)Mnesia 实例上实现最佳写入性能

Erlang mnesia 数据库访问

具有同时读/写功能的C#字典

如何锁定 Access 数据库以防止写入

为什么在写锁定挂起时可以获取读锁?