我希望在我需要之前找到一些关于获取一堆id值(比如数据库Identity值)的最佳方法的建议。我有许多类需要一个唯一的id(int),我想要做的是获取下一个可用的id(每个类,每个服务器)并让它在本地缓存。当一个id被带走时,我想让下一个准备等等。
我已经制作了一些代码来演示我想要做的事情。代码很糟糕(它应该包含锁等),但我认为它得到了重点。丢失奇数ID不是问题 - 重复的ID是(问题)。我很满意GetNextIdAsync的胆量 - 它调用了proc
this.Database.SqlQuery<int>("EXEC EntityNextIdentityValue @Key",
new SqlParameter("Key", key))).First();
在SQL Server上使用sp_getapplock确保每个返回值都是唯一的(和增量的)。
static class ClassId
{
static private Dictionary<string, int> _ids = new Dictionary<string,int>();
static private Dictionary<string, Thread> _threads = new Dictionary<string,Thread>();
static ClassId()
{
//get the first NextId for all known classes
StartGetNextId("Class1");
StartGetNextId("Class2");
StartGetNextId("Class3");
}
static public int NextId(string key)
{
//wait for a current call for nextId to finish
while (_threads.ContainsKey(key)) { }
//get the current nextId
int nextId = _ids[key];
//start the call for the next nextId
StartGetNextId(key);
//return the current nextId
return nextId;
}
static private void StartGetNextId(string key)
{
_threads.Add(key, new Thread(() => GetNextIdAsync(key)));
_threads[key].Start();
}
static private void GetNextIdAsync(string key)
{
//call the long running task to get the next available value
Thread.Sleep(1000);
if (_ids.ContainsKey(key)) _ids[key] += 1;
else _ids.Add(key, 1);
_threads.Remove(key);
}
}
我的问题是 - 在我需要它之前总是拥有我需要的下一个值的最佳方法是什么?如何安排课程以及锁定在哪里?例如。锁定GetNextIdAsync()添加新线程但不启动它并更改StartGetNextId()以调用.Start()?
答案 0 :(得分:2)
您应该让数据库通过适当地标记该列来生成标识值。您可以使用SCOPE_IDENTITY或类似内容检索该值。
您实现的主要缺点是NextId中的忙等待以及从多个线程同时访问Dictionary。最简单的解决方案是使用像下面的ohadsc建议的BlockingCollection。您需要预测数据库出现故障并且无法获得更多ID的情况 - 您不希望使应用程序死锁。因此,您可能希望使用接受ConcellationToken的Take()重载,如果访问数据库失败,您将通知它。
答案 1 :(得分:0)
这似乎是生产者 - 消费者模式的一个很好的应用。
我在想:
private ConcurrentDictionary<string, int> _ids;
private ConcurrentDictionary<string, Thread> _threads;
private Task _producer;
private Task _consumer;
private CancellationTokenSource _cancellation;
private void StartProducer()
{
_producer = Task.Factory.StartNew(() =>
while (_cancellation.Token.IsCancellationRequested == false)
{
_ids.Add(GetNextKeyValuePair());
}
)
}
private void StartConsumer()
{
_consumer = Task.Factory.StartNew(() =>
while (_cancellation.Token.IsCancellationRequested == false)
{
UseNextId(id);
_ids.Remove(id);
}
)
}
要指出的一些事情......
首先,您可能已经知道这一点,使用ConcurrentDictionary
或BlockingCollection
等线程安全集合代替普通Dictonary
或List
非常重要。如果你不这样做,会发生坏事,人们会死,而且婴儿会哭。
其次,你可能需要一些比基本CancellationTokenSource
少得多的东西,这就是我在服务编程中习惯的东西。关键是要有一些方法来取消这些东西,这样你就可以优雅地关闭它们。
第三,考虑在那里投掷sleep
以防止它过于猛烈地冲击处理器。
具体细节取决于您生成这些东西的速度,而不是消耗它们的速度。如果消费者以比生产者高得多的速度运行,我的代码绝对不能保证在消费者要求之前你会拥有你想要的ID。然而,这是一种体面的,虽然是同时组织准备这类数据的基本方法。
答案 2 :(得分:0)
您可以使用BlockingCollection
。基本上你会有一个线程将新的ID抽入缓冲区:
BlockingCollection<int> _queue = new BlockingCollection<int>(BufferSize);
void Init()
{
Task.Factory.StartNew(PopulateIdBuffer, TaskCreationOptions.LongRunning);
}
void PopulateIdBuffer()
{
int id = 0;
while (true)
{
Thread.Sleep(1000); //Simulate long retrieval
_queue.Add(id++);
}
}
void SomeMethodThatNeedsId()
{
var nextId = _queue.Take();
....
}