<h2 id="前言">前言
关于缓存的使用,相信大家都是熟悉的不能再熟悉了,简单来说就是下面一句话。
优先从缓存中取数据,缓存中取不到再去数据库中取,取到了在扔进缓存中去。
然后我们就会看到项目中有类似这样的代码了。
public Product Get(int productId) { var product = _cache.Get($"Product_{productId}");if(product == null) { product = Query(productId); _cache.Set($"Product_{productId}",product,10); } return product;
}
然而在初期,没有缓存的时候,可能这个方法就一行代码。
public Product Get(int productId) { return Query(productId); }随着业务的不断发展,可能会出现越来越多类似第一段的示例代码。这样就会出现大量“重复的代码”了!
显然,我们不想让这样的代码到处都是!
基于这样的情景下,我们完全可以使用AOP去简化缓存这一部分的代码。
大致的思路如下 :
在某个有返回值的方法执行前去判断缓存中有没有数据,有就直接返回了;
如果缓存中没有的话,就是去执行这个方法,拿到返回值,执行完成之后,把对应的数据写到缓存中去,
下面就根据这个思路来实现。
本文分别使用了Castle和AspectCore来进行演示。
这里主要是做了做了两件事
- 自动处理缓存的key,避免硬编码带来的坑
- 通过Attribute来简化缓存操作
下面就先从Castle开始吧!
一般情况下,我都会配合Autofac来实现,所以这里也不例外。
我们先新建一个ASP.NET Core 2.0的项目,通过Nuget添加下面几个包(当然也可以直接编辑csproj来完成的)。
然后做一下前期准备工作
1.缓存的使用
定义一个ICachingProvider和其对应的实现类MemoryCachingProvider
简化了一下定义,就留下读和取的操作。
public interface ICachingProvider { object Get(string cacheKey);void Set(string cacheKey,object cacheValue,TimeSpan absoluteExpirationRelativeToNow);
}
public class MemoryCachingProvider : ICachingProvider
{
private IMemoryCache _cache;public MemoryCachingProvider(IMemoryCache cache) { _cache = cache; } public object Get(string cacheKey) { return _cache.Get(cacheKey); } public void Set(string cacheKey,TimeSpan absoluteExpirationRelativeToNow) { _cache.Set(cacheKey,cacheValue,absoluteExpirationRelativeToNow); }
}
2.定义一个Attribute
这个Attribute就是我们使用时候的关键了,把它添加到要缓存数据的方法中,即可完成缓存的操作。
这里只用了一个绝对过期时间(单位是秒)来作为演示。如果有其他缓存的配置,也是可以往这里加的。
[AttributeUsage(AttributeTargets.Method,Inherited = true)] public class QCachingAttribute : Attribute { public int AbsoluteExpiration { get; set; } = 30;//add other settings ...
}
3.定义一个空接口
这个空接口只是为了做一个标识的作用,为了后面注册类型而专门定义的。
public interface IQCaching { }4.定义一个与缓存键相关的接口
定义这个接口是针对在方法中使用了自定义类的时候,识别出这个类对应的缓存键。
public interface IQCachable { string CacheKey { get; } }准备工作就这4步(AspectCore中也是要用到的),
下面我们就是要去做方法的拦截了(拦截器)。
拦截器首先要继承并实现IInterceptor这个接口。
public class QCachingInterceptor : IInterceptor { private ICachingProvider _cacheProvider;public QCachingInterceptor(ICachingProvider cacheProvider) { _cacheProvider = cacheProvider; } public void Intercept(IInvocation invocation) { var qCachingAttribute = this.GetQCachingAttributeInfo(invocation.MethodInvocationTarget ?? invocation.Method); if (qCachingAttribute != null) { ProceedCaching(invocation,qCachingAttribute); } else { invocation.Proceed(); } }
}
有两点要注意:
- 因为要使用缓存,所以这里需要我们前面定义的缓存操作接口,并且在构造函数中进行注入。
- Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义。
Intercept方法其实很简单,获取一下当前执行方法是不是有我们前面自定义的QCachingAttribute,有的话就去处理缓存,没有的话就是仅执行这个方法而已。
下面揭开ProceedCaching方法的面纱。
private void ProceedCaching(IInvocation invocation,QCachingAttribute attribute) { var cacheKey = GenerateCacheKey(invocation);var cacheValue = _cacheProvider.Get(cacheKey); if (cacheValue != null) { invocation.ReturnValue = cacheValue; return; } invocation.Proceed(); if (!string.IsNullOrWhiteSpace(cacheKey)) { _cacheProvider.Set(cacheKey,invocation.ReturnValue,TimeSpan.FromSeconds(attribute.AbsoluteExpiration)); }
}
这个方法,就是和大部分操作缓存的代码一样的写法了!
注意下面几个地方
invocation.Proceed()
表示执行当前的方法invocation.ReturnValue
是要执行后才会有值的。- 在每次执行前,都会依据当前执行的方法去生成一个缓存的键。
下面来看看生成缓存键的操作。
这里生成的依据是当前执行方法的名称,参数以及该方法所在的类名。
生成的代码如下:
private string GenerateCacheKey(IInvocation invocation) { var typeName = invocation.TargetType.Name; var methodName = invocation.Method.Name; var methodArguments = this.FormatArgumentsToPartOfCacheKey(invocation.Arguments);return this.GenerateCacheKey(typeName,methodName,methodArguments);
}
//拼接缓存的键
private string GenerateCacheKey(string typeName,string methodName,IListparameters)
{
var builder = new StringBuilder();builder.Append(typeName); builder.Append(_linkChar); builder.Append(methodName); builder.Append(_linkChar); foreach (var param in parameters) { builder.Append(param); builder.Append(_linkChar); } return builder.ToString().TrimEnd(_linkChar);
}
private IList
FormatArgumentsToPartOfCacheKey(IList if (arg is DateTime) return ((DateTime)arg).ToString("yyyyMMddHHmmss"); if (arg is IQCachable) return ((IQCachable)arg).CacheKey; return null;
}
这里要注意的是GetArgumentValue这个方法,因为一个方法的参数有可能是基本的数据类型,也有可能是自己定义的类。
对于自己定义的类,必须要去实现IQCachable这个接口,并且要定义好键要取的值!
如果说,在一个方法的参数中,有一个自定义的类,但是这个类却没有实现IQCachable这个接口,那么生成的缓存键将不会包含这个参数的信息。
举个生成的例子:
MyClass:MyMethod:100:abc:999
到这里,我们缓存的拦截器就已经完成了。
下面是删除了注释的代码(可去上查看完整的代码)
public class QCachingInterceptor : IInterceptor { private ICachingProvider _cacheProvider; private char _linkChar = ':';public QCachingInterceptor(ICachingProvider cacheProvider) { _cacheProvider = cacheProvider; } public void Intercept(IInvocation invocation) { var qCachingAttribute = this.GetQCachingAttributeInfo(invocation.MethodInvocationTarget ?? invocation.Method); if (qCachingAttribute != null) { ProceedCaching(invocation,qCachingAttribute); } else { invocation.Proceed(); } } private QCachingAttribute GetQCachingAttributeInfo(MethodInfo method) { return method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(QCachingAttribute)) as QCachingAttribute; } private void ProceedCaching(IInvocation invocation,QCachingAttribute attribute) { var cacheKey = GenerateCacheKey(invocation); var cacheValue = _cacheProvider.Get(cacheKey); if (cacheValue != null) { invocation.ReturnValue = cacheValue; return; } invocation.Proceed(); if (!string.IsNullOrWhiteSpace(cacheKey)) { _cacheProvider.Set(cacheKey,TimeSpan.FromSeconds(attribute.AbsoluteExpiration)); } } private string GenerateCacheKey(IInvocation invocation) { var typeName = invocation.TargetType.Name; var methodName = invocation.Method.Name; var methodArguments = this.FormatArgumentsToPartOfCacheKey(invocation.Arguments); return this.GenerateCacheKey(typeName,methodArguments); } private string GenerateCacheKey(string typeName,IList<string> parameters) { var builder = new StringBuilder(); builder.Append(typeName); builder.Append(_linkChar); builder.Append(methodName); builder.Append(_linkChar); foreach (var param in parameters) { builder.Append(param); builder.Append(_linkChar); } return builder.ToString().TrimEnd(_linkChar); } private IList<string> FormatArgumentsToPartOfCacheKey(IList<object> methodArguments,int maxCount = 5) { return methodArguments.Select(this.GetArgumentValue).Take(maxCount).ToList(); } private string GetArgumentValue(object arg) { if (arg is int || arg is long || arg is string) return arg.ToString(); if (arg is DateTime) return ((DateTime)arg).ToString("yyyyMMddHHmmss"); if (arg is IQCachable) return ((IQCachable)arg).CacheKey; return null; }
}
下面就是怎么用的问题了。
这里考虑了两种用法:
- 一种是面向接口的用法,也是目前比较流行的用法
- 一种是传统的,类似通过实例化一个BLL层对象的方法。
先来看看面向接口的用法
public interface IDateTimeService
{
string GetCurrentUtcTime();
}