异步/等待和缓存

Pau*_*aul 15 c# caching async-await

我的服务层正在缓存很多对memcached的Db请求,这是否使得无法使用Async/Await?例如,我怎么能等待这个?

public virtual Store GetStoreByUsername(string username)
{
        return _cacheManager.Get(string.Format("Cache_Key_{0}", username), () =>
        {
                return _storeRepository.GetSingle(x => x.UserName == username);
        });
}
Run Code Online (Sandbox Code Playgroud)

注意:如果密钥存在于缓存中,它将返回"Store"(而不是a Task<Store>),如果缓存中不存在该密钥,则它将执行lambda.如果我将Func更改为

return await _storeRepository.GetSingleAsync(x => x.UserName == username);
Run Code Online (Sandbox Code Playgroud)

并且方法签名为

public virtual async Task<Store> GetStoreByUsername(string username)
Run Code Online (Sandbox Code Playgroud)

由于缓存返回类型,这显然不起作用.

Bra*_*don 18

这是一种缓存异步操作结果的方法,可以保证没有缓存未命中并且是线程安全的.

在接受的答案中,如果在循环或多个线程中多次请求相同的用户名,则DB请求将一直发送,直到有缓存的响应,此时缓存将开始被使用.

下面的方法SemaphoreSlim为每个唯一键创建一个对象.这将防止长时间运行的async操作多次运行同一个键,同时允许它同时针对不同的键运行.显然,有保留SemaphoreSlim对象的开销以防止缓存未命中,因此根据用例可能不值得.但是,如果保证没有缓存未命中很重要,那么这就完成了.

private readonly ConcurrentDictionary<string, SemaphoreSlim> _keyLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
private readonly ConcurrentDictionary<string, Store> _cache = new ConcurrentDictionary<string, Store>();

public async Task<Store> GetStoreByUsernameAsync(string username)
{   
    Store value;
    // get the semaphore specific to this username
    var keyLock = _keyLocks.GetOrAdd(username, x => new SemaphoreSlim(1));
    await keyLock.WaitAsync().ConfigureAwait(false);
    try
    {
        // try to get Store from cache
        if (!_cache.TryGetValue(username, out value))
        {
            // if value isn't cached, get it from the DB asynchronously
            value = await _storeRepository.GetSingleAsync(x => x.UserName == username).ConfigureAwait(false);

            // cache value
            _cache.TryAdd(username, value);
        }
    }
    finally
    {
        keyLock.Release();
    }
    return value;
}
Run Code Online (Sandbox Code Playgroud)

注意:要进一步优化此方法,可以在锁定获取步骤之前执行其他高速缓存检查.

  • 我使用“SemaphoreSlim”,因为它提供了异步等待方法。并且您无法在锁定语句体内“await”。 (2认同)

Mar*_*ell 7

它看起来像缓存管理器执行所有"检查它是否存在,如果不运行lambda然后存储".如果是这样,唯一的方法async是使用一个GetAsync返回a Task<Store>而不是a的方法Store,即

public virtual Task<Store> GetStoreByUsernameAsync(string username)
{
    return _cacheManager.GetAsync(string.Format("Cache_Key_{0}", username), () =>
    {
        return _storeRepository.GetSingleAsync(x => x.UserName == username);
    });
}
Run Code Online (Sandbox Code Playgroud)

请注意,这不需要标记,async因为我们没有使用await.然后缓存管理器将执行以下操作:

public async Task<Store> GetAsync(string key, Func<Task<Store>> func)
{
    var val = cache.Get(key);
    if(val == null)
    {
        val = await func().ConfigureAwait(false);
        cache.Set(key, val);
    }
    return val;
}
Run Code Online (Sandbox Code Playgroud)

  • 此外,此方法不能保证同一用户名不会出现缓存丢失。与上面相同的示例 - 使用相同的用户名循环。在返回并缓存第一个响应之前,可以多次发出数据库请求,此时缓存将开始被使用。请参阅我的答案以获取可能的解决方案。 (2认同)