暂停同时进行的 REST 调用直到第一个调用完成

5

我们有一个类似于以下REST API方法:

List<item> GetItems(int AccountID)
{
    var x = getFromCache(AccountID);
    if(x==null)
    {
        x = getFromDatabase(AccountID);
        addToCache(AccountID, x);
    }
    return x;
}

这是一种成本相对较高的方法,需要进行一些复杂的数据库调用。我们有一个常见的情况,即数百个具有相同AccountId的用户几乎同时发出调用请求(它们都被广播通知)。
在该方法中,我们将结果集缓存10秒,因为在该时间窗口内发出请求的所有人都可以获得近实时的结果。然而,由于他们都同时发出调用请求(再次针对特定的AccountId),所以缓存从未提前填充,因此每个人最终都会进行数据库调用。
那么我的问题是,在该方法内,如何暂停所有传入的请求,使它们都等待第一个结果集完成,以便其余的调用可以使用缓存的结果集?
我已经了解了一些关于Monitor.Pulse和Monitor.Lock的内容,但是针对每个accountId锁的实现似乎有些困难。任何帮助都将不胜感激。

许多用户使用相同的AccountId是否有原因,这是一个服务账户ID吗?如果您正在使用Sql Server,您可以修改存储过程以使用事务或在数据库端添加一些With No Lock命令吗? - MethodMan
我会考虑使用 2 级缓存,因此如果您有第二个缓存用于“挂起”,则最后 10 到 20 秒钟内处于挂起缓存中的任何调用都将自动阻塞并等待一段时间再尝试真正的数据库调用。同样,出于类似原因,您可能希望缓存“已被证明不存在”的信息。我会避免根据变量的值来监视和锁定。无论变量的值如何,我都会为代码和内存保留这些锁和同步器。 - Sql Surfer
2个回答

2

对于具有相同AccountId但使用不同对象的请求,您必须锁定相同的对象,但对于每个单独的AccountId使用不同的对象。以下是如何使用字典来跟踪各个AccountId的锁定对象的示例。

    Dictionary<int, Object> Locks = new Dictionary<int, object>();

    List<item> GetItems(int AccountID)
    {
        //Get different lock object for each AccountId
        Object LockForAccountId = GetLockObject(AccountID);

        //block subsequent threads until first thread fills the cache
        lock (LockForAccountId)
        {
            var x = getFromCache(AccountID);
            if (x == null)
            {
                x = getFromDatabase(AccountID);
                addToCache(AccountID, x);
            }
            return x;
        }
    }

    private Object GetLockObject(int AccountID)
    {
        Object LockForAccountId;

        //we must use global lock while accessing dictionary with locks to prevent multiple different lock objects to be created for the same AccountId
        lock (Locks)
        {
            if (!Locks.TryGetValue(AccountID, out LockForAccountId))
            {
                LockForAccountId = new Object();
                Locks[AccountID] = LockForAccountId;
            }
        }
        return LockForAccountId;
    }

0
你有考虑过在这里使用 Lazy<T> 吗?
试试这段代码:
private object _gate = new object();
List<item> GetItems(int AccountID)
{
    lock (_gate)
    {
        var x = getFromCache(AccountID);
        if (x == null)
        {
            x = new Lazy<List<item>>(() => getFromDatabase(AccountID));
            addToCache(AccountID, x);
        }
        return x.Value;
    }
}

您需要更改getFromCacheaddToCache的签名如下:

Lazy<List<item>> getFromCache(int AccountID)
void addToCache(int AccountID, Lazy<List<item>> x)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接