4

我有一个数据库,其中包含需要操作的行列表。它看起来像这样:

id       remaining        delivered   locked
============================================
1        10               24          f 
2        6                0           f
3        0                14          f

我正在将 DataMapper 与 Ruby 一起使用,但实际上我认为这是一个一般性的编程问题,并不特定于我正在使用的确切实现......

我正在创建一堆执行类似操作的工作线程(伪 ruby​​ 代码):

while true do
  t = any_row_in_database_where_remaining_greater_than_zero_and_unlocked
  t.lock   # update database to set locked = true
  t.do_some_stuff
  t.delivered += 1
  t.remaining -= 1
  t.unlock
end

当然,问题是,这些线程相互竞争,整个事情并不是真正的线程安全。while 循环中的第一行可以轻松地在多个线程中拉出同一行,然后才有机会被锁定。

我需要确保一个线程同时只在一行上工作。

做这个的最好方式是什么?

4

2 回答 2

6

关键步骤是从数据库中选择一个未锁定的行并将其标记为锁定。如果你能安全地做到这一点,那么其他一切都会好起来的。

我所知道的 2 种可以保证安全的方法是悲观锁定和乐观锁定。在并发方面,它们都依赖您的数据库作为最终保证。

悲观锁定

悲观锁定意味着在您选择要使用的行时预先获取锁定,以便其他人无法读取它们。就像是

SELECT * from some_table WHERE ... FOR UPDATE

与 mysql 和 postgres(可能还有其他)一起使用,并将阻止与数据库的任何其他连接读取返回给您的行(该锁的粒度取决于所使用的引擎、索引等 - 检查您的数据库的文档)。它被称为悲观,因为您假设将发生并发问题并预防性地获取锁。这确实意味着即使在不必要的情况下您也要承担锁定的成本,并且可能会根据您拥有的锁定粒度降低并发性。

乐观锁定

乐观锁定是指您不希望悲观锁定负担的技术,因为大多数时候不会有并发更新(如果您在读取行后立即更新行,将锁定标志设置为 true ,窗口比较小)。AFAIK这仅在一次更新一行时有效

首先向表中添加一个整数列lock_version。每当您更新表时,lock_version与您正在进行的其他更新一起增加 1。假设当前lock_version为3。更新时,将更新查询更改为

update some_table set ... where id=12345 and lock_version = 3

并检查更新的行数(数据库驱动程序返回这个)。如果这更新了 1 行,那么您就知道一切正常。如果这更新了 0 行,则您想要的行已被删除或其锁定版本已更改,因此您返回流程中的第 1 步并搜索要处理的新行。

我不是数据映射器用户,所以我不知道它/它的插件是否为这些方法提供支持。Active Record 支持两者,因此如果数据映射器不支持,您可以在那里寻找灵感。

于 2012-04-14T18:10:43.070 回答
1

我会使用一个Mutex

# outside your threads
worker_updater = Mutex.new

# inside each thread's updater
while true
  worker_updater.synchronize do
    # your code here
  end
  sleep 0.1 # Slow down there, mister!
end

这保证了一次只有一个线程可以输入synchronize. 为了获得最佳性能,请考虑代码的哪一部分需要是线程安全的(前两行?),并且只将那部分包装在 Mutex 中。

于 2012-04-15T14:10:26.873 回答