大神留步
先说下一个窝心的问题,求大神帮忙,如何在Task异步编程中,使用Redis存、取Task<List<T>>泛型,有偿帮助,这里谢谢,文末有详细问题说明,可以留言或者私信都可以。
当然我也会一直思考,大家持续关注本帖,如果我想到好办法,会及时更新,并通知大家。
代码已上传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、添加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层中的某些类的某些方法上增加特性(一定是方法,不懂的可以看定义特性的时候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()方法,带有缓存特性的才可以
5、当然,这里还有一个小问题,就是所有的方法还是走的切面,只是增加了过滤验证,大家也可以直接把那些需要的注入,不需要的干脆不注入容器,我之所以需要都经过的目的,就是想把它和日志结合,用来记录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服务(可以设置为开机自动启动)
四、创建appsettings.json数据获取类
如果你对.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" } }
当然,我受到他的启发,简单做了下处理,大家看看是否可行
0、将上面代码添加到appsettings.json文件中
1、在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、这个时候运行项目,就可以看到结果了
五、创建Redis缓存接口以及类,并在Controller中测试
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、在Blog.Core.Common类库中,新建Redis文件夹,并新建IRedisCacheManager接口和RedisCacheManager类,并引用Nuget包StackExchange.Redis
public interface IRedisCacheManager { /// <summary> /// 获取 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="key"></param> /// <returns></returns> TEntity Get<TEntity>(string key); //设置 void Set(string key, object value, TimeSpan cacheTime); //判断是否存在 bool Get(string key); //移除 void Remove(string key); //清除 void Clear(); }
因为在开发的过程中,通过ConnectionMultiplexer频繁的连接关闭服务,是很占内存资源的,所以我们使用单例模式来实现
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(); } this.redisConnection = ConnectionMultiplexer.Connect(redisConnenctionString); } return this.redisConnection; } /// <summary> /// 清除 /// </summary> 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); } } } /// <summary> /// 判断是否存在 /// </summary> /// <param name="key"></param> /// <returns></returns> public bool Get(string key) { return redisConnection.GetDatabase().KeyExists(key); } /// <summary> /// 获取 /// </summary> /// <typeparam name="TEntity"></typeparam> /// <param name="key"></param> /// <returns></returns> 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); } } /// <summary> /// 移除 /// </summary> /// <param name="key"></param> public void Remove(string key) { redisConnection.GetDatabase().KeyDelete(key); } /// <summary> /// 设置 /// </summary> /// <param name="key"></param> /// <param name="value"></param> /// <param name="cacheTime"></param> public void Set(string key, object value, TimeSpan cacheTime) { if (value != null) { //序列化,将object值生成RedisValue redisConnection.GetDatabase().StringSet(key, SerializeHelper.Serialize(value), cacheTime); } } }
代码还是很简单的,网上都有很多资源,就是普通的添加,获取
3、将redis接口和类 在ConfigureServices中 进行注入,(注意是构造函数注入)然后在controller中添加代码测试
services.AddScoped<IRedisCacheManager, RedisCacheManager>();
IAdvertisementServices advertisementServices; IBlogArticleServices blogArticleServices; IRedisCacheManager redisCacheManager;//Reids缓存 /// <summary> /// 构造函数 /// </summary> /// <param name="advertisementServices"></param> /// <param name="blogArticleServices"></param> /// <param name="redisCacheManager"></param> public BlogController(IAdvertisementServices advertisementServices, IBlogArticleServices blogArticleServices, IRedisCacheManager redisCacheManager) { this.advertisementServices = advertisementServices; this.blogArticleServices = blogArticleServices; this.redisCacheManager = redisCacheManager; }
/// <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、运行,执行Redis缓存,看到结果
六、心结
今天的讲解就到里了,是不是有一种草草收场的感觉,是的!本来后来应该最后一节。细心的你应该发现了,我们是在controller进行测试,Redis缓存的是List泛型,但是呢,AOP切面缓存还是基于内存缓存,昨天我本想合并下,奈何AOP切面是通过异步编程,获取到的Task的List泛型,在Redis中需要序列化,鄙人表示不是很懂,希望看到的大神帮忙解决下,
如何把异步返回的Task<List<T>>结果,缓存到Redis,并能通过泛型取出来,有偿服务。感谢!