从壹开始前后端分离【 .NET Core2.0/3.0 +Vue2.0 】框架之十一 || AOP自定义筛选,Redis入门 11.1
本文3.0版本文章
代码已上传Github+Gitee,文末有地址
书说上文《从壹开始前后端分离【 .NET Core2.0 Api + Vue 2.0 + AOP + 分布式】框架之十 || AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存》,昨天咱们说到了AOP面向切面编程,简单的举出了两个栗子,不知道大家有什么想法呢,不知道是否与传统的缓存的使用有做对比了么?
传统的缓存是在Controller中,将获取到的数据手动处理,然后当另一个controller中又使用的时候,还是Get,Set相关操作,当然如果小项目,有两三个缓存还好,如果是特别多的接口调用,面向Service服务层还是很有必要的,不需要额外写多余代码,只需要正常调取Service层的接口就行,AOP结合Autofac注入,会自动的查找,然后返回数据,不继续往下走Repository仓储了。
昨天我发布文章后,有一个网友提出了一个问题,他想的很好,就是如果面向到了Service层,那BaseService中的CURD等基本方法都被注入了,这样会造成太多的代理类,不仅没有必要,甚至还有问题,比如把Update也缓存了,这个就不是很好了,嗯,我也发现了这个问题,所以需要给AOP增加验证特性,只针对Service服务层中特定的常使用的方法数据进行缓存等。这样既能保证切面缓存的高效性,又能手动控制,不知道大家有没有其他的好办法,如果有的话,欢迎留言,或者加群咱们一起讨论,一起解决平时的问题。
零、今天完成的大红色部分
一、给缓存增加验证筛选特性
1、自定义缓存特性
在解决方案中添加新项目Blog.Core.Common,然后在该Common类库中添加 特性文件夹 和 特性实体类,以后特性就在这里
//CachingAttribute
/// <summary>
/// 这个Attribute就是使用时候的验证,把它添加到要缓存数据的方法中,即可完成缓存的操作。注意是对Method验证有效
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
public class CachingAttribute : Attribute
{
//缓存绝对过期时间
public int AbsoluteExpiration { get; set; } = 30;
}
2、在AOP拦截器中进行过滤
添加Common程序集引用,然后修改缓存AOP类方法 BlogCacheAOP=》Intercept,简单对方法的方法进行判断
//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参数中,包含了几乎所有的方法,大家可以深入研究下,获取到自己需要的数据
3、在service层中增加缓存特性
在指定的Service层中的某些类的某些方法上增加特性(一定是方法,不懂的可以看定义特性的时候AttributeTargets.Method)
/// <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;
}
4、特定缓存效果展示
运行项目,打断点,就可以看到,普通的Query或者CURD等都不继续缓存了,只有咱们特定的 getBlogs()方法,带有缓存特性的才可以
当然,这里还有一个小问题,就是所有的方法还是走的切面,只是增加了过滤验证,大家也可以直接把那些需要的注入,不需要的干脆不注入Autofac容器,我之所以需要都经过的目的,就是想把它和日志结合,用来记录Service层的每一个请求,包括CURD的调用情况。
二、什么是Redis,为什么使用它
我个人有一个理解,关于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的使用,看到网上一个流程图:
1、保存数据不经常变化
2、如果数据经常变化,就需要取操作Redis和持久化数据层的动作了,保证所有的都是最新的,实时更新Redis 的key到数据库,data到Redis中,但是要注意高并发
三、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 ,之前有报导说可能存在被攻击的可能性,不过个人开发,我感觉无可厚非。知道有这个事儿即可。
四、创建appsettings.json数据获取类
如果你对.net 获取app.config或者web.config得心应手的话,在.net core中就稍显吃力,因为不支持直接对Configuration的操作,
1、appsettings.json文件配置参数
前几篇文章中有一个网友说了这样的方法,在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"
}
}
当然,我受到他的启发,简单做了下处理,大家看看是否可行
1、创建 appsettings 帮助类
在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 ""; } } }
2、按照规则获取指定参数
如何使用呢,直接引用类库,传递想要的参数就行(这里对参数是有顺序要求的,这个顺序就是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(); }
3、将appsettings.json添加到bin生成文件中
如果直接运行,会报错,提示没有权限,
操作:右键appsettings.json =》 属性 =》 Advanced =》 复制到输出文件夹 =》 永远复制 =》应用,保存
4、运行项目,查看效果
五、基于Controller的Redis缓存
1、自定义序列化帮助类
在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);
}
}
2、定义Redis接口和实现类
在Blog.Core.Common类库中,新建Redis文件夹,新建IRedisBasketRepository接口和RedisBasketRepository类,并引用Nuget包StackExchange.Redis
namespace Blog.Core.Common
{
/// <summary>
/// Redis缓存接口
/// </summary>
public interface IRedisBasketRepository
{
//获取 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 RedisBasketRepository : IRedisBasketRepository { private readonly ILogger<RedisBasketRepository> _logger; private readonly ConnectionMultiplexer _redis; private readonly IDatabase _database; public RedisBasketRepository(ILogger<RedisBasketRepository> logger, ConnectionMultiplexer redis) { _logger = logger; _redis = redis; _database = redis.GetDatabase(); } private IServer GetServer() { var endpoint = _redis.GetEndPoints(); return _redis.GetServer(endpoint.First()); } public async Task Clear() { foreach (var endPoint in _redis.GetEndPoints()) { var server = GetServer(); foreach (var key in server.Keys()) { await _database.KeyDeleteAsync(key); } } } public async Task<bool> Exist(string key) { return await _database.KeyExistsAsync(key); } public async Task<string> GetValue(string key) { return await _database.StringGetAsync(key); } public async Task Remove(string key) { await _database.KeyDeleteAsync(key); } public async Task Set(string key, object value, TimeSpan cacheTime) { if (value != null) { //序列化,将object值生成RedisValue await _database.StringSetAsync(key, SerializeHelper.Serialize(value), cacheTime); } } public async Task<TEntity> Get<TEntity>(string key) { var value = await _database.StringGetAsync(key); if (value.HasValue) { //需要用的反序列化,将Redis存储的Byte[],进行反序列化 return SerializeHelper.Deserialize<TEntity>(value); } else { return default(TEntity); } } /// <summary> /// 根据key获取RedisValue /// </summary> /// <typeparam name="T"></typeparam> /// <param name="redisKey"></param> /// <returns></returns> public async Task<RedisValue[]> ListRangeAsync(string redisKey) { return await _database.ListRangeAsync(redisKey); } /// <summary> /// 在列表头部插入值。如果键不存在,先创建再插入值 /// </summary> /// <param name="redisKey"></param> /// <param name="redisValue"></param> /// <returns></returns> public async Task<long> ListLeftPushAsync(string redisKey, string redisValue, int db = -1) { return await _database.ListLeftPushAsync(redisKey, redisValue); } /// <summary> /// 在列表尾部插入值。如果键不存在,先创建再插入值 /// </summary> /// <param name="redisKey"></param> /// <param name="redisValue"></param> /// <returns></returns> public async Task<long> ListRightPushAsync(string redisKey, string redisValue, int db = -1) { return await _database.ListRightPushAsync(redisKey, redisValue); } /// <summary> /// 在列表尾部插入数组集合。如果键不存在,先创建再插入值 /// </summary> /// <param name="redisKey"></param> /// <param name="redisValue"></param> /// <returns></returns> public async Task<long> ListRightPushAsync(string redisKey, IEnumerable<string> redisValue, int db = -1) { var redislist = new List<RedisValue>(); foreach (var item in redisValue) { redislist.Add(item); } return await _database.ListRightPushAsync(redisKey, redislist.ToArray()); } /// <summary> /// 移除并返回存储在该键列表的第一个元素 反序列化 /// </summary> /// <param name="redisKey"></param> /// <returns></returns> public async Task<T> ListLeftPopAsync<T>(string redisKey, int db = -1) where T : class { return JsonConvert.DeserializeObject<T>(await _database.ListLeftPopAsync(redisKey)); } /// <summary> /// 移除并返回存储在该键列表的最后一个元素 反序列化 /// 只能是对象集合 /// </summary> /// <param name="redisKey"></param> /// <returns></returns> public async Task<T> ListRightPopAsync<T>(string redisKey, int db = -1) where T : class { return JsonConvert.DeserializeObject<T>(await _database.ListRightPopAsync(redisKey)); } /// <summary> /// 移除并返回存储在该键列表的第一个元素 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="redisKey"></param> /// <param name="db"></param> /// <returns></returns> public async Task<string> ListLeftPopAsync(string redisKey, int db = -1) { return await _database.ListLeftPopAsync(redisKey); } /// <summary> /// 移除并返回存储在该键列表的最后一个元素 /// </summary> /// <typeparam name="T"></typeparam> /// <param name="redisKey"></param> /// <param name="db"></param> /// <returns></returns> public async Task<string> ListRightPopAsync(string redisKey, int db = -1) { return await _database.ListRightPopAsync(redisKey); } /// <summary> /// 列表长度 /// </summary> /// <param name="redisKey"></param> /// <param name="db"></param> /// <returns></returns> public async Task<long> ListLengthAsync(string redisKey, int db = -1) { return await _database.ListLengthAsync(redisKey); } /// <summary> /// 返回在该列表上键所对应的元素 /// </summary> /// <param name="redisKey"></param> /// <returns></returns> public async Task<IEnumerable<string>> ListRangeAsync(string redisKey, int db = -1) { var result = await _database.ListRangeAsync(redisKey); return result.Select(o => o.ToString()); } /// <summary> /// 根据索引获取指定位置数据 /// </summary> /// <param name="redisKey"></param> /// <param name="start"></param> /// <param name="stop"></param> /// <param name="db"></param> /// <returns></returns> public async Task<IEnumerable<string>> ListRangeAsync(string redisKey, int start, int stop, int db = -1) { var result = await _database.ListRangeAsync(redisKey, start, stop); return result.Select(o => o.ToString()); } /// <summary> /// 删除List中的元素 并返回删除的个数 /// </summary> /// <param name="redisKey">key</param> /// <param name="redisValue">元素</param> /// <param name="type">大于零 : 从表头开始向表尾搜索,小于零 : 从表尾开始向表头搜索,等于零:移除表中所有与 VALUE 相等的值</param> /// <param name="db"></param> /// <returns></returns> public async Task<long> ListDelRangeAsync(string redisKey, string redisValue, long type = 0, int db = -1) { return await _database.ListRemoveAsync(redisKey, redisValue, type); } /// <summary> /// 清空List /// </summary> /// <param name="redisKey"></param> /// <param name="db"></param> public async Task ListClearAsync(string redisKey, int db = -1) { await _database.ListTrimAsync(redisKey, 1, 0); } }
代码还是很简单的,网上都有很多资源,就是普通的CURD
3、将Redis服务注入到容器中,并在Controller中调用
将redis接口和类 在ConfigureServices中 进行注入,
services.AddTransient<IRedisBasketRepository, RedisBasketRepository>(); // 配置启动Redis服务,虽然可能影响项目启动速度,但是不能在运行的时候报错,所以是合理的 services.AddSingleton<ConnectionMultiplexer>(sp => { //获取连接字符串 string redisConfiguration = Appsettings.app(new string[] { "Redis", "ConnectionString" }); var configuration = ConfigurationOptions.Parse(redisConfiguration, true); configuration.ResolveDns = true; return ConnectionMultiplexer.Connect(configuration); });
关于为啥我使用了 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 (RedisBasketRepository.Get<object>("Redis.Blog") != null)
{
blogArticleList = RedisBasketRepository.Get<List<BlogArticle>>("Redis.Blog");
}
else
{
blogArticleList = await blogArticleServices.Query(d => d.bID > 5);
RedisBasketRepository.Set("Redis.Blog", blogArticleList, TimeSpan.FromHours(2));//缓存2小时
}
return blogArticleList;
}
4、运行,执行Redis缓存,看到结果
六、基于AOP的Redis缓存
旁白:这一块终于解决了,时间大概经过了4个月,终于被群里的小伙伴@JoyLing 给解决了,我个人感觉还是很不错的,这里记录一下:
1、核心:Redis缓存切面拦截器
在上篇文章中,我们已经定义过了一个拦截器,只不过是基于内存Memory缓存的,并不适应于Redis,上边咱们也说到了Redis必须要存入指定的值,比如字符串,而不能将异步对象 Task<T> 保存到硬盘上,所以我们就修改下拦截器方法,一个专门应用于 Redis 的切面拦截器:
//通过注入的方式,把Redis缓存操作接口通过构造函数注入 private IRedisBasketRepository _cache; public BlogRedisCacheAOP(IRedisBasketRepository 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,不知道他博客园地址。
七、CODE
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core