对于多个进程1 使用lock statement不足。即使named/system semaphores 也仅限于单机,因此无法从多台服务器中获取。
如果重复处理没问题并且可以选择“获胜者”,则只需重写/更新或使用optimistic concurrency 的风格就足够了。如果需要维护更强的 process-once 并发保证,则需要采用全局锁定机制 - SQL Server 通过sp_getapplock 支持这种机制。
同样,可以更新模型,以便每个代理“请求”下一个工作单元,以便可以集中控制调度,并且基于 ID 等的实体一次只提供给单个代理用于处理。另一种选择可能是使用消息系统,如RabbitMQ(或 Kafka 等,fsvo);对于 RabbitMQ,甚至可以使用Consistent Hashing 来确保(在大多数情况下)不同的消费者接收到不重叠的消息。具体细节因使用的实现而异。
由于 SQL RDBMS 和 MongoDB 的不同性质(尤其是用作“缓存”时),放宽限制和/或使用 MongoDB 作为读取来设计问题可能就足够了(这是一个很好的使用缓存的方法)。这可以缓解配对写入问题,尽管它不会阻止对相同项目的全局并发处理。
1即使锁语句全局不足,仍然可以在本地单个进程中的线程之间使用,以减少本地争用和/或最小化全局锁定。
下面的答案是针对原始问题的,假设单个进程。
避免通过多个线程同时处理同一对象的“标准”方法是在特定对象上使用lock statement。在对象本身上获取锁,这样lock(X) 和lock(Y) 在!ReferenceEquals(X,Y) 时是独立的。
lock 语句获取给定对象的互斥锁,执行语句块,然后释放锁。 持有锁时,持有锁的线程可以再次获取并释放锁。 任何其他线程都被阻止获取锁并等待直到锁被释放。
lock (objectBeingSaved) {
// This code execution is mutually-exclusive over a specific object..
// ..and independent (non-blocking) over different objects.
Process(objectBeingSaved);
}
本地进程锁不一定转化为对数据库访问的充分保证,或者当访问溢出进程时。还应考虑锁的范围:例如。它应该涵盖所有处理、仅保存还是其他一些工作单元?
为了控制哪些对象被锁定并减少不希望的/意外锁定交互的机会,有时建议显式(并且仅用于)为建立锁定的目的向对象添加最具体的可见性字段。如果需要考虑的话,这也可以用于对应该相互锁定的对象进行分组。
也可以使用锁定池,尽管这往往是一个更“高级”的用例,只有特定的适用性。使用池还允许使用信号量(在更具体的用例中)以及简单的锁。
如果每个外部 ID 需要一个锁,一种方法是将正在处理的实体与池集成,在实体之间建立锁:
// Some lock pool. Variations of the strategy:
// - Weak-value hash table
// - Explicit acquire/release lock
// - Explicit acquire/release from ctor and finalizer (or Dispose)
var locks = CreateLockPool();
// When object is created, assign a lock object
var entity = CreateEntity();
// Returns same lock object (instance) for the given ID, and a different
// lock object (instance) for a different ID.
etity.Lock = GetLock(locks, entity.ID);
lock (entity.Lock) {
// Mutually exclusive per whatever rules are to select the lock
Process(entity);
}
另一个变体是本地化池,而不是每个实体本身都携带一个锁对象。它在概念上与上面的模型相同,只是从外向内翻转。这是一个要点。 YMMV。
private sealed class Locker { public int Count; }
IDictionary<int, Locker> _locks = new Dictionary<int, Locker>();
void WithLockOnId(int id, Action action) {
Locker locker;
lock (_locks) {
// The _locks might have lots of contention; the work
// done inside is expected to be FAST in comparison to action().
if (!_locks.TryGetValue(id, out locker)
locker = _locks[id] = new Locker();
++locker.Count;
}
lock (locker) {
// Runs mutually-exclusive by ID, as established per creation of
// distinct lock objects.
action();
}
lock (_locks) {
// Don't forget to take out the garbage..
// This would be better with try/finally, which is left as an exercise
// to the reader, along with fixing any other minor errors.
if (--_locks[id].Count == 0)
_locks.Remove(id);
}
}
// And then..
WithLockOnId(x.ID, () => Process(x));
另一种方法是跨线程/处理单元“分片”实体。因此,保证每个线程永远不会与另一个线程处理相同的实体:X、Y、Z 总是转到#1,P、D、Q 总是转到#2。 (优化吞吐量有点复杂..)
var threadIndex = entity.ID % NumThreads;
QueueWorkOnThread(threadIndex, entity); // eg. add to List<ConcurrentQueue>