我有一个数据库,其中包含需要操作的行列表。它看起来像这样:
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循环中的第一行可以很容易地在多个线程中拉出相同的行,然后才有机会被锁定。
我需要确保一个线程同时只在一行上工作。
这样做的最佳方式是什么?
答案 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中。