It seems you want to avoid calling to the data source unnecessarily. Then, if many threads request the same thing at the same time, you want to allow one to query the data source and cache the data and hold the others until the data is populated.
You may want to have one decorator, to make sure that only one query type is performed at the same time. You can represent the query with an object that is in a thread safe collection, and lock on it during the query execution.
So given an interface expressing the way you query your data source:
interface IQueryExecuter<TQuery, TResult>
{
TResult Execute(TQuery query);
}
You can use a thread safe decorator object that caches queries' results and, in case the query result is not cached, only one thread performs the query to the data source:
Untested code!
class QueryThrottler<TQuery, TResult> : IQueryExecuter<TQuery, TResult>
{
// do not lock on external objects
class QueryObject
{
public TQuery Query { get; set; }
}
readonly IQueryExecuter<TQuery, TResult> _inner;
readonly ConcurrentDictionary<TQuery, QueryObject> _queries;
public QueryThrottler(IQueryExecuter<TQuery, TResult> inner)
{
_queries = new ConcurrentDictionary<TQuery, QueryObject>();
_inner = inner;
}
public TResult Execute(TQuery query)
{
// if it is on cache return the result
TResult result;
if (!IsCached(query, out result))
{
// otherwise lock other threads
// on the same query
var queryObject = _queries.GetOrAdd(query, k => new QueryObject() { Query = k });
lock (queryObject)
{
// double check it is not cached already
if (!IsCached(query, out result))
{
result = _inner.Execute(queryObject.Query);
PopulateCache(query, result);
}
}
}
return result;
}
private void PopulateCache(TQuery query, TResult result)
{
// Save the result in Redis using TQuery as key
}
private bool IsCached(TQuery query, out TResult result)
{
// go to redis and check if the query is cached using TQuery as key
// if exists, set the result out parameter and return true
// otherwise, return false
result = default(TResult);
return false;
}
}
This code relies on TQuery having proper implementations of GetHashCode
and Equals
.
The decorated object (inner
in the constructor) is the object that would do the actual query to the data source.
If you have many servers, and want to make sure only one thread from all servers do the actual query to the data source, rather than lock
, you can use a distributed lock like LockTake/LockRelease from StackExchange.Redis.