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

发布时间 2023-11-21 11:34:40作者: 何镇汐

本节介绍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 包.

基础用法

配置缓存

配置本地缓存

  • 引用Nuget包

    Nuget包名: Util.Caching.EasyCaching

  • AddMemoryCache

    使用 AddMemoryCache 扩展方法启用本地缓存.

    • 默认配置不带参数,设置以下默认值:

      • MaxRdSecond 设置为 1200秒.

      • CacheNulls 设置为 true.

      var builder = WebApplication.CreateBuilder( args );
      builder.AsBuild().AddMemoryCache();
      
    • 使用 IConfiguration 进行配置.

      可以使用 appsettings.json 文件进行配置.

      var builder = WebApplication.CreateBuilder( args );
      builder.AsBuild().AddMemoryCache( builder.Configuration );
      

      默认配置节: EasyCaching:Memory

      appsettings.json 配置文件示例.

      {
        "EasyCaching": {
          "Memory": {
            "MaxRdSecond": 1200,
            "CacheNulls": true
          }
        }
      }
      
    • 使用委托进行配置.

      var builder = WebApplication.CreateBuilder( args );
      builder.AsBuild().AddMemoryCache( options => {
          options.MaxRdSecond = 1200;
          options.CacheNulls = true;
      } );
      

配置Redis缓存

  • 引用Nuget包

    Nuget包名: Util.Caching.EasyCaching

  • AddRedisCache

    使用 AddRedisCache 扩展方法启用Redis缓存.

    • 最简单的配置方法只需传入Redis服务地址,并设置以下默认值.

      • MaxRdSecond 设置为 1200秒.

      • CacheNulls 设置为 true.

      • AllowAdmin 设置为 true.

      • 端口设置为 6379.

      • SerializerName 设置为 "SystemTextJson".

      范例:

      var builder = WebApplication.CreateBuilder( args );
      builder.AsBuild().AddRedisCache( "127.0.0.1" );
      

      如果要修改端口为 6666,如下所示.

      builder.AsBuild().AddRedisCache( "127.0.0.1",6666 );
      

      还可以统一设置缓存键前缀,下面的示例将缓存键前缀设置为 "test:".

      builder.AsBuild().AddRedisCache( "127.0.0.1",6666,"test:" );
      
    • 使用 IConfiguration 进行配置.

      可以使用 appsettings.json 文件进行配置.

      builder.AsBuild().AddRedisCache( builder.Configuration );
      

      默认配置节: EasyCaching:Redis

      appsettings.json 配置文件示例.

      {
        "EasyCaching": {
          "Redis": {
            "MaxRdSecond": 1200,
            "CacheNulls": true,
            "DbConfig": {
              "AllowAdmin": true,
              "Endpoints": [
                {
                  "Host": "localhost",
                  "Port": 6739
                }
              ],
              "Database": 0
            }
          }
        }
      }
      
    • 使用委托进行配置.

      builder.AsBuild().AddRedisCache( options => {
          options.MaxRdSecond = 1200;
          options.CacheNulls = true;        
          options.DBConfig.AllowAdmin = true;
          options.DBConfig.KeyPrefix = "test:";
          options.DBConfig.Endpoints.Add( new ServerEndPoint( "127.0.0.1", 6379 ) );
      } );
      

配置二级缓存

  • 引用Nuget包

    Nuget包名: Util.Caching.EasyCaching

  • AddHybridCache

    使用 AddHybridCache 扩展方法启用2级缓存.

    • 最简单的配置方法不带参数,设置以下默认值.

      • TopicName 设置为 EasyCachingHybridCache.

        TopicName 是Redis总线发布事件的主题名称.

      启用2级缓存之前,应先配置本地缓存和Redis缓存.

      范例:

      var builder = WebApplication.CreateBuilder( args );
      builder.AsBuild()
        .AddMemoryCache()
        .AddRedisCache( "127.0.0.1" )
        .AddHybridCache();
      

      如果要修改 TopicName,传入主题参数,如下所示.

      builder.AsBuild()
        .AddMemoryCache()
        .AddRedisCache( "127.0.0.1" )
        .AddHybridCache( "topic" );
      
    • 使用 IConfiguration 进行配置.

      可以使用 appsettings.json 文件进行配置.

      builder.AsBuild()
        .AddMemoryCache()
        .AddRedisCache( "127.0.0.1" )
        .AddHybridCache( builder.Configuration );
      

      除了需要配置2级缓存提供程序,还需要配置 Redis 总线.

      默认配置节名称:

      • 2级缓存默认配置节名称: EasyCaching:Hybrid

      • Redis总线配置节名称: EasyCaching:RedisBus

      appsettings.json 配置文件示例.

      {
        "EasyCaching": {
          "Hybrid": {
            "LocalCacheProviderName": "DefaultInMemory",
            "DistributedCacheProviderName": "DefaultRedis",
            "TopicName": "EasyCachingHybridCache"
          },
          "RedisBus": {
            "Endpoints": [
              {
                "Host": "localhost",
                "Port": 6739
              }
            ],
            "SerializerName": "SystemTextJson"
          }
        }
      }
      
    • 使用委托进行配置.

      builder.AsBuild()
        .AddMemoryCache()
        .AddRedisCache( "127.0.0.1" )
        .AddHybridCache( hybridOptions => {
          hybridOptions.LocalCacheProviderName = "DefaultInMemory";
          hybridOptions.DistributedCacheProviderName = "DefaultRedis";
          hybridOptions.TopicName = "topic";
        }, redisBusOptions => {
            redisBusOptions.Endpoints.Add( new ServerEndPoint( "127.0.0.1", 6379 ) );
            redisBusOptions.SerializerName = "SystemTextJson";
        } )
      
  • 配置参数

    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.

Util.Caching.ICache 是缓存操作的主要接口.

根据缓存配置,ICache可以在本地缓存,Redis缓存,2级缓存切换.

  • 如果仅配置本地缓存, ICache实例为本地缓存操作.

  • 如果仅配置 Redis 缓存,ICache实例为Redis缓存操作.

  • 如果同时配置本地缓存和 Redis 缓存,ICache 实例为后配置的缓存操作.

  • 如果配置了2级缓存,ICache 实例为2级缓存操作.

注意事项

如果使用2级缓存,有些操作不可用,调用会抛出异常.

示例上下文

  • 通过依赖注入获取 ICache 实例.

    public class Service : IService {
      private ICache _cache;
      private IUserResourceRepository _repository;
    
      public Service( ICache cache,IUserResourceRepository repository ) {
          _cache = cache;
          _repository = repository;
      }
    }
    
  • 用户资源示例

    public class UserResource {
        public string UserId { get; set; }
        public string UserName { get; set; }
        public string ResourceId { get; set; }
        public string ResourceName { get; set; }
    }
    
  • 用户资源缓存键示例

    public class UserResourceCacheKey : CacheKey {
        public UserResourceCacheKey( string userId,string resourceId ) {
            Prefix = $"User-{userId}:";
            Key = $"Resource-{resourceId}";
        }
    }
    
  • 用户资源仓储示例

    public interface IUserResourceRepository {
      UserResource GetUserResource( string userId, string resourceId );
      Task<UserResource> GetUserResourceAsync( string userId, string resourceId );
    }
    

API

  • Exists

    功能: 判断缓存是否存在

    • bool Exists( CacheKey key )

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      bool exists = _cache.Exists( cacheKey );
      
    • bool Exists( string key )

      范例:

      bool exists = _cache.Exists( "User-1:Resource-2" );
      
  • ExistsAsync

    功能: 判断缓存是否存在

    • Task<bool> ExistsAsync( CacheKey key, CancellationToken cancellationToken = default )

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      bool exists = await _cache.ExistsAsync( cacheKey );
      
    • Task<bool> ExistsAsync( string key, CancellationToken cancellationToken = default )

      范例:

      bool exists = await _cache.ExistsAsync( "User-1:Resource-2" );
      
  • Get

    功能: 从缓存中获取数据

    • T Get<T>( CacheKey key )

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = _cache.Get<UserResource>( cacheKey );
      
    • T Get<T>( string key )

      范例:

      var result = _cache.Get<UserResource>( "User-1:Resource-2" );
      
    • List<T> Get<T>( IEnumerable<CacheKey> keys )

      通过缓存键集合获取结果集合.

      范例:

      var keys = new List<UserResourceCacheKey> { new ( "1", "2" ), new( "3", "4" ) };
      var result = _cache.Get<UserResource>( keys );
      
    • List<T> Get<T>( IEnumerable<string> keys )

      通过缓存键集合获取结果集合.

      范例:

      var keys = new List<string> { "User-1:Resource-2", "User-3:Resource-4" };
      var result = _cache.Get<UserResource>( keys );
      
    • T Get<T>( CacheKey key, Func<T> action, CacheOptions options = null )

      从缓存中获取数据,如果数据不存在,则执行获取数据操作并添加到缓存中.

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = _cache.Get( cacheKey,()=> _repository.GetUserResource( "1", "2" ) );
      

      CacheOptions 配置包含 Expiration 属性,用于设置缓存过期时间间隔,默认值: 8小时.

      设置1小时过期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = _cache.Get( cacheKey, () => _repository.GetUserResource( "1", "2" ), new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • T Get<T>( string key, Func<T> action, CacheOptions options = null )

      从缓存中获取数据,如果数据不存在,则执行获取数据操作并添加到缓存中.

      范例:

      var result = _cache.Get( "User-1:Resource-2",()=> _repository.GetUserResource( "1", "2" ) );
      
  • GetAsync

    功能: 从缓存中获取数据

    • Task<object> GetAsync( string key, Type type, CancellationToken cancellationToken = default )

      无法传入泛型返回类型参数可使用该重载方法.

      范例:

      object result = await _cache.GetAsync( "User-1:Resource-2", typeof( UserResource ) );
      
    • Task<T> GetAsync<T>( CacheKey key, CancellationToken cancellationToken = default )

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = await _cache.GetAsync<UserResource>( cacheKey );
      
    • Task<T> GetAsync<T>( string key, CancellationToken cancellationToken = default )

      范例:

      var result = await _cache.GetAsync<UserResource>( "User-1:Resource-2" );
      
    • Task<List> GetAsync<T>( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default )

      通过缓存键集合获取结果集合.

      范例:

      var keys = new List<UserResourceCacheKey> { new ( "1", "2" ), new( "3", "4" ) };
      var result = await _cache.GetAsync<UserResource>( keys );
      
    • Task<List> GetAsync<T>( IEnumerable<string> keys, CancellationToken cancellationToken = default )

      通过缓存键集合获取结果集合.

      范例:

      var keys = new List<string> { "User-1:Resource-2", "User-3:Resource-4" };
      var result = await _cache.GetAsync<UserResource>( keys );
      
    • Task<T> GetAsync<T>( CacheKey key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default )

      从缓存中获取数据,如果数据不存在,则执行获取数据操作并添加到缓存中.

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = await _cache.GetAsync( cacheKey,async ()=> await _repository.GetUserResourceAsync( "1", "2" ) );
      

      CacheOptions 配置包含 Expiration 属性,用于设置缓存过期时间间隔,默认值: 8小时.

      设置1小时过期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var result = await _cache.GetAsync( cacheKey,async ()=> await _repository.GetUserResourceAsync( "1", "2" ), new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • Task<T> GetAsync<T>( string key, Func<Task<T>> action, CacheOptions options = null, CancellationToken cancellationToken = default )

      从缓存中获取数据,如果数据不存在,则执行获取数据操作并添加到缓存中.

      范例:

      var result = await _cache.GetAsync( "User-1:Resource-2",async ()=> await _repository.GetUserResourceAsync( "1", "2" ) );
      
  • GetByPrefix

    功能: 通过缓存键前缀获取数据

    • List<T> GetByPrefix<T>( string prefix )

      范例:

      var result = _cache.GetByPrefix<UserResource>( "User-1" );
      
  • GetByPrefixAsync

    功能: 通过缓存键前缀获取数据

    • Task<List<T>> GetByPrefixAsync<T>( string prefix, CancellationToken cancellationToken = default )

      范例:

      var result = await _cache.GetByPrefixAsync<UserResource>( "User-1" );
      
  • TrySet

    功能: 设置缓存,当缓存已存在则忽略,设置成功返回true

    • bool TrySet<T>( CacheKey key, T value, CacheOptions options = null )

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = _repository.GetUserResource( "1", "2" );
      var result = _cache.TrySet( cacheKey, value );
      

      CacheOptions 配置包含 Expiration 属性,用于设置缓存过期时间间隔,默认值: 8小时.

      设置1小时过期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = _repository.GetUserResource( "1", "2" );
      var result = _cache.TrySet( cacheKey, value, new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • bool TrySet<T>( string key, T value, CacheOptions options = null )

      范例:

      var value = _repository.GetUserResource( "1", "2" );
      var result = _cache.TrySet( "User-1:Resource-2", value );
      
  • TrySetAsync

    功能: 设置缓存,当缓存已存在则忽略,设置成功返回true

    • Task<bool> TrySetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default )

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = await _repository.GetUserResourceAsync( "1", "2" );
      var result = await _cache.TrySetAsync( cacheKey, value );
      

      CacheOptions 配置包含 Expiration 属性,用于设置缓存过期时间间隔,默认值: 8小时.

      设置1小时过期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = await _repository.GetUserResourceAsync( "1", "2" );
      var result = await _cache.TrySetAsync( cacheKey, value, new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • Task<bool> TrySetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default )

      范例:

      var value = await _repository.GetUserResourceAsync( "1", "2" );
      var result = await _cache.TrySetAsync( "User-1:Resource-2", value );
      
  • Set

    功能: 设置缓存,当缓存已存在则覆盖

    • void Set<T>( CacheKey key, T value, CacheOptions options = null )

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = _repository.GetUserResource( "1", "2" );
      _cache.Set( cacheKey, value );
      

      CacheOptions 配置包含 Expiration 属性,用于设置缓存过期时间间隔,默认值: 8小时.

      设置1小时过期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = _repository.GetUserResource( "1", "2" );
      _cache.Set( cacheKey, value, new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • void Set<T>( string key, T value, CacheOptions options = null )

      范例:

      var value = _repository.GetUserResource( "1", "2" );
      _cache.Set( "User-1:Resource-2", value );
      
    • void Set<T>( IDictionary<CacheKey,T> items, CacheOptions options = null )

      范例:

      var items = new Dictionary<CacheKey, UserResource> {
          { new UserResourceCacheKey( "1", "2" ), _repository.GetUserResource( "1", "2" ) },
          { new UserResourceCacheKey( "3", "4" ), _repository.GetUserResource( "3", "4" ) }
      };
      _cache.Set( items );
      
    • void Set<T>( IDictionary<string,T> items, CacheOptions options = null )

      范例:

      var items = new Dictionary<string, UserResource> {
          { "User-1:Resource-2", _repository.GetUserResource( "1", "2" ) },
          { "User-3:Resource-4", _repository.GetUserResource( "3", "4" ) }
      };
      _cache.Set( items );
      
  • SetAsync

    功能: 设置缓存,当缓存已存在则覆盖

    • Task SetAsync<T>( CacheKey key, T value, CacheOptions options = null, CancellationToken cancellationToken = default )

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = await _repository.GetUserResourceAsync( "1", "2" );
      await _cache.SetAsync( cacheKey, value );
      

      CacheOptions 配置包含 Expiration 属性,用于设置缓存过期时间间隔,默认值: 8小时.

      设置1小时过期,如下所示.

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      var value = await _repository.GetUserResourceAsync( "1", "2" );
      await _cache.SetAsync( cacheKey, value, new CacheOptions { Expiration = TimeSpan.FromHours( 1 ) } );
      
    • Task SetAsync<T>( string key, T value, CacheOptions options = null, CancellationToken cancellationToken = default )

      范例:

      var value = await _repository.GetUserResourceAsync( "1", "2" );
      await _cache.SetAsync( "User-1:Resource-2", value );
      
    • Task SetAsync<T>( IDictionary<CacheKey, T> items, CacheOptions options = null, CancellationToken cancellationToken = default )

      范例:

      var items = new Dictionary<CacheKey, UserResource> {
          { new UserResourceCacheKey( "1", "2" ), await _repository.GetUserResourceAsync( "1", "2" ) },
          { new UserResourceCacheKey( "3", "4" ), await _repository.GetUserResourceAsync( "3", "4" ) }
      };
      await _cache.SetAsync( items );
      
    • Task SetAsync<T>( IDictionary<string, T> items, CacheOptions options = null, CancellationToken cancellationToken = default )

      范例:

      var items = new Dictionary<string, UserResource> {
          { "User-1:Resource-2", await _repository.GetUserResourceAsync( "1", "2" ) },
          { "User-3:Resource-4", await _repository.GetUserResourceAsync( "3", "4" ) }
      };
      await _cache.SetAsync( items );
      
  • Remove

    功能: 移除缓存

    • void Remove( CacheKey key )

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      _cache.Remove( cacheKey );
      
    • void Remove( string key )

      范例:

      _cache.Remove( "User-1:Resource-2" );
      
    • void Remove( IEnumerable<CacheKey> keys )

      范例:

      var keys = new List<UserResourceCacheKey> { new ( "1", "2" ), new( "3", "4" ) };
      _cache.Remove( keys );
      
    • void Remove( IEnumerable<string> keys )

      范例:

      var keys = new List<string> { "User-1:Resource-2", "User-3:Resource-4" };
      _cache.Remove( keys );
      
  • RemoveAsync

    功能: 移除缓存

    • Task RemoveAsync( CacheKey key, CancellationToken cancellationToken = default )

      范例:

      var cacheKey = new UserResourceCacheKey( "1", "2" );
      await _cache.RemoveAsync( cacheKey );
      
    • Task RemoveAsync( string key, CancellationToken cancellationToken = default )

      范例:

      await _cache.RemoveAsync( "User-1:Resource-2" );
      
    • Task RemoveAsync( IEnumerable<CacheKey> keys, CancellationToken cancellationToken = default )

      范例:

      var keys = new List<UserResourceCacheKey> { new( "1", "2" ), new( "3", "4" ) };
      await _cache.RemoveAsync( keys );
      
    • Task RemoveAsync( IEnumerable<string> keys, CancellationToken cancellationToken = default )

      范例:

      var keys = new List<string> { "User-1:Resource-2", "User-3:Resource-4" };
      await _cache.RemoveAsync( keys );
      
  • RemoveByPrefix

    功能: 通过缓存键前缀移除缓存

    • void RemoveByPrefix( string prefix )

      范例:

      _cache.RemoveByPrefix( "User-1" );
      
    • Task RemoveByPrefixAsync( string prefix, CancellationToken cancellationToken = default )

      范例:

      await _cache.RemoveByPrefixAsync( "User-1" );
      
  • RemoveByPattern

    功能: 通过模式移除缓存

    • void RemoveByPattern( string pattern )

      范例:

      移除 User 开头的缓存.

      _cache.RemoveByPattern( "User*" );
      
  • RemoveByPatternAsync

    功能: 通过模式移除缓存

    • Task RemoveByPatternAsync( string pattern, CancellationToken cancellationToken = default )

      范例:

      移除 User 开头的缓存.

      await _cache.RemoveByPatternAsync( "User*" );
      
  • Clear

    功能: 清空缓存

    • void Clear()

      范例:

      _cache.Clear();
      
  • ClearAsync

    功能: 清空缓存

    • Task ClearAsync( CancellationToken cancellationToken = default )

      范例:

      await _cache.ClearAsync();
      

ILocalCache

Util.Caching.ILocalCache 从 ICache 派生,表示本地缓存.

当同时配置本地缓存和Redis缓存, 如果你想明确使用本地缓存, 请使用 ILocalCache.

Api 参考 ICache.

IRedisCache

Util.Caching.IRedisCache 从 ICache 派生,表示 Redis 分布式缓存.

当同时配置本地缓存和Redis缓存, 如果你想明确使用 Redis 缓存, 请使用 IRedisCache.

IRedisCache 除了继承基础缓存操作外,还将添加 Redis 专用缓存操作.

目前 IRedisCache 尚未添加 Redis 专用操作,后续根据需要进行添加.

Api 参考 ICache.

更新缓存

  • 设置缓存到期时间

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

    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>();
    }
}