ABP - 缓存模块(2)

1. 缓存模块源码解析

个人觉得 ABP 分布式缓存模块有三个值得关注的核心点。首先是 AbpRedisCache 类继承了微软原生的 RedisCache,并 通过反射的方式获取RedisCache的私有方法对 RedisCache 进行扩展,实现了 ABP 分布式缓存中的批量操作方法。

image

为什么要这么做呢?因为基于 Redis 缓存的批量操作需要使用到 StackExchange.Redis 原生的SDK,而且在进行操作前总需要先进行 Redis 连接,而相应的方法和属性原生的 RedisCache 中都有,但是都是私有(private)的,在继承类中也无法使用,所以使用反射的方式提取出相应的方法和属性,以便在继承类中复用。这也是对于类功能进行继承扩展的时候的一种很有用的方式。

image

image

第二点是 ABP 缓存模块通过 IDistributedCache<CacheItem, CacheKey> 接口扩展了原生的 IDistributedCache 接口的功能,而在具体实现上是将原有的 IDistributedCache 服务注入进行复用相应功能的,并在前后增加额外的逻辑对功能进行扩展增强,实际上就是适配器模式。

image

image

而最常使用的 IDistributedCache<CacheItem> 接口,以前其实现类是直接继承 DistributedCache<CacheItem, CacheKey> 的,现在改成了直接用适配器模式。

image

最后一个是缓存的事务性,通过工作单元使缓存和其他事务操作保持原子性,避免缓存已经更新而其他事务失败回滚导致数据不一致的问题。实际上就是先不真正地更新缓存,而是将缓存数据通过字典保存在工作单元中,保证一个工作单元内拿到的缓存数据是最新的,同时注册工作单元提交事件,在工作单元正在提交成功的时候才执行真正更新缓存的逻辑。

image

工作单元相关的内容就后面再在专门的章节讲吧,这部分的内容比较复杂,一时半会比较难讲清。

2. 自己扩展的 IDistributedCache

ABP 框架扩展的 IDistributedCache 泛型接口在内部帮我们处理实例对象进行序列化/反序列化以及转码为 byte 数组的问题,大大提升了我们使用分布式缓存的方便性,但也存在一些问题。

  • 它是基于类的泛型,如果我们在一个服务中需要使用多个缓存类型的话,我们就得注入多个的泛型接口,还是有些不方便的
  • 它的缓存键是以默认是以泛型类型的全类名作为前缀,虽然我们可以通过特性指定类型名称,但是对于集合,缓存键就很不清晰了
    由于这些情况的存在,也基于我们在日常开发中的使用习惯,我在工作中又基于 ABP 的 IDistributedCache<CacheItem> 接口进行扩展。内部实现基本一致,主要就是将基于类的泛型,改成基于方法的泛型,并且提供自己的 IDistributedCacheKeyNormalizer 实现类,将缓存键的设置规则交给了缓存存取时进行设置。 代码如下:
public interface IWantDistributedCache
{
	/// <summary>
	/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <returns>The cache item, or null.</returns>
	TCacheItem Get<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Gets multiple cache items with the given keys.
	///
	/// The returned list contains exactly the same count of items specified in the given keys.
	/// An item in the return list can not be null, but an item in the list has null value
	/// if the related key not found in the cache.
	/// </summary>
	/// <param name="keys">The keys of cached items to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <returns>List of cache items.</returns>
	KeyValuePair<TCacheKey, TCacheItem>[] GetMany<TCacheItem, TCacheKey>(
		IEnumerable<TCacheKey> keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Gets multiple cache items with the given keys.
	///
	/// The returned list contains exactly the same count of items specified in the given keys.
	/// An item in the return list can not be null, but an item in the list has null value
	/// if the related key not found in the cache.
	///
	/// </summary>
	/// <param name="keys">The keys of cached items to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// /// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>List of cache items.</returns>
	Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyAsync<TCacheItem, TCacheKey>(
		IEnumerable<TCacheKey> keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;

	/// <summary>
	/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The cache item, or null.</returns>
	Task<TCacheItem> GetAsync<TCacheItem, TCacheKey>(
		[NotNull] TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;

	/// <summary>
	/// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item
	/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
	/// <param name="optionsFactory">The cache options for the factory delegate.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <returns>The cache item.</returns>
	TCacheItem GetOrAdd<TCacheItem, TCacheKey>(
		TCacheKey key,
		Func<TCacheItem> factory,
		string cacheName,
		Func<DistributedCacheEntryOptions> optionsFactory = null,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item
	/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
	/// <param name="optionsFactory">The cache options for the factory delegate.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The cache item.</returns>
	Task<TCacheItem> GetOrAddAsync<TCacheItem, TCacheKey>(
		[NotNull] TCacheKey key,
		Func<Task<TCacheItem>> factory,
		string cacheName,
		Func<DistributedCacheEntryOptions> optionsFactory = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;

	/// <summary>
	/// Sets the cache item value for the provided key.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="value">The cache item value to set in the cache.</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	void Set<TCacheItem, TCacheKey>(
		TCacheKey key,
		TCacheItem value,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Sets the cache item value for the provided key.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="value">The cache item value to set in the cache.</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	Task SetAsync<TCacheItem, TCacheKey>(
		[NotNull] TCacheKey key,
		[NotNull] TCacheItem value,
		string cacheName,
		[CanBeNull] DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;

	/// <summary>
	/// Sets multiple cache items.
	/// Based on the implementation, this can be more efficient than setting multiple items individually.
	/// </summary>
	/// <param name="items">Items to set on the cache</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	void SetMany<TCacheItem, TCacheKey>(
		IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Sets multiple cache items.
	/// Based on the implementation, this can be more efficient than setting multiple items individually.
	/// </summary>
	/// <param name="items">Items to set on the cache</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	Task SetManyAsync<TCacheItem, TCacheKey>(
		IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;

	/// <summary>
	/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	void Refresh<TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null
	);

	/// <summary>
	/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	Task RefreshAsync<TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		CancellationToken token = default
	);

	/// <summary>
	/// Removes the cache item for given key from cache.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	void Remove<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false
	) where TCacheItem : class;

	/// <summary>
	/// Removes the cache item for given key from cache.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	Task RemoveAsync<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default
	) where TCacheItem : class;
}

public class WantDistributedCache : IWantDistributedCache
{
	public const string UowCacheName = "WantDistributedCache";

	public ILogger<WantDistributedCache> Logger { get; set; }

	protected string CacheName { get; set; }

	protected bool IgnoreMultiTenancy { get; set; }

	protected IDistributedCache Cache { get; }

	protected ICancellationTokenProvider CancellationTokenProvider { get; }

	protected IDistributedCacheSerializer Serializer { get; }

	protected IDistributedCacheKeyNormalizer KeyNormalizer { get; }

	protected IHybridServiceScopeFactory ServiceScopeFactory { get; }

	protected IUnitOfWorkManager UnitOfWorkManager { get; }

	protected SemaphoreSlim SyncSemaphore { get; }

	protected DistributedCacheEntryOptions DefaultCacheOptions;

	private readonly AbpDistributedCacheOptions _distributedCacheOption;

	public WantDistributedCache(
		IOptions<AbpDistributedCacheOptions> distributedCacheOption,
		IDistributedCache cache,
		ICancellationTokenProvider cancellationTokenProvider,
		IDistributedCacheSerializer serializer,
		IDistributedCacheKeyNormalizer keyNormalizer,
		IHybridServiceScopeFactory serviceScopeFactory,
		IUnitOfWorkManager unitOfWorkManager)
	{
		_distributedCacheOption = distributedCacheOption.Value;
		Cache = cache;
		CancellationTokenProvider = cancellationTokenProvider;
		Logger = NullLogger<WantDistributedCache>.Instance;
		Serializer = serializer;
		KeyNormalizer = keyNormalizer;
		ServiceScopeFactory = serviceScopeFactory;
		UnitOfWorkManager = unitOfWorkManager;

		SyncSemaphore = new SemaphoreSlim(1, 1);

		SetDefaultOptions();
	}

	protected virtual string NormalizeKey<TCacheKey>(TCacheKey key, string cacheName)
	{
		return KeyNormalizer.NormalizeKey(
			new DistributedCacheKeyNormalizeArgs(
				key.ToString(),
				cacheName,
				IgnoreMultiTenancy
			)
		);
	}

	protected virtual DistributedCacheEntryOptions GetDefaultCacheEntryOptions()
	{
		foreach (var configure in _distributedCacheOption.CacheConfigurators)
		{
			var options = configure.Invoke(CacheName);
			if (options != null)
			{
				return options;
			}
		}

		return _distributedCacheOption.GlobalCacheEntryOptions;
	}

	protected virtual void SetDefaultOptions()
	{
		//CacheName = CacheNameAttribute.GetCacheName(typeof(TCacheItem));

		////IgnoreMultiTenancy
		//IgnoreMultiTenancy = typeof(TCacheItem).IsDefined(typeof(IgnoreMultiTenancyAttribute), true);

		//Configure default cache entry options
		DefaultCacheOptions = GetDefaultCacheEntryOptions();
	}

	/// <summary>
	/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <returns>The cache item, or null.</returns>
	public virtual TCacheItem Get<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		if (ShouldConsiderUow(considerUow))
		{
			var value = GetUnitOfWorkCache<TCacheItem, TCacheKey>().GetOrDefault(key)?.GetUnRemovedValueOrNull();
			if (value != null)
			{
				return value;
			}
		}

		byte[] cachedBytes;

		try
		{
			cachedBytes = Cache.Get(NormalizeKey(key, cacheName));
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				HandleException(ex);
				return null;
			}

			throw;
		}

		return ToCacheItem<TCacheItem>(cachedBytes);
	}

	public virtual KeyValuePair<TCacheKey, TCacheItem>[] GetMany<TCacheItem, TCacheKey>(
		IEnumerable<TCacheKey> keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		var keyArray = keys.ToArray();

		var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
		if (cacheSupportsMultipleItems == null)
		{
			return GetManyFallback<TCacheItem, TCacheKey>(
				keyArray,
				cacheName,
				hideErrors,
				considerUow
			);
		}

		var notCachedKeys = new List<TCacheKey>();
		var cachedValues = new List<KeyValuePair<TCacheKey, TCacheItem>>();
		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			foreach (var key in keyArray)
			{
				var value = uowCache.GetOrDefault(key)?.GetUnRemovedValueOrNull();
				if (value != null)
				{
					cachedValues.Add(new KeyValuePair<TCacheKey, TCacheItem>(key, value));
				}
			}

			notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList();
			if (!notCachedKeys.Any())
			{
				return cachedValues.ToArray();
			}
		}

		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
		byte[][] cachedBytes;

		var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray;
		try
		{
			cachedBytes = cacheSupportsMultipleItems.GetMany(readKeys.Select(key => NormalizeKey(key, cacheName)));
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				HandleException(ex);
				return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keyArray);
			}

			throw;
		}

		return cachedValues.Concat(ToCacheItems<TCacheItem, TCacheKey>(cachedBytes, readKeys)).ToArray();
	}

	protected virtual KeyValuePair<TCacheKey, TCacheItem>[] GetManyFallback<TCacheItem, TCacheKey>(
		TCacheKey[] keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			return keys
				.Select(key => new KeyValuePair<TCacheKey, TCacheItem>(
						key,
						Get<TCacheItem, TCacheKey>(key, cacheName, false, considerUow)
					)
				).ToArray();
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				HandleException(ex);
				return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keys);
			}

			throw;
		}
	}

	public virtual async Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyAsync<TCacheItem, TCacheKey>(
		IEnumerable<TCacheKey> keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		var keyArray = keys.ToArray();

		var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
		if (cacheSupportsMultipleItems == null)
		{
			return await GetManyFallbackAsync<TCacheItem, TCacheKey>(
				keyArray,
				cacheName,
				hideErrors,
				considerUow,
				token
			);
		}

		var notCachedKeys = new List<TCacheKey>();
		var cachedValues = new List<KeyValuePair<TCacheKey, TCacheItem>>();
		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			foreach (var key in keyArray)
			{
				var value = uowCache.GetOrDefault(key)?.GetUnRemovedValueOrNull();
				if (value != null)
				{
					cachedValues.Add(new KeyValuePair<TCacheKey, TCacheItem>(key, value));
				}
			}

			notCachedKeys = keyArray.Except(cachedValues.Select(x => x.Key)).ToList();
			if (!notCachedKeys.Any())
			{
				return cachedValues.ToArray();
			}
		}

		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;
		byte[][] cachedBytes;

		var readKeys = notCachedKeys.Any() ? notCachedKeys.ToArray() : keyArray;

		try
		{
			cachedBytes = await cacheSupportsMultipleItems.GetManyAsync(
				readKeys.Select(key => NormalizeKey(key, cacheName)),
				CancellationTokenProvider.FallbackToProvider(token)
			);
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				await HandleExceptionAsync(ex);
				return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keyArray);
			}

			throw;
		}

		return cachedValues.Concat(ToCacheItems<TCacheItem, TCacheKey>(cachedBytes, readKeys)).ToArray();
	}

	protected virtual async Task<KeyValuePair<TCacheKey, TCacheItem>[]> GetManyFallbackAsync<TCacheItem, TCacheKey>(
		TCacheKey[] keys,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			var result = new List<KeyValuePair<TCacheKey, TCacheItem>>();

			foreach (var key in keys)
			{
				result.Add(new KeyValuePair<TCacheKey, TCacheItem>(
					key,
					await GetAsync<TCacheItem, TCacheKey>(key, cacheName, false, considerUow, token: token))
				);
			}

			return result.ToArray();
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				await HandleExceptionAsync(ex);
				return ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(keys);
			}

			throw;
		}
	}

	/// <summary>
	/// Gets a cache item with the given key. If no cache item is found for the given key then returns null.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The cache item, or null.</returns>
	public virtual async Task<TCacheItem> GetAsync<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		if (ShouldConsiderUow(considerUow))
		{
			var value = GetUnitOfWorkCache<TCacheItem, TCacheKey>().GetOrDefault(key)?.GetUnRemovedValueOrNull();
			if (value != null)
			{
				return value;
			}
		}

		byte[] cachedBytes;

		try
		{
			cachedBytes = await Cache.GetAsync(
				NormalizeKey(key, cacheName),
				CancellationTokenProvider.FallbackToProvider(token)
			);
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				await HandleExceptionAsync(ex);
				return null;
			}

			throw;
		}

		if (cachedBytes == null)
		{
			return null;
		}

		return Serializer.Deserialize<TCacheItem>(cachedBytes);
	}

	/// <summary>
	/// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item
	/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
	/// <param name="optionsFactory">The cache options for the factory delegate.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <returns>The cache item.</returns>
	public virtual TCacheItem GetOrAdd<TCacheItem, TCacheKey>(
		TCacheKey key,
		Func<TCacheItem> factory,
		string cacheName,
		Func<DistributedCacheEntryOptions> optionsFactory = null,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		var value = Get<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow);
		if (value != null)
		{
			return value;
		}

		using (SyncSemaphore.Lock())
		{
			value = Get<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow);
			if (value != null)
			{
				return value;
			}

			value = factory();

			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
				if (uowCache.TryGetValue(key, out var item))
				{
					item.SetValue(value);
				}
				else
				{
					uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
				}
			}

			Set(key, value, cacheName, optionsFactory?.Invoke(), hideErrors, considerUow);
		}

		return value;
	}

	/// <summary>
	/// Gets or Adds a cache item with the given key. If no cache item is found for the given key then adds a cache item
	/// provided by <paramref name="factory" /> delegate and returns the provided cache item.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="factory">The factory delegate is used to provide the cache item when no cache item is found for the given <paramref name="key" />.</param>
	/// <param name="optionsFactory">The cache options for the factory delegate.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The cache item.</returns>
	public virtual async Task<TCacheItem> GetOrAddAsync<TCacheItem, TCacheKey>(
		TCacheKey key,
		Func<Task<TCacheItem>> factory,
		string cacheName,
		Func<DistributedCacheEntryOptions> optionsFactory = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		token = CancellationTokenProvider.FallbackToProvider(token);
		var value = await GetAsync<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow, token);
		if (value != null)
		{
			return value;
		}

		using (await SyncSemaphore.LockAsync(token))
		{
			value = await GetAsync<TCacheItem, TCacheKey>(key, cacheName, hideErrors, considerUow, token);
			if (value != null)
			{
				return value;
			}

			value = await factory();

			if (ShouldConsiderUow(considerUow))
			{
				var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
				if (uowCache.TryGetValue(key, out var item))
				{
					item.SetValue(value);
				}
				else
				{
					uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
				}
			}

			await SetAsync(key, value, cacheName, optionsFactory?.Invoke(), hideErrors, considerUow, token);
		}

		return value;
	}

	/// <summary>
	/// Sets the cache item value for the provided key.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="value">The cache item value to set in the cache.</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	public virtual void Set<TCacheItem, TCacheKey>(
		TCacheKey key,
		TCacheItem value,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		void SetRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				Cache.Set(
					NormalizeKey(key, cacheName),
					Serializer.Serialize(value),
					options ?? DefaultCacheOptions
				);
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			if (uowCache.TryGetValue(key, out _))
			{
				uowCache[key].SetValue(value);
			}
			else
			{
				uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(() =>
			{
				SetRealCache();
				return Task.CompletedTask;
			});
		}
		else
		{
			SetRealCache();
		}
	}
	/// <summary>
	/// Sets the cache item value for the provided key.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="value">The cache item value to set in the cache.</param>
	/// <param name="options">The cache options for the value.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	public virtual async Task SetAsync<TCacheItem, TCacheKey>(
		TCacheKey key,
		TCacheItem value,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		async Task SetRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				await Cache.SetAsync(
					NormalizeKey(key, cacheName),
					Serializer.Serialize(value),
					options ?? DefaultCacheOptions,
					CancellationTokenProvider.FallbackToProvider(token)
				);
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			if (uowCache.TryGetValue(key, out _))
			{
				uowCache[key].SetValue(value);
			}
			else
			{
				uowCache.Add(key, new UnitOfWorkCacheItem<TCacheItem>(value));
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(SetRealCache);
		}
		else
		{
			await SetRealCache();
		}
	}

	public void SetMany<TCacheItem, TCacheKey>(
		IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		var itemsArray = items.ToArray();

		var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
		if (cacheSupportsMultipleItems == null)
		{
			SetManyFallback(
				itemsArray,
				cacheName,
				options,
				hideErrors,
				considerUow
			);

			return;
		}

		void SetRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				cacheSupportsMultipleItems.SetMany(
					ToRawCacheItems(itemsArray, cacheName),
					options ?? DefaultCacheOptions
				);
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();

			foreach (var pair in itemsArray)
			{
				if (uowCache.TryGetValue(pair.Key, out _))
				{
					uowCache[pair.Key].SetValue(pair.Value);
				}
				else
				{
					uowCache.Add(pair.Key, new UnitOfWorkCacheItem<TCacheItem>(pair.Value));
				}
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(() =>
			{
				SetRealCache();
				return Task.CompletedTask;
			});
		}
		else
		{
			SetRealCache();
		}
	}

	protected virtual void SetManyFallback<TCacheItem, TCacheKey>(
		KeyValuePair<TCacheKey, TCacheItem>[] items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			foreach (var item in items)
			{
				Set(
					item.Key,
					item.Value,
					cacheName,
					options,
					false,
					considerUow
				);
			}
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				HandleException(ex);
				return;
			}

			throw;
		}
	}

	public virtual async Task SetManyAsync<TCacheItem, TCacheKey>(
		IEnumerable<KeyValuePair<TCacheKey, TCacheItem>> items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		var itemsArray = items.ToArray();

		var cacheSupportsMultipleItems = Cache as ICacheSupportsMultipleItems;
		if (cacheSupportsMultipleItems == null)
		{
			await SetManyFallbackAsync(
				itemsArray,
				cacheName,
				options,
				hideErrors,
				considerUow,
				token
			);

			return;
		}

		async Task SetRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				await cacheSupportsMultipleItems.SetManyAsync(
					ToRawCacheItems(itemsArray, cacheName),
					options ?? DefaultCacheOptions,
					CancellationTokenProvider.FallbackToProvider(token)
				);
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();

			foreach (var pair in itemsArray)
			{
				if (uowCache.TryGetValue(pair.Key, out _))
				{
					uowCache[pair.Key].SetValue(pair.Value);
				}
				else
				{
					uowCache.Add(pair.Key, new UnitOfWorkCacheItem<TCacheItem>(pair.Value));
				}
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(SetRealCache);
		}
		else
		{
			await SetRealCache();
		}
	}

	protected virtual async Task SetManyFallbackAsync<TCacheItem, TCacheKey>(
		KeyValuePair<TCacheKey, TCacheItem>[] items,
		string cacheName,
		DistributedCacheEntryOptions options = null,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			foreach (var item in items)
			{
				await SetAsync(
					item.Key,
					item.Value,
					cacheName,
					options,
					false,
					considerUow,
					token: token
				);
			}
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				await HandleExceptionAsync(ex);
				return;
			}

			throw;
		}
	}

	/// <summary>
	/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	public virtual void Refresh<TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null)
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			Cache.Refresh(NormalizeKey(key, cacheName));
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				HandleException(ex);
				return;
			}

			throw;
		}
	}

	/// <summary>
	/// Refreshes the cache value of the given key, and resets its sliding expiration timeout.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	public virtual async Task RefreshAsync<TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		CancellationToken token = default)
	{
		hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

		try
		{
			await Cache.RefreshAsync(NormalizeKey(key, cacheName), CancellationTokenProvider.FallbackToProvider(token));
		}
		catch (Exception ex)
		{
			if (hideErrors == true)
			{
				await HandleExceptionAsync(ex);
				return;
			}

			throw;
		}
	}

	/// <summary>
	/// Removes the cache item for given key from cache.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	public virtual void Remove<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false) where TCacheItem : class
	{
		void RemoveRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				Cache.Remove(NormalizeKey(key, cacheName));
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					HandleException(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			if (uowCache.TryGetValue(key, out _))
			{
				uowCache[key].RemoveValue();
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(() =>
			{
				RemoveRealCache();
				return Task.CompletedTask;
			});
		}
		else
		{
			RemoveRealCache();
		}
	}

	/// <summary>
	/// Removes the cache item for given key from cache.
	/// </summary>
	/// <param name="key">The key of cached item to be retrieved from the cache.</param>
	/// <param name="hideErrors">Indicates to throw or hide the exceptions for the distributed cache.</param>
	/// <param name="considerUow">This will store the cache in the current unit of work until the end of the current unit of work does not really affect the cache.</param>
	/// <param name="token">The <see cref="T:System.Threading.CancellationToken" /> for the task.</param>
	/// <returns>The <see cref="T:System.Threading.Tasks.Task" /> indicating that the operation is asynchronous.</returns>
	public virtual async Task RemoveAsync<TCacheItem, TCacheKey>(
		TCacheKey key,
		string cacheName,
		bool? hideErrors = null,
		bool considerUow = false,
		CancellationToken token = default) where TCacheItem : class
	{
		async Task RemoveRealCache()
		{
			hideErrors = hideErrors ?? _distributedCacheOption.HideErrors;

			try
			{
				await Cache.RemoveAsync(NormalizeKey(key, cacheName), CancellationTokenProvider.FallbackToProvider(token));
			}
			catch (Exception ex)
			{
				if (hideErrors == true)
				{
					await HandleExceptionAsync(ex);
					return;
				}

				throw;
			}
		}

		if (ShouldConsiderUow(considerUow))
		{
			var uowCache = GetUnitOfWorkCache<TCacheItem, TCacheKey>();
			if (uowCache.TryGetValue(key, out _))
			{
				uowCache[key].RemoveValue();
			}

			// ReSharper disable once PossibleNullReferenceException
			UnitOfWorkManager.Current.OnCompleted(RemoveRealCache);
		}
		else
		{
			await RemoveRealCache();
		}
	}

	protected virtual void HandleException(Exception ex)
	{
		AsyncHelper.RunSync(() => HandleExceptionAsync(ex));
	}

	protected virtual async Task HandleExceptionAsync(Exception ex)
	{
		Logger.LogException(ex, LogLevel.Warning);

		using (var scope = ServiceScopeFactory.CreateScope())
		{
			await scope.ServiceProvider
				.GetRequiredService<IExceptionNotifier>()
				.NotifyAsync(new ExceptionNotificationContext(ex, LogLevel.Warning));
		}
	}

	protected virtual KeyValuePair<TCacheKey, TCacheItem>[] ToCacheItems<TCacheItem, TCacheKey>(byte[][] itemBytes, TCacheKey[] itemKeys) where TCacheItem: class
	{
		if (itemBytes.Length != itemKeys.Length)
		{
			throw new AbpException("count of the item bytes should be same with the count of the given keys");
		}

		var result = new List<KeyValuePair<TCacheKey, TCacheItem>>();

		for (int i = 0; i < itemKeys.Length; i++)
		{
			result.Add(
				new KeyValuePair<TCacheKey, TCacheItem>(
					itemKeys[i],
					ToCacheItem<TCacheItem>(itemBytes[i])
				)
			);
		}

		return result.ToArray();
	}

	[CanBeNull]
	protected virtual TCacheItem ToCacheItem<TCacheItem>([CanBeNull] byte[] bytes) where TCacheItem : class
	{
		if (bytes == null)
		{
			return null;
		}

		return Serializer.Deserialize<TCacheItem>(bytes);
	}


	protected virtual KeyValuePair<string, byte[]>[] ToRawCacheItems<TCacheItem, TCacheKey>(KeyValuePair<TCacheKey, TCacheItem>[] items, string cacheName)
	{
		return items
			.Select(i => new KeyValuePair<string, byte[]>(
					NormalizeKey(i.Key, cacheName),
					Serializer.Serialize(i.Value)
				)
			).ToArray();
	}

	private static KeyValuePair<TCacheKey, TCacheItem>[] ToCacheItemsWithDefaultValues<TCacheItem, TCacheKey>(TCacheKey[] keys)
	{
		return keys
			.Select(key => new KeyValuePair<TCacheKey, TCacheItem>(key, default))
			.ToArray();
	}

	protected virtual bool ShouldConsiderUow(bool considerUow)
	{
		return considerUow && UnitOfWorkManager.Current != null;
	}

	protected virtual string GetUnitOfWorkCacheKey()
	{
		return UowCacheName + CacheName;
	}

	protected virtual Dictionary<TCacheKey, UnitOfWorkCacheItem<TCacheItem>> GetUnitOfWorkCache<TCacheItem, TCacheKey>() where TCacheItem : class
	{
		if (UnitOfWorkManager.Current == null)
		{
			throw new AbpException($"There is no active UOW.");
		}

		return UnitOfWorkManager.Current.GetOrAddItem(GetUnitOfWorkCacheKey(),
			key => new Dictionary<TCacheKey, UnitOfWorkCacheItem<TCacheItem>>());
	}
}

[Dependency(ReplaceServices = true)]
public class WantDistributedCacheKeyNormalizer : IDistributedCacheKeyNormalizer, ITransientDependency
{
	protected AbpDistributedCacheOptions DistributedCacheOptions { get; }

	public SuncereDistributedCacheKeyNormalizer(
		IOptions<AbpDistributedCacheOptions> distributedCacheOptions)
	{
		DistributedCacheOptions = distributedCacheOptions.Value;
	}

	public virtual string NormalizeKey(DistributedCacheKeyNormalizeArgs args)
	{
		// 缓存格式: a:appname,c:cachename,k:key
		var normalizedKey = $"a:{DistributedCacheOptions.KeyPrefix},c:{args.CacheName},k:{args.Key}";
		return normalizedKey;
	}
}

[DependsOn(typeof(AbpCachingModule))]
public class WantAbpCacheModule : AbpModule
{
	public override void ConfigureServices(ServiceConfigurationContext context)
	{
		//注入缓存类
		context.Services.AddSingleton(typeof(ISuncereDistributedCache), typeof(SuncereDistributedCache));
	}
}


参考文章:
ABP 官方文档 - 缓存



ABP 系列总结:
目录:ABP 系列总结
上一篇:ABP - 缓存模块(1)
下一篇:ABP - 本地事件总线

posted @ 2023-06-27 12:57  啊晚  阅读(683)  评论(0编辑  收藏  举报