• .Net Core缓存组件(Redis)源码解析


      上一篇文章已经介绍了MemoryCache,MemoryCache存储的数据类型是Object,也说了Redis支持五中数据类型的存储,但是微软的Redis缓存组件只实现了Hash类型的存储。在分析源码之前,先学几个关于Redis操作的命令。

    一、Redis命令

      Redis所有的命令在http://doc.redisfans.com/上有详细介绍。下面介绍几个常用的关于Hash类型的命令。

      HSET:用于添加缓存

        用法:HSET key field value 。

        返回值:如果 field 是哈希表中的一个新建域,并且值设置成功,返回 1 。

            如果哈希表中域 field 已经存在且旧值已被新值覆盖,返回 0 。
        例如:HSET user Name Microheart
           HSET user Age 18

      HMSET:用于同时添加多个

        用法:HMSET key [field value field1 value1 ...]

        返回值:如果命令执行成功,返回 OK 。

            当 key 不是哈希表(hash)类型时,返回一个错误。
        例如:HMSET user1 Name Microheart Age 18
        

      HGET:获取字段值

        用法:HGET key field  

        返回值:给定域的值。

            当给定域不存在或是给定 key 不存在时,返回 nil 

        例如:HGET user Name (注意 Redis区分大小写)

        

      HMGET:获取多个字段的值

        用法:HMGET key [field1,field2]

        返回值:一个包含多个给定域的关联值的表,表值的排列顺序和给定域参数的请求顺序一样。

        例如:HMGET user Name Age

        

      EXPIRE:设置缓存的过期时间

        用法:EXPIRE key seconds

        返回值:设置成功返回 1 。

            当 key 不存在或者不能为 key 设置生存时间时(比如在低于 2.1.3 版本的 Redis 中你尝试更新 key 的生存时间),返回 0 。
        例如:EXPIRE user 60
        
      TTL:表示剩余生存时间。57表示还有57秒这个缓存过期。过期后,Redis会自动删除。在 Redis 2.4 版本中,过期时间的延迟在 1 秒钟之内 —— 也即是,就算 key 已经过期,但它还是可能在过期之后一秒钟之内被访问到,而在新的 Redis 2.6 版本中,延迟被降低到 1 毫秒之内。

    二、在.Net Core中使用Redis组件

      首先在Startup类中添加Redis缓存功能。配置的Option中设置的InstanceName的值会作为key的一部分。比如设置的InstanceName为test,代码中设置一个缓存key为user,存储到Redis中的实际key为testuser。

    public void ConfigureServices(IServiceCollection services)
    {
          services.AddMvc();
          services.AddDistributedRedisCache(option =>
          {
               option.Configuration = "121.41.55.55:6379";//连接字符串
               option.InstanceName = "test";
          });
    }

    然后在需要使用的地方注入IDistributedCache。如下面所示:

    public class ValuesController : Controller
    {
            private readonly IDistributedCache redisCache;
            public ValuesController(IDistributedCache redisCache)
            {
                this.redisCache = redisCache;
            }
            [HttpGet]
            public IEnumerable<string> Get()
            {
                redisCache.SetAsync("key1", Encoding.UTF8.GetBytes("value1"), new DistributedCacheEntryOptions()
                {
                    AbsoluteExpiration = DateTime.Now.AddSeconds(10)//设置过期时间,时间一到缓存立刻就被移除了
                });
    
                redisCache.SetString("key2", "value2");//没有设置缓存过期时间,表示是永久缓存
    
                return new string[] { "value1", "value2" };
            }
    }

     三、源码解析

      源码在https://github.com/aspnet/Caching,Redis的源码相对简单,主要是因为很多都直接使用的StackExchange.Redis的API。

    RedisCacheOptions类:主要是Redis配置相关。

      Configuration:设置Redis配置,如连接字符串、超时时间等,最终被装换为StackExchange.Redis中的ConfigurationOptions

      InstanceName:实例名称。会和代码中设置的key拼接成为Redis中的key。

    RedisCacheServiceCollectionExtensions类:跟服务注入相关。

      就一个方法AddDistributedRedisCache,依赖注入IDistributedCache的实例。

            public static IServiceCollection AddDistributedRedisCache(this IServiceCollection services, Action<RedisCacheOptions> setupAction)
            {
                if (services == null)
                {
                    throw new ArgumentNullException(nameof(services));
                }
    
                if (setupAction == null)
                {
                    throw new ArgumentNullException(nameof(setupAction));
                }
                services.AddOptions();
                services.Configure(setupAction);
                services.Add(ServiceDescriptor.Singleton<IDistributedCache, RedisCache>());//注入一个单例
                return services;
            }

    RedisCache类:最主要的类,缓存操作相关的类。

      其中插入、获取数据的方法比较重要。

    3.1 Set方法,插入数据。

    public void Set(string key, byte[] value, DistributedCacheEntryOptions options)      
    {  //省略一些逻辑判断
    Connect(); var creationTime = DateTimeOffset.UtcNow;        //对于一个缓存可以设置为绝对过期时间,相对于现在时间的过期时间和滑动过期时间三种(上一篇文章有例子),其实前两种时间类型可以相互转换。
           //下面这一步就是 如果设置了绝对过期时间或者相对于现在时间的过期时间,装换为绝对过期时间
    var absoluteExpiration = GetAbsoluteExpiration(creationTime, options);        //调用了StackExchange.Redis的API 插入缓存 var result = _cache.ScriptEvaluate(SetScript, new RedisKey[] { _instance + key },//这里的key是实例名称+key=Redis中的key,当然我们在查找缓存的时候,并不需要我们手动拼接,只需要传我们复制的key,不需要实例名称
          
    new RedisValue[] { absoluteExpiration?.Ticks ?? NotPresent, options.SlidingExpiration?.Ticks ?? NotPresent,
                   //如果对于一个缓存同时设置了绝对过期时间和滑动过期时间,则取即将到期的时间,也就是最小的那个时间。 GetExpirationInSeconds(creationTime, absoluteExpiration, options)
    ?? NotPresent, value }); }
    上面的添加缓存中,使用了脚本插入。
    private const string SetScript = (@" redis.call('HMSET', KEYS[1], 'absexp', ARGV[1], 'sldexp', ARGV[2], 'data', ARGV[4])//设置key、绝对过期时间、滑动过期时间、和value的值 if ARGV[3] ~= '-1' then redis.call('EXPIRE', KEYS[1], ARGV[3])//设置缓存的时间 end return 1");

      如果absexp和sldexp都没有设置值,默认为-1,表示永不过期,缓存时间就是从设置的绝对过期时间和滑动过期时间中取,当时间到了,Redis自动删除过期缓存,这一点和MemoryCache不一样,MemoryCahe是在对缓存操作的时候,会扫描整个缓存删除,存在很大的延时,而Redis采用下面三种策略清理过期的key:

    1. 被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key
    2. 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key
    3. 当前已用内存超过maxmemory限定时,触发主动清理策略

    这就保证了过期缓存的及时清理。关于Redis清理过期key的策略可以看这篇文章

      当插入一条Hash类型数据时,打开RedisManager会看到下面这样,absexp:绝对过期时间,sldexp:滑动过期时间,data:就是我们代码中设置的value。

    3.2 Get方法中,实现的主要获取功能调用了下面代码。

    internal static class RedisExtensions
        {
            private const string HmGetScript = (@"return redis.call('HMGET', KEYS[1], unpack(ARGV))");//通过脚本HMGET命令获取key的值
         //Get方法中调用此方法,memebers为固定值 data,也就是获取字段data的值 
            internal static RedisValue[] HashMemberGet(this IDatabase cache, string key, params string[] members)
            {
                var result = cache.ScriptEvaluate(
                    HmGetScript,
                    new RedisKey[] { key },
                    GetRedisMembers(members));
                return (RedisValue[])result;
            }
    
            internal static async Task<RedisValue[]> HashMemberGetAsync(
                this IDatabase cache,
                string key,
                params string[] members)
            {
                var result = await cache.ScriptEvaluateAsync(
                    HmGetScript,
                    new RedisKey[] { key },
                    GetRedisMembers(members));
    
                // TODO: Error checking?
                return (RedisValue[])result;
            }
    
            private static RedisValue[] GetRedisMembers(params string[] members)
            {
                var redisMembers = new RedisValue[members.Length];
                for (int i = 0; i < members.Length; i++)
                {
                    redisMembers[i] = (RedisValue)members[i];
                }
                return redisMembers;
            }
        }

    Remove方法就直接调用了StackExchange的API,这里就不做解释。

      相比MemoryCache的代码,Redis代码相对简单,主要是微软的开发人员"偷工减料"吧(我自己感觉),很多重要的方法,比如Redis连接、添加、设置过期时间、都调用了StackExchange的API,没有实现自己的链接池等等。更像是对StackExchangeAPI中的Hash类型的再次封装。

  • 相关阅读:
    js(四) 全选/全不选和反选
    js(三) ajax异步局部刷新技术底层代码实现
    js(二) 实现省市联动(json)
    接口
    内部类
    封装
    Static关键字
    this关键字
    带参数的方法
    abstract关键字
  • 原文地址:https://www.cnblogs.com/MicroHeart/p/9475047.html
Copyright © 2020-2023  润新知