在同一个数据库上工作的多个工作线程 - 如何使其正常工作?

时间:2012-04-14 17:29:08

标签: ruby-on-rails ruby sinatra datamapper

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

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

我在Ruby中使用DataMapper,但实际上我认为这是一个通用编程问题,并不是我所使用的具体实现所特有的......

我正在创建一堆工作线程,它们执行类似这样的操作(伪红宝石代码):

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循环中的第一行可以很容易地在多个线程中拉出相同的行,然后才有机会被锁定。

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

这样做的最佳方式是什么?

2 个答案:

答案 0 :(得分:5)

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

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

悲观锁定

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

这样的东西
SELECT * from some_table WHERE ... FOR UPDATE

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

乐观锁定

乐观锁定指的是一种技术,你不需要悲观锁定的负担,因为大多数时候不会有并发更新(如果你更新行设置锁定标志为真,只要你有读行,窗口比较小)。 AFAIK只适用于一次更新一行

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

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

并检查更新的行数(db驱动程序返回此值)。如果这更新1行,那么你知道一切都好。如果此更新为0行,则删除所需的行或其锁定版本已更改,因此您将返回到流程中的第1步并搜索要处理的新行。

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

答案 1 :(得分: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中。