实现搜索结果的线程安全缓存

Posted

技术标签:

【中文标题】实现搜索结果的线程安全缓存【英文标题】:Implementing thread safe caching of search results 【发布时间】:2015-11-29 16:09:58 【问题描述】:

搜索结果缓存的工作原理

当用户输入query 进行搜索时:

查询被拆分成一个标记数组 为这个令牌数组创建一个唯一的散列(按字母顺序排列令牌,然后按 MD5 排序)。这是搜索的唯一标识。 根据哈希检查缓存结果 如果缓存不存在,则使用哈希将结果保存到缓存

我正在尝试解决的问题

如果用户执行的搜索需要 10 秒,并且他们不耐烦地刷新页面,我们不希望它再次开始查询。这应该被锁定。

但是,如果正在运行昂贵的查询,我们不希望阻止其他执行成本较低的搜索的用户。

要解决这个问题,我需要多个锁。

实施

这就是我目前实现它的方式:

private static readonly object MasterManualSearchLock = new object();
private static readonly Dictionary<string, object> ManualSearchLocks = new Dictionary<string, object>();

/// <summary>
/// Search the manual
/// </summary>
public static SearchResponse DoSearch(string query, Manual forManual)

    var tokens = Search.Functions.TokeniseSearchQuery(query);
    var tokenHash = Search.Functions.GetUniqueHashOfTokens(tokens);
    var cacheIndex = Settings.CachePrefix + "SavedManualSearch_" + tokenHash;
    var context = HttpContext.Current;

    if (context.Cache[cacheIndex] == null)
    
        // Create lock if it doesn't exist
        if (!ManualSearchLocks.ContainsKey(tokenHash))
        
            lock (MasterManualSearchLock)
            
                if (!ManualSearchLocks.ContainsKey(tokenHash))
                
                    ManualSearchLocks.Add(tokenHash, new object());
                
            

        

        lock (ManualSearchLocks[tokenHash])
        
            if (context.Cache[cacheIndex] == null)
            
                var searchResponse = new SearchResponse(tokens, forManual, query);
                context.Cache.Add(cacheIndex, searchResponse, null, DateTime.Now.AddMinutes(Settings.Search.SearchResultsAbsoluteTimeoutMins), Cache.NoSlidingExpiration, CacheItemPriority.BelowNormal, null);
            
            ManualSearchLocks.Remove(tokenHash);
        

    
    return (SearchResponse)context.Cache[cacheIndex];
 

问题

这是一个合理的实现吗? 这个线程安全吗? 在锁本身中包括移除锁是否可以?

【问题讨论】:

有一个小窗口lock (ManualSearchLocks[tokenHash]) 可能会失败,因为另一个线程刚刚为相同的哈希执行了ManualSearchLocks.Remove(tokenHash);(在第一个线程“保证”它在字典中之后)。 @EricJ。拆除锁是否应该由主锁锁定?另外,我是否应该为锁字典使用不同的数据类型,也许是ConcurrentDictionary 这不会改变任何事情,因为在我的场景中你已经释放了你当前的lock (MasterManualSearchLock),但是如果你将整段代码包装在同一个锁中,你将有效地序列化你的代码。 System.Runtime.Caching.MemoryCache 查看@Eser 的建议。 MemoryCache 已经处理了多线程访问缓存的问题,提供灵活的缓存失效等。 【参考方案1】:

您同时使用ManualSearchLocks 是不安全的,ConcurrentDictionary 是一个很好的替代品。不,仅仅从字典中阅读是不安全的,因为它没有被证明是安全的。

我会将Lazy&lt;T&gt; 放入缓存中。可能会产生多个这样的惰性实例,但只会实现一个。所有想要访问特定键的线程都将调用Lazy.Value 并自动同步。一旦执行一次实际的“搜索”。

根据您访问缓存的方式,可能存在允许执行多个惰性实例的小型竞争条件。在您的情况下,这可能没什么大不了的。

【讨论】:

以上是关于实现搜索结果的线程安全缓存的主要内容,如果未能解决你的问题,请参考以下文章

Java内存模型和线程安全

线程结果缓存实现(future与concurrenthashmap)

并发编程中经常用到的ConcurrentHashMap

java中的线程安全随机访问循环数组?

9月11号面试总结(guangfa)

如何实现线程安全的 LRU 缓存驱逐?