Skip to content

缓存拦截(AOP)

Catcher Wong edited this page Oct 5, 2019 · 1 revision

为什么我们需要做缓存拦截?

我们先来看看下面这段简单的代码。

public Product GetProduct(int id)  
{  
    string cacheKey = $"product:{id}";  
      
    var val = _cache.Get<Product>(cacheKey);  
      
    if(val != null)  
        return val;  
      
    val = _db.GetProduct(id);  
      
    if(val != null)  
        _cache.Set<Product>(cacheKey, val, expiration);  
          
    return val;  
} 

其实这个方法做的事情很简单:

先从缓存中取出对应id的产品信息,如果能从缓存中取出来,就直接把缓存的数据返回出去,反之,就去数据库中查找对应的产品信息。

如果能在数据库中成功找到这个产品信息,就要把它丢进缓存中。逻辑就这些。

相信大部分人都写过类似的代码,思路也很清晰的表达出来了。

不过,写一两个这样的方法,或许还能接受,多了就会觉得很麻烦,然后就少不了CV大法了,结果就是随处可见的ifelse,这也就是为什么EasyCaching要提供一个这样的功能的主要原因。

举个日志的例子,相信大家就更加清晰了,方法的入参和出参都打印出来,我们肯定不可以一个方法一个方法的去加。

这里引入了切面编程的概念!

EasyCaching提供了3种缓存拦截的策略

EasyCaching把CURD这4个常见的操作拆解成3种策略。

  • Able
  • Put
  • Evict

其中,Able的行为就和上面的例子是一样的,就是先查后写,也就是我们常说的CURD中的C和R。

Able策略适用于大部分查询操作。实时性要求很高的要慎用或禁止使用!

下面来看看Put和Evict这两个策略。

Put策略,对应的就是CURD中的U(更新)操作。

如果对某个数据进行了修改操作,理当也要更新其对应的缓存数据。如果是更新频繁的数据,不建议使用!

Evict策略,对应的就是CURD中的D(删除)操作。

如果对某个数据进行了删除操作,理当也要清理其对应的缓存数据。

这就是三个策略的意义和作用。

EasyCaching的实现

缓存拦截有下面三个子模块。

  1. 缓存Key的生成规则
  2. 拦截的规则
  3. 拦截的配置及操作

缓存Key的生成规则

这里的Key是根据要拦截的方法的相关信息(方法,参数)和自定义前缀等来进行自动生成的。

对于一些基础的数据类型会直接把它转化成字符串后进行拼接。

对于复杂类型,像自定义的类,EasyCaching提供了一个ICachable接口,可以让用户自己定义生成这个类的缓存Key。

public interface ICachable
{
    string CacheKey { get; }
}

拦截的规则

Attribute是拦截的重要组成部分,对三种策略,提供了三个Attribute。

  • EasyCachingAble
  • EasyCachingPut
  • EasyCachingEvict

要想拦截这个方法,只需要在其接口上面添加上面三个Attribute或者是继承了它们的一些Attribute。

示例:

 public interface IDemoService 
 {
    [EasyCachingAble(Expiration = 10)]
    string GetCurrentUtcTime();
 }

下面是一些可以用的参数说明

属性 描述 适用策略
CacheKeyPrefix 指定生成缓存Key的前缀 All
CacheProviderName 指定要使用那个Provider All
IsHighAvailability 指定是否要“高可用”(操作缓存出错,是否直接抛异常) All
Expiration 缓存绝对过期时间,单位是秒 Able 和 Put
IsAll 是否要删除以CacheKeyPrefix开头的所有缓存 Evict
IsBefore 在执行方法之前删除缓存还是指定方法之后 Evict

这里要注意一点,如果要使用Put和Evict策略,尽可能要指定CacheKeyPrefix,不然会导致无法正确更新或删除缓存。这个是取决于生成缓存Key的实现,指定了CacheKeyPrefix会忽略方法信息。

拦截的配置

目前EasyCaching只有一个CacheProviderName的整体配置,主要是用于指定要用那个Provider,会被Attribute的属性覆盖。

EasyCaching目前提供了两种实现,一种基于AspectCore,另一种基于Castle+Autofac.Extras.DynamicProxy

EasyCaching的使用

在使用上,要区.NET Core 2.x 还是.NET Core 3.0。主要是因为 ConfigureServices方法就不支持直接返回System.IServiceProvider。

下面来看看要怎么使用。

.NET Core 3.0

需要使用0.8.0版本以上的EasyCaching!

首先要在Program上面添加UseServiceProviderFactory的使用

// for aspcectcore
using AspectCore.Extensions.DependencyInjection;
// for castle
using Autofac.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            })
            // for aspcectcore
            .UseServiceProviderFactory(new AspectCoreServiceProviderFactory())
            //// for castle
            //.UseServiceProviderFactory(new AutofacServiceProviderFactory())
        ;
}

其次要在Startup里加入ConfigureContainer方法

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IAspectCoreService, AspectCoreService>();

        services.AddEasyCaching(options =>
        {
            options.UseInMemory("m1");
        });

        services.AddControllers();

        // 1 AspectCore
        services.ConfigureAspectCoreInterceptor(options => options.CacheProviderName = "m1");
        services.AddTransient<ICastleService, CastleService>();
        // 2 Castle  
        services.ConfigureCastleInterceptor(options => options.CacheProviderName = "m1");
    }

    #region ConfigureContainer方法只能有一个,并且它也只能带有一个参数!
    // for aspectcore
    public void ConfigureContainer(IServiceContainer builder)
    {
        builder.ConfigureAspectCoreInterceptor();
    }
    
     //// for castle
     //public void ConfigureContainer(ContainerBuilder builder)
     //{
     //    builder.ConfigureCastleInterceptor();
     //} 
     #endregion

     public void Configure(IApplicationBuilder app)
     {           
         app.UseRouting();
         app.UseEndpoints(endpoints =>
         {
            endpoints.MapControllers();
         });
     }
}

接口方法的定义

public interface IAspectCoreService 
{
    [EasyCachingAble(Expiration = 10)]
    string GetCurrentUtcTime();

    [EasyCachingPut(CacheKeyPrefix = "AspectCore")]
    string PutSomething(string str);

    [EasyCachingEvict(IsBefore = true)]
    void DeleteSomething(int id);   

    // ...  
}

控制器上面的调用

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly IAspectCoreService _aService;
    private readonly ICastleService _cService;

    public ValuesController(
        IAspectCoreService aService = null
        , ICastleService cService = null
    )
    {
        this._aService = aService;
        this._cService = cService;
    }

    [HttpGet]       
    public string Get()
    {
        _aService.xxxx

        _cService.xxxx
            
        return "ok";
    }
}

.NET Core 2.x

0.8.0版本以上的EasyCaching不再支持.NET Core 2.x了

在Startup中进行配置

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }
        
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<IAspectCoreService, AspectCoreService>();

        services.AddEasyCaching(options =>
        {
            options.UseInMemory("m1");
        });

        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

        services.AddTransient<ICastleService, CastleService>();

        // 1 AspectCore
        return services.ConfigureAspectCoreInterceptor(options => options.CacheProviderName = "m1");
        //  // 2 Castle  
        // return services.ConfigureCastleInterceptor(options => options.CacheProviderName = "m1");
    }
        
    public void Configure(IApplicationBuilder app)
    {           
        app.UseMvc();
    }
}

接口中的方法定义和在控制器中的使用同.NET Core 3.0一致,这里就不在累赘了!