Estoy usando ConcurrentDictionary para almacenar datos en caché con acceso paralelo y, a veces, los elementos nuevos se pueden almacenar en db y no se cargan en caché. Esta es la razón por la que uso GetOrAdd
public User GetUser(int userId) { return _user.GetOrAdd(userId, GetUserFromDb); } private User GetUserFromDb(int userId) { var user = _unitOfWork.UserRepository.GetById(userId); // if user is null, it is stored to dictionary return user; }Pero, ¿cómo puedo verificar si el usuario se obtuvo de db y almacenó el usuario en el diccionario solo si el usuario no es nulo?
Posiblemente pueda eliminar nulo de ConcurrentDictionary inmediatamente después de GetOrAdd, pero no parece seguro para subprocesos y no es una solución muy elegante. Insertar y quitar inútil del diccionario. ¿Tienes alguna idea de cómo hacerlo?
Aquí hay una solución hacky, espero que algo mejor sea posible. Haga GetUserFromDb lance si no se encuentra el usuario. Esto aborta el almacenamiento en el diccionario. Haga GetUser la excepción. Esto está usando excepciones para controlar el flujo, lo cual no es agradable.
public User GetUser(int userId) { var user = _user.GetOrAdd(userId, GetUserFromDb); if (user == null) _user.TryRemove(userId, out user); }También puede envolver eso en un método de extensión:
public static TValue GetOrAddIfNotNull<TKey, TValue>( this ConcurrentDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> valueFactory) where TValue : class { var value = dictionary.GetOrAdd(key, valueFactory); if (value == null) dictionary.TryRemove(key, out value); return value; }Entonces su código se verá así:
public User GetUser(int userId) { var user = _user.GetOrAddIfNotNull(userId, GetUserFromDb) }ACTUALIZAR
Según el comentario de @usr, puede haber un caso en el que:
GetOrAdd , agrega null al diccionario y hace una pausa.GetOrAdd y recupera valores null del diccionario en lugar de acceder a la base de datos.TryRemove y eliminan el registro del diccionario. Con este tiempo, el subproceso 2 se volverá null en lugar de acceder a la base de datos y obtener el registro del usuario. Si este caso extremo es importante para usted y aún desea usar ConcurrentDictionary , entonces puede usar lock en el método de extensión:
public static class ConcurrentDictionaryExtensions { private static readonly object myLock = new object(); public static TValue GetOrAddIfNotNull<TKey, TValue>( this ConcurrentDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> valueFactory) where TValue : class { lock (myLock) { var value = dictionary.GetOrAdd(key, valueFactory); if (value == null) dictionary.TryRemove(key, out value); return value; } } }Estoy ampliando la solución @NikolaiSamteladze para incluir el bloqueo verificado dos veces para que otros subprocesos puedan omitir la adquisición del bloqueo después de la actualización del diccionario.
public static class ConcurrentDictionaryExtensions { private static readonly object myLock = new object(); public static TValue GetOrAddIfNotNull<TKey, TValue>( this ConcurrentDictionary<TKey, TValue> dictionary, TKey key, Func<TKey, TValue> valueFactory) where TValue : class { TValue value; if (!dictionary.TryGetValue(key, out value)) { lock (myLock) { value = dictionary.GetOrAdd(key, valueFactory); if (value == null) dictionary.TryRemove(key, out value); } } return value; } }