从壹开始前后端分离【 .NET Core2.0 +Vue2.0 】框架之十一 || AOP自定义筛选,Redis入门 11.1

本文涉及的产品
云数据库 RDS SQL Server,基础系列 2核4GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
RDS SQL Server Serverless,2-4RCU 50GB 3个月
推荐场景:
简介: 大神留步 先说下一个窝心的问题,求大神帮忙,如何在Task异步编程中,使用Redis存、取Task泛型,有偿帮助,这里谢谢,文末有详细问题说明,可以留言或者私信都可以。 当然我也会一直思考,大家持续关注本帖,如果我想到好办法,会及时更新,并通知大家。

大神留步

先说下一个窝心的问题,求大神帮忙,如何在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安装)

下载地址:https://github.com/MicrosoftArchive/redis/releases

 


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,并能通过泛型取出来,有偿服务。感谢!

 

七、CODE

 https://github.com/anjoy8/Blog.Core

https://gitee.com/laozhangIsPhi/Blog.Core

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
9天前
|
设计模式 开发框架 JavaScript
基于.NET8 + Vue/UniApp前后端分离的快速开发框架,开箱即用!
基于.NET8 + Vue/UniApp前后端分离的快速开发框架,开箱即用!
|
1月前
|
NoSQL Java Redis
redis的基本命令,并用netty操作redis(不使用springboot或者spring框架)就单纯的用netty搞。
这篇文章介绍了Redis的基本命令,并展示了如何使用Netty框架直接与Redis服务器进行通信,包括设置Netty客户端、编写处理程序以及初始化Channel的完整示例代码。
48 1
redis的基本命令,并用netty操作redis(不使用springboot或者spring框架)就单纯的用netty搞。
|
1月前
|
存储 消息中间件 NoSQL
Redis 入门 - C#.NET Core客户端库六种选择
Redis 入门 - C#.NET Core客户端库六种选择
60 8
|
1月前
|
Windows
.NET 隐藏/自定义windows系统光标
【10月更文挑战第20天】在.NET中,可以使用`Cursor`类来控制光标。要隐藏光标,可将光标设置为`Cursors.None`。此外,还可以通过从文件或资源加载自定义光标来更改光标的样式。例如,在表单加载时设置`this.Cursor = Cursors.None`隐藏光标,或使用`Cursor.FromFile`方法加载自定义光标文件,也可以将光标文件添加到项目资源中并通过资源管理器加载。这些方法适用于整个表单或特定控件。
|
3月前
|
缓存 NoSQL Java
【Azure Redis 缓存 Azure Cache For Redis】Redis出现 java.net.SocketTimeoutException: Read timed out 异常
【Azure Redis 缓存 Azure Cache For Redis】Redis出现 java.net.SocketTimeoutException: Read timed out 异常
|
3月前
|
缓存 NoSQL 网络协议
【Azure Redis 缓存】Redisson 连接 Azure Redis出现间歇性 java.net.UnknownHostException 异常
【Azure Redis 缓存】Redisson 连接 Azure Redis出现间歇性 java.net.UnknownHostException 异常
|
3月前
|
存储 NoSQL Redis
【Azure Developer】一个复制Redis Key到另一个Redis服务的工具(redis_copy_net8)
【Azure Developer】一个复制Redis Key到另一个Redis服务的工具(redis_copy_net8)
【Azure Developer】一个复制Redis Key到另一个Redis服务的工具(redis_copy_net8)
|
3月前
|
开发框架 .NET Docker
【Azure 应用服务】App Service .NET Core项目在Program.cs中自定义添加的logger.LogInformation,部署到App Service上后日志不显示Log Stream中的问题
【Azure 应用服务】App Service .NET Core项目在Program.cs中自定义添加的logger.LogInformation,部署到App Service上后日志不显示Log Stream中的问题
|
2月前
|
消息中间件 NoSQL Go
PHP转Go系列 | ThinkPHP与Gin框架之Redis延时消息队列技术实践
【9月更文挑战第7天】在从 PHP 的 ThinkPHP 框架迁移到 Go 的 Gin 框架时,涉及 Redis 延时消息队列的技术实践主要包括:理解延时消息队列概念,其能在特定时间处理消息,适用于定时任务等场景;在 ThinkPHP 中使用 Redis 实现延时队列;在 Gin 中结合 Go 的 Redis 客户端库实现类似功能;Go 具有更高性能和简洁性,适合处理大量消息。迁移过程中需考虑业务需求及系统稳定性。
|
3月前
|
开发框架 前端开发 JavaScript
前后端分离,Asp.net core webapi 如何配置跨域
前后端分离,Asp.net core webapi 如何配置跨域