写点什么

Util 应用框架基础(七)- 缓存

作者:何镇汐
  • 2023-11-21
    四川
  • 本文字数:19416 字

    阅读完需:约 64 分钟

缓存

Util 应用框架缓存操作


本节介绍 Util 应用框架如何操作缓存.

概述

缓存是提升性能的关键手段之一.


除了提升性能,缓存对系统健壮性和安全性也有影响.


不同类型的系统对缓存的依赖程度不同.


对于后台管理系统,由于是给管理人员使用的,用户有限,而且操作基本都需要身份认证和授权,甚至可能部署在局域网内,一般仅对耗时操作使用缓存即可.


但是商城,门户网站这类系统, 它们部署在互联网上,并且允许匿名用户访问,仅缓存耗时操作是不够的.


除了访问量可能比较大,另外需要防范网络流氓的恶意攻击,他们会发送大量请求来试探你的系统.


如果某个读取操作直接到达数据库,哪怕仅执行非常简单的 SQL,由于请求非常密集,服务器的 CPU 将很快到达 100%从而拒绝服务.


对于这类系统,需要对暴露到互联网上的所有页面和读取数据的 API 进行缓存.


当然也可以使用更多的只读数据库和其它高性能数据库分摊读取压力,本文介绍基于内存和 Redis 的缓存操作.

缓存框架

要缓存数据,需要选择一种缓存框架.

.Net 缓存

  • 本地缓存 IMemoryCache


  • .Net 提供了 Microsoft.Extensions.Caching.Memory.IMemoryCache 进行本地缓存操作.

  • IMemoryCache 可以将数据对象缓存到 Web 服务器进程的内存中.

  • 本地缓存的主要优势是性能非常高,而且不需要序列化对象.

    本地缓存的主要问题是内存容量受限和更新同步困难.

  • 本地缓存可使用的内存容量受 Web 服务器内存的限制.

  • 可以在单体项目中使用本地缓存.

  • 如果单体项目仅部署一个 Web 服务器实例,缓存只有一个副本,不存在更新同步的问题.

  • 但是如果将单体项目部署到 Web 集群,由于 Web 服务器实例不止一个,每个 Web 服务器都会产生一个缓存副本.

  • 想要同时更新多个 Web 服务器的本地缓存非常困难,这可能导致缓存的数据不一致.

  • 可以使用负载均衡器的会话粘滞特性将用户每次请求都定位到同一台 Web 服务器,从而避免多次请求看到不一致的数据.

  • 微服务项目情况则更为复杂,由于包含多个 Web Api 项目,每个 Web Api 项目都会部署到一个或多个 Web 服务器.

  • 不同 Web Api 项目可能需要使用相同的缓存数据,无法使用负载均衡器的会话粘滞特性解决该问题.

  • 我们需要使用分布式缓存来解决内存容量和更新同步的问题.

  • 分布式缓存 IDistributedCache


  • .Net 提供了 Microsoft.Extensions.Caching.Distributed.IDistributedCache 进行分布式缓存操作.

  • IDistributedCache 可以将数据对象序列化后保存到 Redis 等缓存服务器中.

  • 相比基于内存的本地缓存, 分布式缓存的性能要低得多, 不仅要序列化对象,还需要跨进程网络调用.

  • 但是由于不使用 Web 服务器的内存,所以可以轻松的增加缓存容量.

  • 把缓存抽出来放到专门的服务器后,多个 Web Api 项目就可以共享缓存.

  • 由于缓存只有一份,也就不存在同步更新.

  • 直接使用.Net 缓存的问题


  • 毫无疑问,你可以直接使用 IMemoryCacheIDistributedCache 接口进行缓存操作.

  • 但会面临以下问题:

  • Api 生硬且不统一.

  • IDistributedCache 直接操作 byte[] ,如果不进一步封装很难使用.

  • 你需要明确指定是本地缓存还是分布式缓存,无法使用统一的 API,不能通过配置进行切换.

  • 需要自行处理缓存过期引起的性能问题.

  • 如果同一时间,缓存正好大面积过期,大量请求到达数据库,从而导致系统可能崩溃,这称为缓存雪崩.

  • 简单的处理办法是给每个缓存项设置不同的缓存时间,如果统一配置缓存时间,则添加一个随机间隔,让缓存过期的时间错开即可.

  • 另一个棘手的问题,如果很多请求并发访问某个热点缓存项,当缓存过期,这些并发请求将到达数据库,这称为缓存击穿.

  • 虽然只有一个缓存项过期,但还是会损害系统性能.

  • 可以对并发请求加锁,只允许第一个进入的请求到达数据库并更新缓存,后续请求将从更新的缓存读取.

  • 为进一步提升性能,可以在缓存过期前的某个时间更新缓存,从而避免锁定请求造成的等待.

  • 缺失前缀移除等关键特性.

  • 有些缓存项具有相关性,比如为当前用户设置权限,菜单,个人偏好等缓存项,当他退出登录时,需要清除跟他相关的所有缓存项.

  • 你可以一个个的移除,但相当费力.

  • 可以为具有相关性的缓存项设置相同的缓存前缀,并通过缓存前缀找出所有相关缓存项,从而一次性移除它们.

  • 遗憾的是, .Net 缓存并不支持这些特性,需要自行实现.

缓存框架 EasyCaching

EasyCaching 是一个专业而易用的缓存框架,提供统一的 API 接口,并解决了上述问题.


EasyCaching 支持多种缓存提供程序,可以将缓存写入内存,Redis,Memcached 等.


EasyCaching 支持多种序列化方式,可在使用分布式缓存时指定.


EasyCaching 支持前缀移除,模式移除等高级用法.


除此之外,EasyCaching 还支持 2 级缓存.


2 级缓存可以让你的项目从本地缓存中获取数据,这样可以获得很高的读取性能.


当本地缓存过期,本地缓存会请求 Redis 分布式缓存,Redis 缓存从数据库读取最新数据,并更新本地缓存.


Redis 还充当事件总线的角色,每当数据更新,通过 Redis 总线发布事件,同步更新所有本地缓存副本,解决了本地缓存更新困难的难题.


与 IMemoryCache 相比, EasyCaching 的本地缓存性能稍低,毕竟实现了更多功能.


Util 应用框架使用 EasyCaching 缓存框架,并进行简单包装.


Util 仅引入了 EasyCaching 的本地缓存Redis 缓存两种提供程序, 以及 SystemTextJson 序列化方式.


如果需要使用其它提供程序和序列化方式,请自行引入相关 Nuget 包.

基础用法

配置缓存

请转到本地缓存,Redis 缓存,2 级缓存章节查看具体配置方法.


  • 配置参数


  • EasyCaching 缓存提供了多个配置参数,具体请参考 EasyCaching 文档.

  • 下面介绍几个比较重要的参数.

  • MaxRdSecond

  • MaxRdSecond 是额外添加的缓存间隔最大随机秒数.

  • MaxRdSecond 用于防止缓存雪崩,在缓存时间基础上增加随机秒数,以防止同一时间所有缓存项失效.

  • MaxRdSecond 的默认值为 120, 增加的随机间隔是 120 秒以内的某个随机值.

  • 你可以增大 MaxRdSecond ,以更大的范围错开各缓存项的失效时间.

  • 对于集成测试,你如果要测试缓存失效时间,需要将该值设置为 0.

  • CacheNulls

  • CacheNulls 用于解决缓存穿透问题.

  • 当使用 Get( key, ()=> value ) 方法获取缓存时,如果返回的 value 为 null,是否应该创建缓存项.

  • CacheNulls 的值为 true 时,创建缓存项.

  • 如果返回值为 null 不创建缓存项,使用相同缓存键的每次请求都会到达数据库.

  • CacheNulls 设置为 true 可防范正常业务的缓存穿透.

  • 但恶意攻击每次传递的参数可能不同,请求依然会到达数据库,且浪费缓存空间.

  • 可以通过缓存全部有效参数的方式精确判断输入参数是否在有效业务范围,不过会占用过多内存.

  • 要减少内存占用,可使用布隆过滤器.

  • EasyCaching 尚未内置布隆过滤器,请自行实现.

缓存键

每个缓存项有一个唯一标识的键名,通过缓存键来获取缓存项.


缓存键通常是一个字符串.


可以以任意方式构造缓存键,只要保证唯一即可.


但是根据缓存项的功能进行构造更容易识别缓存项的用途.


范例 1:


是否管理员缓存键


IsAdmin-1
复制代码


IsAdmin 代表是否管理员, 1 是用户的 Id,需要把用户 Id 的参数拼接到缓存键,以识别特定的缓存项


范例 2:


菜单缓存键.


Menu-1
复制代码


Menu 代表菜单, 1 是用户的 Id.

缓存键前缀

如果用户退出了,我们需要清除他的全部缓存项.


EasyCaching 支持通过缓存键前缀批量移除缓存项.


修改前面的范例.


是否管理员缓存键: User-1-IsAdmin


菜单缓存键: User-1-Menu


User 代表用户,1 是用户 Id, User-1 前缀可以标识 Id 为 1 的用户.


使用 User-1 前缀就可以移除用户 1 的所有缓存项.

CacheKey

你可以直接创建缓存键字符串,不过有些缓存键可能比较复杂,由很多参数构成.


另外可能需要在多个地方使用同一个缓存键进行操作.


用一个对象来封装缓存键的构造,不仅可以降低缓存键的复杂性,而且也方便多处使用.


Util 应用框架提供了一个缓存键对象 Util.Caching.CacheKey.


CacheKey 包含两个属性, Prefix 和 Key.


Prefix 是缓存键前缀,Key 是缓存键.


通常不直接使用 CacheKey,而是从它派生具体的缓存键,这样可以更清晰的表示缓存项的用途,以及更好的接收参数.


范例:


  • 定义 AclCacheKey 缓存键.

  • AclCacheKey 表示访问控制缓存键,接收用户 Id 和资源 Id 参数.


public class AclCacheKey : CacheKey {    public AclCacheKey( string userId, string resourceId ) {        Prefix = $"User-{userId}:";        Key = $"Acl-{resourceId}";    }}
复制代码
  • 使用 AclCacheKey 缓存键.

  • 实例化 AclCacheKey ,传入参数, 通过 Key 属性获取缓存键.


var cacheKey = new AclCacheKey("1","2");var key = cacheKey.Key;
复制代码
  • Key 属性返回 Prefix 与 Key 连接后的结果: User-1:Acl-2

  • 也可以使用 ToString 方法获取缓存键.


var cacheKey = new AclCacheKey("1","2");var key = cacheKey.ToString();
复制代码

缓存操作

Util 应用框架缓存操作提供了三个接口: ICache, ILocalCache, IRedisCache.


具体参考缓存操作 API 章节.

更新缓存

  • 设置缓存到期时间


  • 创建缓存项时可以设置一个过期时间间隔,超过到期时间,缓存将失效.

  • EasyCaching 目前尚不支持滑动过期.

  • 下面的示例设置 1 小时的过期时间间隔,当超过 1 小时,缓存过期后,将重新加载最新数据.


var cacheKey = new UserResourceCacheKey( "1", "2" );var result = _cache.Get( cacheKey, () => _repository.GetUserResource( "1", "2" ), new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
复制代码
  • 通过本地事件总线更新缓存


  • 基于过期时间被动更新,适合实时性要求低的场景.

  • 当数据库的值已更新,从缓存中读取旧值,对业务基本没有影响或影响很小.

  • 但有些数据具有更高的实时性,在数据库更新时,需要同步更新缓存中的副本.

  • 可以通过发布订阅本地事件总线实时更新特定缓存.

缓存拦截器

  • CacheAttribute 缓存拦截器

  • [Cache] 是一个缓存拦截器,使用 ICache 接口操作缓存.

  • 它会根据参数自动创建缓存键,并调用拦截的方法获取数据并缓存起来.

  • 如果你不关心缓存键的长相,可以使用 [Cache] 拦截器快速添加缓存.

  • 范例:


public interface ITestService {    [Cache]    UserResource Get( string userId, string resourceId );}
复制代码
  • 设置缓存键前缀 Prefix.

  • 缓存键前缀支持占位符, {0} 代表第一个参数.

  • 范例:


public interface ITestService {    [Cache( Prefix = "User-{0}" )]    UserResource Get( string userId, string resourceId );}
复制代码
  • 下面的示例调用 ITestService 的 Get 方法,传入参数 userId = "1" , resourceId = "2" .

  • 创建的缓存键为: "User-1:1:2".

  • 缓存键前缀 User-{0} 中的 {0} 替换为第一个参数 userId ,即 User-1.

  • 使用 : 按顺序连接所有参数值.


var result = _service.Get( "1", "2" );
复制代码
  • 设置缓存过期间隔 Expiration ,单位: 秒,默认值: 36000

  • 范例:

  • 设置 120 秒过期.


public interface ITestService {    [Cache( Expiration = 120 )]    UserResource Get( string userId, string resourceId );}
复制代码
  • LocalCacheAttribute 本地缓存拦截器

  • [LocalCache] 与 [Cache] 类似,但它使用 ILocalCache 接口操作缓存.

  • 如果你的某个操作需要使用本地缓存,可以用 [LocalCache].

  • 具体操作请参考 [Cache].

  • RedisCacheAttribute Redis 缓存拦截器

  • [RedisCache] 与 [Cache] 类似,但它使用 IRedisCache 接口操作缓存.

  • 如果你的某个操作需要使用 Redis 缓存,可以用 [RedisCache].

  • 具体操作请参考 [Cache].

缓存内存释放

当缓存占据大量内存空间,调用 Clear 清理缓存并不会释放内存,等待一段时间仍然不会释放.


对于 IMemoryCache 同样如此.


某些测试环境,你可以调用 GC.Collect() 强制回收内存空间.


生产环境,不应手工回收.

源码解析

ICache 缓存操作

Util.Caching.ICache 是缓存操作接口.


CacheManager 将缓存操作委托给 EasyCaching 的 IEasyCachingProvider 接口.


IEasyCachingProvider 根据配置的提供程序切换为本地缓存或 Redis 缓存.


当配置了 2 级缓存, 缓存操作委托给 IHybridCachingProvider 2 级缓存提供程序接口.


/// <summary>/// 缓存/// </summary>public interface ICache {    /// <summary>    /// 缓存是否已存在    /// </summary>    /// <param name="key">缓存键</param>    bool Exists( CacheKey key );    /// <summary>    /// 缓存是否已存在    /// </summary>    /// <param name="key">缓存键</param>    bool Exists( string key );    /// <summary>    /// 缓存是否已存在    /// </summary>    /// <param name="key">缓存键</param>    /// <param name="cancellationToken">取消令牌</param>    Task<bool> ExistsAsync( CacheKey key, CancellationToken cancellationToken = default );    /// <summary>    /// 缓存是否已存在    /// </summary>    /// <param name="key">缓存键</param>    /// <param name="cancellationToken">取消令牌</param>    Task<bool> ExistsAsync( string key, CancellationToken cancellationToken = default );    /// <summary>    /// 从缓存中获取数据    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    T Get<T>( CacheKey key );    /// <summary>    /// 从缓存中获取数据    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    T Get<T>( string key );    /// <summary>    /// 从缓存中获取数据    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="keys">缓存键集合</param>    List<T> Get<T>( IEnumerable<CacheKey> keys );    /// <summary>    /// 从缓存中获取数据    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="keys">缓存键集合</param>    List<T> Get<T>( IEnumerable<string> keys );    /// <summary>    /// 从缓存中获取数据,如果不存在,则执行获取数据操作并添加到缓存中    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="action">获取数据操作</param>    /// <param name="options">缓存配置</param>    T Get<T>( CacheKey key, Func<T> action, CacheOptions options = null );    /// <summary>    /// 从缓存中获取数据,如果不存在,则执行获取数据操作并添加到缓存中    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="action">获取数据操作</param>    /// <param name="options">缓存配置</param>    T Get<T>( string key, Func<T> action, CacheOptions options = null );    /// <summary>    /// 从缓存中获取数据    /// </summary>    /// <param name="key">缓存键</param>    /// <param name="type">缓存数据类型</param>    /// <param name="cancellationToken">取消令牌</param>    Task<object> GetAsync( string key, Type type, CancellationToken cancellationToken = default );    /// <summary>    /// 从缓存中获取数据    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="cancellationToken">取消令牌</param>    Task<T> GetAsync<T>( CacheKey key, CancellationToken cancellationToken = default );    /// <summary>    /// 从缓存中获取数据    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="cancellationToken">取消令牌</param>    Task<T> GetAsync<T>( string key, CancellationToken cancellationToken = default );    /// <summary>    /// 从缓存中获取数据    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="keys">缓存键集合</param>    /// <param name="cancellationToken">取消令牌</param>    Task<List<T>> GetAsync<T>( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default );    /// <summary>    /// 从缓存中获取数据    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="keys">缓存键集合</param>    /// <param name="cancellationToken">取消令牌</param>    Task<List<T>> GetAsync<T>( IEnumerable<string> keys, CancellationToken cancellationToken = default );    /// <summary>    /// 从缓存中获取数据,如果不存在,则执行获取数据操作并添加到缓存中    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="action">获取数据操作</param>    /// <param name="options">缓存配置</param>    /// <param name="cancellationToken">取消令牌</param>    Task<T> GetAsync<T>( CacheKey key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default );    /// <summary>    /// 从缓存中获取数据,如果不存在,则执行获取数据操作并添加到缓存中    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="action">获取数据操作</param>    /// <param name="options">缓存配置</param>    /// <param name="cancellationToken">取消令牌</param>    Task<T> GetAsync<T>( string key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default );    /// <summary>    /// 通过缓存键前缀获取数据    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="prefix">缓存键前缀</param>    List<T> GetByPrefix<T>( string prefix );    /// <summary>    /// 通过缓存键前缀获取数据    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="prefix">缓存键前缀</param>    /// <param name="cancellationToken">取消令牌</param>    Task<List<T>> GetByPrefixAsync<T>( string prefix, CancellationToken cancellationToken = default );    /// <summary>    /// 设置缓存,当缓存已存在则忽略,设置成功返回true    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="value">值</param>    /// <param name="options">缓存配置</param>    bool TrySet<T>( CacheKey key, T value, CacheOptions options = null );    /// <summary>    /// 设置缓存,当缓存已存在则忽略,设置成功返回true    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="value">值</param>    /// <param name="options">缓存配置</param>    bool TrySet<T>( string key, T value, CacheOptions options = null );    /// <summary>    /// 设置缓存,当缓存已存在则忽略,设置成功返回true    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="value">值</param>    /// <param name="options">缓存配置</param>    /// <param name="cancellationToken">取消令牌</param>    Task<bool> TrySetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default );    /// <summary>    /// 设置缓存,当缓存已存在则忽略,设置成功返回true    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="value">值</param>    /// <param name="options">缓存配置</param>    /// <param name="cancellationToken">取消令牌</param>    Task<bool> TrySetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default );    /// <summary>    /// 设置缓存,当缓存已存在则覆盖    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="value">值</param>    /// <param name="options">缓存配置</param>    void Set<T>( CacheKey key, T value, CacheOptions options = null );    /// <summary>    /// 设置缓存,当缓存已存在则覆盖    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="value">值</param>    /// <param name="options">缓存配置</param>    void Set<T>( string key, T value, CacheOptions options = null );    /// <summary>    /// 设置缓存集合    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="items">缓存项集合</param>    /// <param name="options">缓存配置</param>    void Set<T>( IDictionary<CacheKey,T> items, CacheOptions options = null );    /// <summary>    /// 设置缓存集合    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="items">缓存项集合</param>    /// <param name="options">缓存配置</param>    void Set<T>( IDictionary<string, T> items, CacheOptions options = null );    /// <summary>    /// 设置缓存,当缓存已存在则覆盖    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="value">值</param>    /// <param name="options">缓存配置</param>    /// <param name="cancellationToken">取消令牌</param>    Task SetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default );    /// <summary>    /// 设置缓存,当缓存已存在则覆盖    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="key">缓存键</param>    /// <param name="value">值</param>    /// <param name="options">缓存配置</param>    /// <param name="cancellationToken">取消令牌</param>    Task SetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default );    /// <summary>    /// 设置缓存集合    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="items">缓存项集合</param>    /// <param name="options">缓存配置</param>    /// <param name="cancellationToken">取消令牌</param>    Task SetAsync<T>( IDictionary<CacheKey, T> items, CacheOptions options = null, CancellationToken cancellationToken = default );    /// <summary>    /// 设置缓存集合    /// </summary>    /// <typeparam name="T">缓存数据类型</typeparam>    /// <param name="items">缓存项集合</param>    /// <param name="options">缓存配置</param>    /// <param name="cancellationToken">取消令牌</param>    Task SetAsync<T>( IDictionary<string, T> items, CacheOptions options = null, CancellationToken cancellationToken = default );    /// <summary>    /// 移除缓存    /// </summary>    /// <param name="key">缓存键</param>    void Remove( CacheKey key );    /// <summary>    /// 移除缓存    /// </summary>    /// <param name="key">缓存键</param>    void Remove( string key );    /// <summary>    /// 移除缓存集合    /// </summary>    /// <param name="keys">缓存键集合</param>    void Remove( IEnumerable<CacheKey> keys );    /// <summary>    /// 移除缓存集合    /// </summary>    /// <param name="keys">缓存键集合</param>    void Remove( IEnumerable<string> keys );    /// <summary>    /// 移除缓存    /// </summary>    /// <param name="key">缓存键</param>    /// <param name="cancellationToken">取消令牌</param>    Task RemoveAsync( CacheKey key, CancellationToken cancellationToken = default );    /// <summary>    /// 移除缓存    /// </summary>    /// <param name="key">缓存键</param>    /// <param name="cancellationToken">取消令牌</param>    Task RemoveAsync( string key, CancellationToken cancellationToken = default );    /// <summary>    /// 移除缓存集合    /// </summary>    /// <param name="keys">缓存键集合</param>    /// <param name="cancellationToken">取消令牌</param>    Task RemoveAsync( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default );    /// <summary>    /// 移除缓存集合    /// </summary>    /// <param name="keys">缓存键集合</param>    /// <param name="cancellationToken">取消令牌</param>    Task RemoveAsync( IEnumerable<string> keys, CancellationToken cancellationToken = default );    /// <summary>    /// 通过缓存键前缀移除缓存    /// </summary>    /// <param name="prefix">缓存键前缀</param>    void RemoveByPrefix( string prefix );    /// <summary>    /// 通过缓存键前缀移除缓存    /// </summary>    /// <param name="prefix">缓存键前缀</param>    /// <param name="cancellationToken">取消令牌</param>    Task RemoveByPrefixAsync( string prefix, CancellationToken cancellationToken = default );    /// <summary>    /// 通过缓存键模式移除缓存    /// </summary>    /// <param name="pattern">缓存键模式,范例: test*</param>    void RemoveByPattern( string pattern );    /// <summary>    /// 通过缓存键模式移除缓存    /// </summary>    /// <param name="pattern">缓存键模式,范例: test*</param>    /// <param name="cancellationToken">取消令牌</param>    Task RemoveByPatternAsync( string pattern, CancellationToken cancellationToken = default );    /// <summary>    /// 清空缓存    /// </summary>    void Clear();    /// <summary>    /// 清空缓存    /// </summary>    /// <param name="cancellationToken">取消令牌</param>    Task ClearAsync( CancellationToken cancellationToken = default );}
/// <summary>/// EasyCaching缓存服务/// </summary>public class CacheManager : ICache {
#region 字段
/// <summary> /// 缓存提供器 /// </summary> private readonly IEasyCachingProviderBase _provider; /// <summary> /// 缓存提供器 /// </summary> private readonly IEasyCachingProvider _cachingProvider;
#endregion
#region 构造方法
/// <summary> /// 初始化EasyCaching缓存服务 /// </summary> /// <param name="provider">EasyCaching缓存提供器</param> /// <param name="hybridProvider">EasyCaching 2级缓存提供器</param> public CacheManager( IEasyCachingProvider provider, IHybridCachingProvider hybridProvider = null ) { CachingOptions.Clear(); if ( provider != null ) { _provider = provider; _cachingProvider = provider; } if( hybridProvider != null ) _provider = hybridProvider; _provider.CheckNull( nameof( provider ) ); }
#endregion
#region Exists
/// <inheritdoc /> public bool Exists( CacheKey key ) { key.Validate(); return Exists( key.Key ); }
/// <inheritdoc /> public bool Exists( string key ) { return _provider.Exists( key ); }
#endregion
#region ExistsAsync
/// <inheritdoc /> public async Task<bool> ExistsAsync( CacheKey key, CancellationToken cancellationToken = default ) { key.Validate(); return await ExistsAsync( key.Key, cancellationToken ); }
/// <inheritdoc /> public async Task<bool> ExistsAsync( string key, CancellationToken cancellationToken = default ) { return await _provider.ExistsAsync( key, cancellationToken ); }
#endregion
#region Get
/// <inheritdoc /> public T Get<T>( CacheKey key ) { key.Validate(); return Get<T>( key.Key ); }
/// <inheritdoc /> public T Get<T>( string key ) { var result = _provider.Get<T>( key ); return result.Value; }
/// <inheritdoc /> public List<T> Get<T>( IEnumerable<CacheKey> keys ) { return Get<T>( ToKeys( keys ) ); }
/// <summary> /// 转换为缓存键字符串集合 /// </summary> private IEnumerable<string> ToKeys( IEnumerable<CacheKey> keys ) { keys.CheckNull( nameof( keys ) ); var cacheKeys = keys.ToList(); cacheKeys.ForEach( t => t.Validate() ); return cacheKeys.Select( t => t.Key ); }
/// <inheritdoc /> public List<T> Get<T>( IEnumerable<string> keys ) { Validate(); var result = _cachingProvider.GetAll<T>( keys ); return result.Values.Select( t => t.Value ).ToList(); }
/// <summary> /// 验证 /// </summary> private void Validate() { if ( _cachingProvider == null ) throw new NotSupportedException( "2级缓存不支持该操作" ); }
/// <inheritdoc /> public T Get<T>( CacheKey key, Func<T> action, CacheOptions options = null ) { key.Validate(); return Get( key.Key, action, options ); }
/// <inheritdoc /> public T Get<T>( string key, Func<T> action, CacheOptions options = null ) { var result = _provider.Get( key, action, GetExpiration( options ) ); return result.Value; }
/// <summary> /// 获取过期时间间隔 /// </summary> private TimeSpan GetExpiration( CacheOptions options ) { var result = options?.Expiration; result ??= TimeSpan.FromHours( 8 ); return result.SafeValue(); }
#endregion
#region GetAsync
/// <inheritdoc /> public async Task<object> GetAsync( string key, Type type, CancellationToken cancellationToken = default ) { return await _provider.GetAsync( key, type, cancellationToken ); }
/// <inheritdoc /> public async Task<T> GetAsync<T>( CacheKey key, CancellationToken cancellationToken = default ) { key.Validate(); return await GetAsync<T>( key.Key, cancellationToken ); }
/// <inheritdoc /> public async Task<T> GetAsync<T>( string key, CancellationToken cancellationToken = default ) { var result = await _provider.GetAsync<T>( key, cancellationToken ); return result.Value; }
/// <inheritdoc /> public async Task<List<T>> GetAsync<T>( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default ) { return await GetAsync<T>( ToKeys( keys ), cancellationToken ); }
/// <inheritdoc /> public async Task<List<T>> GetAsync<T>( IEnumerable<string> keys, CancellationToken cancellationToken = default ) { Validate(); var result = await _cachingProvider.GetAllAsync<T>( keys, cancellationToken ); return result.Values.Select( t => t.Value ).ToList(); }
/// <inheritdoc /> public async Task<T> GetAsync<T>( CacheKey key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default ) { key.Validate(); return await GetAsync( key.Key, action, options, cancellationToken ); }
/// <inheritdoc /> public async Task<T> GetAsync<T>( string key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default ) { var result = await _provider.GetAsync( key, action, GetExpiration( options ), cancellationToken ); return result.Value; }
#endregion
#region GetByPrefix
/// <inheritdoc /> public List<T> GetByPrefix<T>( string prefix ) { if( prefix.IsEmpty() ) return new List<T>(); Validate(); return _cachingProvider.GetByPrefix<T>( prefix ).Where( t => t.Value.HasValue ).Select( t => t.Value.Value ).ToList(); }
#endregion
#region GetByPrefixAsync
/// <inheritdoc /> public async Task<List<T>> GetByPrefixAsync<T>( string prefix, CancellationToken cancellationToken = default ) { if( prefix.IsEmpty() ) return new List<T>(); Validate(); var result = await _cachingProvider.GetByPrefixAsync<T>( prefix, cancellationToken ); return result.Where( t => t.Value.HasValue ).Select( t => t.Value.Value ).ToList(); }
#endregion
#region TrySet
/// <inheritdoc /> public bool TrySet<T>( CacheKey key, T value, CacheOptions options = null ) { key.Validate(); return TrySet( key.Key, value, options ); }
/// <inheritdoc /> public bool TrySet<T>( string key, T value, CacheOptions options = null ) { return _provider.TrySet( key, value, GetExpiration( options ) ); }
#endregion
#region TrySetAsync
/// <inheritdoc /> public async Task<bool> TrySetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default ) { key.Validate(); return await TrySetAsync( key.Key, value, options, cancellationToken ); }
/// <inheritdoc /> public async Task<bool> TrySetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default ) { return await _provider.TrySetAsync( key, value, GetExpiration( options ), cancellationToken ); }
#endregion
#region Set
/// <inheritdoc /> public void Set<T>( CacheKey key, T value, CacheOptions options = null ) { key.Validate(); Set( key.Key, value, options ); }
/// <inheritdoc /> public void Set<T>( string key, T value, CacheOptions options = null ) { _provider.Set( key, value, GetExpiration( options ) ); }
/// <inheritdoc /> public void Set<T>( IDictionary<CacheKey, T> items, CacheOptions options = null ) { Set( ToItems( items ), options ); }
/// <summary> /// 转换为缓存项集合 /// </summary> private IDictionary<string, T> ToItems<T>( IDictionary<CacheKey, T> items ) { items.CheckNull( nameof( items ) ); return items.Select( item => { item.Key.Validate(); return new KeyValuePair<string, T>( item.Key.Key, item.Value ); } ).ToDictionary( t => t.Key, t => t.Value ); }
/// <inheritdoc /> public void Set<T>( IDictionary<string, T> items, CacheOptions options = null ) { _provider.SetAll( items, GetExpiration( options ) ); }
#endregion
#region SetAsync
/// <inheritdoc /> public async Task SetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default ) { key.Validate(); await SetAsync( key.Key, value, options, cancellationToken ); }
/// <inheritdoc /> public async Task SetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default ) { await _provider.SetAsync( key, value, GetExpiration( options ), cancellationToken ); }
/// <inheritdoc /> public async Task SetAsync<T>( IDictionary<CacheKey, T> items, CacheOptions options = null, CancellationToken cancellationToken = default ) { await SetAsync( ToItems( items ), options, cancellationToken ); }
/// <inheritdoc /> public async Task SetAsync<T>( IDictionary<string, T> items, CacheOptions options = null, CancellationToken cancellationToken = default ) { await _provider.SetAllAsync( items, GetExpiration( options ), cancellationToken ); }
#endregion
#region Remove
/// <inheritdoc /> public void Remove( CacheKey key ) { key.Validate(); Remove( key.Key ); }
/// <inheritdoc /> public void Remove( string key ) { _provider.Remove( key ); }
/// <inheritdoc /> public void Remove( IEnumerable<CacheKey> keys ) { Remove( ToKeys( keys ) ); }
/// <inheritdoc /> public void Remove( IEnumerable<string> keys ) { _provider.RemoveAll( keys ); }
#endregion
#region RemoveAsync
/// <inheritdoc /> public async Task RemoveAsync( CacheKey key, CancellationToken cancellationToken = default ) { key.Validate(); await RemoveAsync( key.Key, cancellationToken ); }
/// <inheritdoc /> public async Task RemoveAsync( string key, CancellationToken cancellationToken = default ) { await _provider.RemoveAsync( key, cancellationToken ); }
/// <inheritdoc /> public async Task RemoveAsync( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default ) { await RemoveAsync( ToKeys( keys ), cancellationToken ); }
/// <inheritdoc /> public async Task RemoveAsync( IEnumerable<string> keys, CancellationToken cancellationToken = default ) { await _provider.RemoveAllAsync( keys, cancellationToken ); }
#endregion
#region RemoveByPrefix
/// <summary> /// 通过缓存键前缀移除缓存 /// </summary> /// <param name="prefix">缓存键前缀</param> public void RemoveByPrefix( string prefix ) { if( prefix.IsEmpty() ) return; _provider.RemoveByPrefix( prefix ); }
#endregion
#region RemoveByPrefixAsync
/// <inheritdoc /> public async Task RemoveByPrefixAsync( string prefix, CancellationToken cancellationToken = default ) { if( prefix.IsEmpty() ) return; await _provider.RemoveByPrefixAsync( prefix, cancellationToken ); }
#endregion
#region RemoveByPattern
/// <inheritdoc /> public void RemoveByPattern( string pattern ) { if( pattern.IsEmpty() ) return; _provider.RemoveByPattern( pattern ); }
#endregion
#region RemoveByPatternAsync
/// <inheritdoc /> public async Task RemoveByPatternAsync( string pattern, CancellationToken cancellationToken = default ) { if( pattern.IsEmpty() ) return; await _provider.RemoveByPatternAsync( pattern, cancellationToken ); }
#endregion
#region Clear
/// <inheritdoc /> public void Clear() { Validate(); _cachingProvider.Flush(); }
#endregion
#region ClearAsync
/// <inheritdoc /> public async Task ClearAsync( CancellationToken cancellationToken = default ) { Validate(); await _cachingProvider.FlushAsync( cancellationToken ); }
#endregion}
复制代码

CacheKey 缓存键

通过继承 CacheKey 创建自定义缓存键对象,可以封装缓存键的构造细节.


/// <summary>/// 缓存键/// </summary>public class CacheKey {    /// <summary>    /// 缓存键    /// </summary>    private string _key;
/// <summary> /// 初始化缓存键 /// </summary> public CacheKey() { }
/// <summary> /// 初始化缓存键 /// </summary> /// <param name="key">缓存键</param> /// <param name="parameters">缓存键参数</param> public CacheKey( string key,params object[] parameters) { _key = string.Format( key, parameters ); }
/// <summary> /// 缓存键 /// </summary> public string Key { get => ToString(); set => _key = value; }
/// <summary> /// 缓存键前缀 /// </summary> public string Prefix { get; set; }
/// <summary> /// 获取缓存键 /// </summary> public override string ToString() { return $"{Prefix}{_key}"; }}
复制代码

CacheAttribute 缓存拦截器

[Cache] 缓存拦截器提供了缓存操作的快捷方式.


/// <summary>/// 缓存拦截器/// </summary>public class CacheAttribute : InterceptorBase {    /// <summary>    /// 缓存键前缀,可使用占位符, {0} 表示第一个参数值,范例: User-{0}    /// </summary>    public string Prefix { get; set; }    /// <summary>    /// 缓存过期间隔,单位:秒,默认值:36000    /// </summary>    public int Expiration { get; set; } = 36000;
/// <summary> /// 执行 /// </summary> public override async Task Invoke( AspectContext context, AspectDelegate next ) { var cache = GetCache( context ); var returnType = GetReturnType( context ); var key = CreateCacheKey( context ); var value = await GetCacheValue( cache, returnType, key ); if( value != null ) { SetReturnValue( context, returnType, value ); return; } await next( context ); await SetCache( context, cache, key ); }
/// <summary> /// 获取缓存服务 /// </summary> protected virtual ICache GetCache( AspectContext context ) { return context.ServiceProvider.GetService<ICache>(); }
/// <summary> /// 获取返回类型 /// </summary> private Type GetReturnType( AspectContext context ) { return context.IsAsync() ? context.ServiceMethod.ReturnType.GetGenericArguments().First() : context.ServiceMethod.ReturnType; }
/// <summary> /// 创建缓存键 /// </summary> private string CreateCacheKey( AspectContext context ) { var keyGenerator = context.ServiceProvider.GetService<ICacheKeyGenerator>(); return keyGenerator.CreateCacheKey( context.ServiceMethod, context.Parameters, GetPrefix( context ) ); }
/// <summary> /// 获取缓存键前缀 /// </summary> private string GetPrefix( AspectContext context ) { try { return string.Format( Prefix, context.Parameters.ToArray() ); } catch { return Prefix; } }
/// <summary> /// 获取缓存值 /// </summary> private async Task<object> GetCacheValue( ICache cache, Type returnType, string key ) { return await cache.GetAsync( key, returnType ); }
/// <summary> /// 设置返回值 /// </summary> private void SetReturnValue( AspectContext context, Type returnType, object value ) { if( context.IsAsync() ) { context.ReturnValue = typeof( Task ).GetMethods() .First( p => p.Name == "FromResult" && p.ContainsGenericParameters ) .MakeGenericMethod( returnType ).Invoke( null, new[] { value } ); return; } context.ReturnValue = value; }
/// <summary> /// 设置缓存 /// </summary> private async Task SetCache( AspectContext context, ICache cache, string key ) { var options = new CacheOptions { Expiration = TimeSpan.FromSeconds( Expiration ) }; var returnValue = context.IsAsync() ? await context.UnwrapAsyncReturnValue() : context.ReturnValue; await cache.SetAsync( key, returnValue, options ); }}
复制代码

LocalCacheAttribute 本地缓存拦截器

/// <summary>/// 本地缓存拦截器/// </summary>public class LocalCacheAttribute : CacheAttribute {    /// <summary>    /// 获取缓存服务    /// </summary>    protected override ICache GetCache( AspectContext context ) {        return context.ServiceProvider.GetService<ILocalCache>();    }}
复制代码

RedisCacheAttribute Redis 缓存拦截器

/// <summary>/// Redis缓存拦截器/// </summary>public class RedisCacheAttribute : CacheAttribute {    /// <summary>    /// 获取缓存服务    /// </summary>    protected override ICache GetCache( AspectContext context ) {        return context.ServiceProvider.GetService<IRedisCache>();    }}
复制代码


用户头像

何镇汐

关注

15年以上.Net开发经验,擅长代码封装 2023-11-01 加入

15年以上.Net开发经验,擅长代码封装,主要作品为Util应用框架

评论

发布
暂无评论
Util应用框架基础(七)- 缓存_开源_何镇汐_InfoQ写作社区