1

我希望在需要之前找到一些关于实现获取一堆 id 值(如数据库标识值)的最佳方法的建议。我有许多需要唯一 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()?

4

3 回答 3

2

您应该通过适当地标记该列来让您的数据库生成标识值。您可以使用SCOPE_IDENTITY或类似方法检索该值。

您的实现的主要失败是 NextId 中的繁忙等待以及从多个线程同时访问 Dictionary 。最简单的解决方案是使用像 ohadsc 下面建议的 BlockingCollection。您需要预测数据库出现故障并且无法获得更多 id 的情况——您不想让应用程序死锁。因此,您需要使用接受 ConcellationToken 的 Take() 重载,以在访问数据库失败时通知它。

于 2012-11-08T17:21:05.283 回答
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);
       }
   )
}

有几点要指出...

首先,您可能已经知道这一点,使用像ConcurrentDictionaryor之类的线程安全集合BlockingCollection而不是普通的Dictonaryor是非常重要的List。如果你不这样做,坏事就会发生,人会死,婴儿会哭。

其次,您可能需要一些比 basic 更少笨拙的东西CancellationTokenSource,这正是我在服务编程中所习惯的。关键是有一些方法可以取消这些事情,这样你就可以优雅地关闭它们。

第三,考虑将sleeps 扔进去以防止它对处理器造成太大的冲击。

这方面的细节将根据您生成这些东西的速度而不是消耗它们的速度而有所不同。如果消费者以比生产者高得多的速度运行,我的代码绝对不能保证在消费者请求之前您将拥有所需的 ID。然而,这是一种体面的,尽管是同时组织准备此类数据的基本方式。

于 2012-11-08T17:33:42.403 回答
0

您可以BlockingCollection为此使用 a 。基本上,您将有一个线程将新 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();
    ....
}
于 2012-11-08T17:48:04.873 回答