书说上文《从壹开始先后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存》,昨天我们说到了AOP面向切面编程,简单的举出了两个栗子,不知道你们有什么想法呢,不知道是否与传统的缓存的使用有作对比了么?html
传统的缓存是在Controller中,将获取到的数据手动处理,而后当另外一个controller中又使用的时候,仍是Get,Set相关操做,固然若是小项目,有两三个缓存还好,若是是特别多的接口调用,面向Service服务层仍是颇有必要的,不须要额外写多余代码,只须要正常调取Service层的接口就行,AOP结合Autofac注入,会自动的查找,而后返回数据,不继续往下走Repository仓储了。git
昨天我发布文章后,有一个网友提出了一个问题,他想的很好,就是若是面向到了Service层,那BaseService中的CURD等基本方法都被注入了,这样会形成太多的代理类,不只没有必要,甚至还有问题,好比把Update也缓存了,这个就不是很好了,嗯,我也发现了这个问题,因此须要给AOP增长验证特性,只针对Service服务层中特定的常使用的方法数据进行缓存等。这样既能保证切面缓存的高效性,又能手动控制,不知道你们有没有其余的好办法,若是有的话,欢迎留言,或者加群我们一块儿讨论,一块儿解决平时的问题。github
在解决方案中添加新项目Blog.Core.Common,而后在该Common类库中添加 特性文件夹 和 特性实体类,之后特性就在这里web
//CachingAttributeredis
/// <summary>
/// 这个Attribute就是使用时候的验证,把它添加到要缓存数据的方法中,便可完成缓存的操做。注意是对Method验证有效
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class CachingAttribute : Attribute { //缓存绝对过时时间 public int AbsoluteExpiration { get; set; } = 30; }
添加Common程序集引用,而后修改缓存AOP类方法 BlogCacheAOP=》Intercept,简单对方法的方法进行判断sql
//qCachingAttribute 代码数据库
//Intercept方法是拦截的关键所在,也是IInterceptor接口中的惟必定义
public void Intercept(IInvocation invocation)
{
var method = invocation.MethodInvocationTarget ?? invocation.Method; //对当前方法的特性验证 var qCachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)) as CachingAttribute; //只有那些指定的才能够被缓存,须要验证 if (qCachingAttribute != null) { //获取自定义缓存键 var cacheKey = CustomCacheKey(invocation); //根据key获取相应的缓存值 var cacheValue = _cache.Get(cacheKey); if (cacheValue != null) { //将当前获取到的缓存值,赋值给当前执行方法 invocation.ReturnValue = cacheValue; return; } //去执行当前的方法 invocation.Proceed(); //存入缓存 if (!string.IsNullOrWhiteSpace(cacheKey)) { _cache.Set(cacheKey, invocation.ReturnValue); } } else { invocation.Proceed();//直接执行被拦截方法 } }
可见在invocation参数中,包含了几乎全部的方法,你们能够深刻研究下,获取到本身须要的数据编程
在指定的Service层中的某些类的某些方法上增长特性(必定是方法,不懂的能够看定义特性的时候AttributeTargets.Method)json
/// <summary>
/// 获取博客列表
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
[Caching(AbsoluteExpiration = 10)]//增长特性
public async Task<List<BlogArticle>> getBlogs() { var bloglist = await dal.Query(a => a.bID > 0, a => a.bID); return bloglist; }
运行项目,打断点,就能够看到,普通的Query或者CURD等都不继续缓存了,只有我们特定的 getBlogs()方法,带有缓存特性的才能够windows
固然,这里还有一个小问题,就是全部的方法仍是走的切面,只是增长了过滤验证,你们也能够直接把那些须要的注入,不须要的干脆不注入Autofac容器,我之因此须要都通过的目的,就是想把它和日志结合,用来记录Service层的每个请求,包括CURD的调用状况。
我我的有一个理解,关于Session或Cache等,在普通单服务器的项目中,很简单,有本身的生命周期等,想获取Session就获取,想拿啥就拿傻,可是在大型的分布式集群中,有可能这一秒的点击的页面和下一秒的都不在一个服务器上,对不对!想一想若是普通的办法,怎么保证session的一致性,怎么获取相同的缓存数据,怎么有效的进行消息队列传递?
这个时候就用到了Redis,这些内容,网上已经处处都是,可是仍是作下记录吧
Redis是一个key-value存储系统。和Memcached相似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操做,并且这些操做都是原子性的。它内置复制、Lua脚本、LRU收回、事务以及不一样级别磁盘持久化功能,同时经过Redis Sentinel提供高可用,经过Redis Cluster提供自动分区。在此基础上,Redis支持各类不一样方式的排序。为了保证效率,数据都是缓存在内存中。区别的是redis会周期性的把更新的数据写入磁盘或者把修改操做写入追加的记录文件,而且在此基础上实现了master-slave(主从)同步。
也就是说,缓存服务器若是意外重启了,数据还都在,嗯!这就是它的强大之处,不只在内存高吞吐,还能持久化。
Redis支持主从同步。数据能够从主服务器向任意数量的从服务器上同步,从服务器能够是关联其余从服务器的主服务器。这使得Redis可执行单层树复制。存盘能够有意无心的对数据进行写操做。因为彻底实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操做的可扩展性和数据冗余颇有帮助。
Redis也是能够作为消息队列的,与之相同功能比较优秀的就是Kafka
Redis仍是有自身的缺点:
Redis只能存储key/value类型,虽然value的类型能够有多种,可是对于关联性的记录查询,没有Sqlserver、Oracle、Mysql等关系数据库方便。
Redis内存数据写入硬盘有必定的时间间隔,在这个间隔内数据可能会丢失,虽而后续会介绍各类模式来保证数据丢失的可能性,可是依然会有可能,因此对数据有严格要求的不建议使用Redis作为数据库。
关于Redis的使用,看到网上一个流程图:
一、保存数据不常常变化
二、若是数据常常变化,就须要取操做Redis和持久化数据层的动做了,保证全部的都是最新的,实时更新Redis 的key到数据库,data到Redis中,可是要注意高并发
1.下载最新版redis,选择.msi安装版本,或者.zip免安装 (我这里是.msi安装)
2.双击执行.msi文件,一路next,中间有一个须要注册服务,由于若是不注册的话,把启动的Dos窗口关闭的话,Redis就中断链接了。
3.若是你是免安装的,须要执行如下语句
启动命令:redis-server.exe redis.windows.conf
注册服务命令:redis-server.exe --service-install redis.windows.conf
去服务列表查询服务,能够看到redis服务默认没有开启,开启redis服务(能够设置为开机自动启动)
还有要看Redis服务是否开启
更新:这里有个小插曲,若是你第一次使用,能够修改下 Redis 的默认端口 6079 ,以前有报导说可能存在被攻击的可能性,不过我的开发,我感受无可厚非。知道有这个事儿便可。
若是你对.net 获取app.config或者web.config驾轻就熟的话,在.net core中就稍显吃力,由于不支持直接对Configuration的操做,
前几篇文章中有一个网友说了这样的方法,在Starup.cs中的ConfigureServices方法中,添加
Blog.Core.Repository.BaseDBConfig.ConnectionString = Configuration.GetSection("AppSettings:SqlServerConnection").Value;
固然这是可行的,只不过,若是配置的数据不少,好比这样的,那就很差写了。
{
"Logging": { "IncludeScopes": false, "Debug": { "LogLevel": { "Default": "Warning" } }, "Console": { "LogLevel": { "Default": "Warning" } } }, //用户配置信息 "AppSettings": { //Redis缓存 "RedisCaching": { "Enabled": true, "ConnectionString": "127.0.0.1:6379" }, //数据库配置 "SqlServer": { "SqlServerConnection": "Server=.;Database=WMBlogDB;User ID=sa;Password=123;", "ProviderName": "System.Data.SqlClient" }, "Date": "2018-08-28", "Author": "Blog.Core" } }
固然,我受到他的启发,简单作了下处理,你们看看是否可行
在Blog.Core.Common类库中,新建Helper文件夹,新建Appsettings.cs操做类,而后引用 Microsoft.Extensions.Configuration.Json 的Nuget包
/// <summary> /// appsettings.json操做类 /// </summary> public class Appsettings { static IConfiguration Configuration { get; set; } static Appsettings() { //ReloadOnChange = true 当appsettings.json被修改时从新加载 Configuration = new ConfigurationBuilder() .Add(new JsonConfigurationSource { Path = "appsettings.json", ReloadOnChange = true }) .Build(); } /// <summary> /// 封装要操做的字符 /// </summary> /// <param name="sections"></param> /// <returns></returns> public static string app(params string[] sections) { try { var val = string.Empty; for (int i = 0; i < sections.Length; i++) { val += sections[i] + ":"; } return Configuration[val.TrimEnd(':')]; } catch (Exception) { return ""; } } }
如何使用呢,直接引用类库,传递想要的参数就行(这里对参数是有顺序要求的,这个顺序就是json文件中的层级)
/// <summary> /// 获取博客列表 /// </summary> /// <returns></returns> [HttpGet] [Route("GetBlogs")] public async Task<List<BlogArticle>> GetBlogs() { var connect=Appsettings.app(new string[] { "AppSettings", "RedisCaching" , "ConnectionString" });//按照层级的顺序,依次写出来 return await blogArticleServices.getBlogs(); }
若是直接运行,会报错,提示没有权限,
操做:右键appsettings.json =》 属性 =》 Advanced =》 复制到输出文件夹 =》 永远复制 =》应用,保存
在Blog.Core.Common的Helper文件夹中,添加SerializeHelper.cs 对象序列化操做,之后再扩展
public class SerializeHelper
{
/// <summary>
/// 序列化
/// </summary>
/// <param name="item"></param>
/// <returns></returns>
public static byte[] Serialize(object item) { var jsonString = JsonConvert.SerializeObject(item); return Encoding.UTF8.GetBytes(jsonString); } /// <summary> /// 反序列化 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="value"></param> /// <returns></returns> public static TEntity Deserialize<TEntity>(byte[] value) { if (value == null) { return default(TEntity); } var jsonString = Encoding.UTF8.GetString(value); return JsonConvert.DeserializeObject<TEntity>(jsonString); } }
在Blog.Core.Common类库中,新建Redis文件夹,新建IRedisCacheManager接口和RedisCacheManager类,并引用Nuget包StackExchange.Redis
namespace Blog.Core.Common
{
/// <summary>
/// Redis缓存接口
/// </summary>
public interface IRedisCacheManager { //获取 Reids 缓存值 string GetValue(string key); //获取值,并序列化 TEntity Get<TEntity>(string key); //保存 void Set(string key, object value, TimeSpan cacheTime); //判断是否存在 bool Get(string key); //移除某一个缓存值 void Remove(string key); //所有清除 void Clear(); } }
由于在开发的过程当中,经过ConnectionMultiplexer频繁的链接关闭服务,是很占内存资源的,因此咱们使用单例模式来实现:
这里要引用 Redis 依赖,如今的在线项目已经把这个类迁移到了Common 层,你们知道怎么用就行。
添加nuget包后,而后引用
using StackExchange.Redis;
public class RedisCacheManager : IRedisCacheManager
{
private readonly string redisConnenctionString; public volatile ConnectionMultiplexer redisConnection; private readonly object redisConnectionLock = new object(); public RedisCacheManager() { string redisConfiguration = Appsettings.app(new string[] { "AppSettings", "RedisCaching", "ConnectionString" });//获取链接字符串 if (string.IsNullOrWhiteSpace(redisConfiguration)) { throw new ArgumentException("redis config is empty", nameof(redisConfiguration)); } this.redisConnenctionString = redisConfiguration; this.redisConnection = GetRedisConnection(); } /// <summary> /// 核心代码,获取链接实例 /// 经过双if 夹lock的方式,实现单例模式 /// </summary> /// <returns></returns> private ConnectionMultiplexer GetRedisConnection() { //若是已经链接实例,直接返回 if (this.redisConnection != null && this.redisConnection.IsConnected) { return this.redisConnection; } //加锁,防止异步编程中,出现单例无效的问题 lock (redisConnectionLock) { if (this.redisConnection != null) { //释放redis链接 this.redisConnection.Dispose(); } try { this.redisConnection = ConnectionMultiplexer.Connect(redisConnenctionString); } catch (Exception) { throw new Exception("Redis服务未启用,请开启该服务"); } } return this.redisConnection; } public void Clear() { foreach (var endPoint in this.GetRedisConnection().GetEndPoints()) { var server = this.GetRedisConnection().GetServer(endPoint); foreach (var key in server.Keys()) { redisConnection.GetDatabase().KeyDelete(key); } } } public bool Get(string key) { return redisConnection.GetDatabase().KeyExists(key); } public string GetValue(string key) { return redisConnection.GetDatabase().StringGet(key); } public TEntity Get<TEntity>(string key) { var value = redisConnection.GetDatabase().StringGet(key); if (value.HasValue) { //须要用的反序列化,将Redis存储的Byte[],进行反序列化 return SerializeHelper.Deserialize<TEntity>(value); } else { return default(TEntity); } } public void Remove(string key) { redisConnection.GetDatabase().KeyDelete(key); }
public void Set(string key, object value, TimeSpan cacheTime) { if (value != null) { //序列化,将object值生成RedisValue redisConnection.GetDatabase().StringSet(key, SerializeHelper.Serialize(value), cacheTime); } } public bool SetValue(string key, byte[] value) { return redisConnection.GetDatabase().StringSet(key, value, TimeSpan.FromSeconds(120)); } }
代码仍是很简单的,网上都有不少资源,就是普通的CURD
将redis接口和类 在ConfigureServices中 进行注入,
services.AddScoped<IRedisCacheManager, RedisCacheManager>();//这里说下,若是是本身的项目,我的更建议使用单例模式 services.AddSingleton
关于为啥我使用了 Scoped 的,多是想多了,想到了分布式里边了,这里有个博问:Redis多实例建立链接开销的一些疑问?你们本身看看就好,用单例就能够。
注意是构造函数注入,而后在controller中添加代码测试
/// <summary>
/// 获取博客列表
/// </summary>
/// <returns></returns>
[HttpGet]
[Route("GetBlogs")] public async Task<List<BlogArticle>> GetBlogs() { var connect=Appsettings.app(new string[] { "AppSettings", "RedisCaching" , "ConnectionString" });//按照层级的顺序,依次写出来 List<BlogArticle> blogArticleList = new List<BlogArticle>(); if (redisCacheManager.Get<object>("Redis.Blog") != null) { blogArticleList = redisCacheManager.Get<List<BlogArticle>>("Redis.Blog"); } else { blogArticleList = await blogArticleServices.Query(d => d.bID > 5); redisCacheManager.Set("Redis.Blog", blogArticleList, TimeSpan.FromHours(2));//缓存2小时 } return blogArticleList; }
旁白:这一块终于解决了,时间大概通过了4个月,终于被群里的小伙伴@JoyLing 给解决了,我我的感受仍是很不错的,这里记录一下:
在上篇文章中,咱们已经定义过了一个拦截器,只不过是基于内存Memory缓存的,并不适应于Redis,上边我们也说到了Redis必需要存入指定的值,好比字符串,而不能将异步对象 Task<T> 保存到硬盘上,因此咱们就修改下拦截器方法,一个专门应用于 Redis 的切面拦截器:
//经过注入的方式,把Redis缓存操做接口经过构造函数注入 private IRedisCacheManager _cache; public BlogRedisCacheAOP(IRedisCacheManager cache) { _cache = cache; } //Intercept方法是拦截的关键所在,也是IInterceptor接口中的惟必定义 public void Intercept(IInvocation invocation) { var method = invocation.MethodInvocationTarget ?? invocation.Method; //对当前方法的特性验证 var qCachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)) as CachingAttribute; if (qCachingAttribute != null) { //获取自定义缓存键,这个和Memory内存缓存是同样的,不细说 var cacheKey = CustomCacheKey(invocation); //核心1:注意这里和以前不一样,是获取的string值,以前是object var cacheValue = _cache.GetValue(cacheKey); if (cacheValue != null) { //将当前获取到的缓存值,赋值给当前执行方法 var type = invocation.Method.ReturnType; var resultTypes = type.GenericTypeArguments; if (type.FullName == "System.Void") { return; } object response; if (type != null && typeof(Task).IsAssignableFrom(type)) { //核心2:返回异步对象Task<T> if (resultTypes.Count() > 0) { var resultType = resultTypes.FirstOrDefault(); // 核心3,直接序列化成 dynamic 类型,以前我一直纠结特定的实体 dynamic temp = Newtonsoft.Json.JsonConvert.DeserializeObject(cacheValue, resultType); response = Task.FromResult(temp); } else { //Task 无返回方法 指定时间内不容许从新运行 response = Task.Yield(); } } else { // 核心4,要进行 ChangeType response = System.Convert.ChangeType(_cache.Get<object>(cacheKey), type); } invocation.ReturnValue = response; return; } //去执行当前的方法 invocation.Proceed(); //存入缓存 if (!string.IsNullOrWhiteSpace(cacheKey)) { object response; //Type type = invocation.ReturnValue?.GetType(); var type = invocation.Method.ReturnType; if (type != null && typeof(Task).IsAssignableFrom(type)) { var resultProperty = type.GetProperty("Result"); response = resultProperty.GetValue(invocation.ReturnValue); } else { response = invocation.ReturnValue; } if (response == null) response = string.Empty; // 核心5:将获取到指定的response 和特性的缓存时间,进行set操做 _cache.Set(cacheKey, response, TimeSpan.FromMinutes(qCachingAttribute.AbsoluteExpiration)); } } else { invocation.Proceed();//直接执行被拦截方法 } }
上边红色标注的,是和以前不同的,总体结构仍是差很少的,相信都能看的懂的,最后咱们就能够很任性的在Autofac容器中,进行任意缓存切换了,是否是很棒!
再次感受小伙伴JoyLing,不知道他博客园地址。