【问题标题】:Async/Await and Caching异步/等待和缓存
【发布时间】:2014-05-14 19:43:32
【问题描述】:

我的服务层正在缓存大量 Db 请求到 memcached,这是否导致无法使用 Async/Await?例如,我怎么能等待这个?

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

注意:如果缓存中存在键,它将返回一个“Store”(不是Task<Store>),如果缓存中不存在键,它将执行 lambda。如果我将 Func 更改为

return await _storeRepository.GetSingleAsync(x => x.UserName == username);

以及方法签名到

public virtual async Task<Store> GetStoreByUsername(string username)

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

【问题讨论】:

    标签: c# caching async-await


    【解决方案1】:

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

    在接受的答案中,如果在一个循环中或从多个线程多次请求相同的用户名,则 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;
    }
    

    注意:为了进一步优化此方法,可以在获取锁之前执行额外的缓存检查。

    【讨论】:

    • 有什么特别的原因,为什么你不使用ConcurrentDictionary&lt;string, object&gt;lock(obj) {...} 而不是SemaphoreSlim?我做了一些基准测试,locks 在我的情况下似乎更快
    • 我使用了SemaphoreSlim,因为它提供了异步等待方法。而且您无法在 lock 语句的主体内使用await
    • 但请注意,它会占用内存,因为键和信号量都不会被 GC'ed,_keyLocks 将保留引用
    • 是的,要清除缓存,您必须同时清除 _keyLocks_cache
    【解决方案2】:

    看起来缓存管理器完成了所有“检查它是否存在,如果不运行 lambda 然后存储”。如果是这样,创建async 的唯一方法是拥有一个返回Task&lt;Store&gt; 而不是StoreGetAsync 方法,即

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

    请注意,这不需要标记为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;
    }
    

    【讨论】:

    • 吹毛求疵:return val.Result。另外,Task.FromResult 可能是多余的:var val = func(); await val.ConfigureAwait(false); cache.Set(key, val);
    • @Noseratio 是的,有意的编辑,它编译得很好
    • @Ryan 确实不行;你有一个你想要的例子吗?
    • 值得注意的是,GetAsync 中的缓存对象应该是线程安全的。例如,如果在循环中使用相同的用户名调用 GetStoreByUsernameAsync,则将值添加到缓存的延续和调用 cache.get() 的原始线程都可能在不同的线程上运行。
    • 另外,这个方法不能保证同一个用户名没有缓存未命中。与上面相同的示例 - 使用相同的用户名循环。在返回和缓存第一个响应之前,可以多次发出 DB 请求,此时缓存将开始被使用。请参阅我的答案以获得可能的解决方案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-06-15
    • 2017-04-17
    • 2018-11-29
    • 2020-12-05
    • 1970-01-01
    • 1970-01-01
    • 2023-03-12
    相关资源
    最近更新 更多