zoukankan      html  css  js  c++  java
  • 带你走进缓存世界(4):缓存之缓

            缓存二字,从字面上分为两块:“缓”与“存”。上节我们提到的缓存原理,其实是在讲的一个“存”字,如何存取。大致回顾下是key对应的hashcode,根据hashcode作为数组下标来存取,因为存在hash冲突,速度虽达不到O(1),但也是非常之快。今天就说下“缓”的策略。

      缓,便意味着“暂时”的意思,过一段时间就不再存在或被替换掉了,所以我们要说的其实是缓存的过期策略。在缓存入门篇中,主要提到了Cache类的Insert的方法,其中的几个变化的参数寓意着各种缓存策略,有具体依赖的有按时间的,一一来看。

      按过期时间缓存
      这种缓存策略最为简单,只要判断当前时间是否超过了指定的过期时间就remove掉该缓存项即可,一般用于不影响大碍的数据,比如论坛帖子列表,热门板块会更新极其频繁,缓存起来最为合适。但是又不能不更新缓存,不然有人发帖和回帖就看不到了,但可以缓存个一两分钟,两分钟后自动过期,重新加载新的列表,这样就不用管了,所以这种缓存策略更倾向于“不用管”的缓存。既然如此,那么我们就自己写一个按时间过期的缓存类吧。下面的这个类非常基础:

        /// <summary>
        /// 按时间缓存类
        /// </summary>
        public class CacheByDateTime<TKey,TValue>
        {
            /// <summary>
            /// 内部缓存项
            /// </summary>
            class CacheItem
            {
                /// <summary>
                /// 缓存的值
                /// </summary>
                public TValue value { get; set; }
                /// <summary>
                /// 过期时间
                /// </summary>
                public DateTime dateTime { get; set; }
            }
    
            /// <summary>
            /// 缓存数据词典
            /// </summary>
            private readonly Dictionary<TKey, CacheItem> _dict;
    
    	//为了线程安全,需要对dict的操作加锁
            private static readonly object LockDict = new object();
    
            public CacheByDateTime()
            {
                _dict = new Dictionary<TKey, CacheItem>();
            }
            /// <summary>
            /// 添加一个缓存
            /// </summary>
            /// <param name="key"></param>
            /// <param name="value"></param>
            /// <param name="dateTime">过期时间</param>
            public void Add(TKey key, TValue value, DateTime dateTime)
            {
                lock (LockDict)
                {
                    if (_dict.ContainsKey(key))
                    {
                        _dict[key].value = value;
                        _dict[key].dateTime = dateTime;
                    }
                    else
                    {
                        _dict.Add(key, new CacheItem { value = value, dateTime = dateTime });
                    }
                }
            }
            /// <summary>
            /// 获取缓存
            /// </summary>
            public TValue Get(TKey key)
            {
                if (_dict.ContainsKey(key))
                {
                    var val = _dict[key].value;
    		//判断缓存项是否过期
                    if (_dict[key].dateTime > DateTime.Now)
                    {
                        return val;
                    }
                    else
                    {
                        Remove(key);
                        return val;//这里可以酌情是否返回Value,因为毕竟可以省去一次查询
                    }
                }
                return default(TValue);
            }
            /// <summary>
            /// 移除缓存
            /// </summary>
            public void Remove(TKey key)
            {
                lock (LockDict)
                {
                    if (_dict.ContainsKey(key))
                    {
                        _dict.Remove(key);
                    }
                }
            }
        }


            按间隔时间缓存
            这个相对上面的绝对过期时间来说更有趣一些,他的策略是只要被访问,就延迟该缓存的绝对过期时间(间隔时间比如是5分钟就延长5分钟)。这种过期策略似乎十分精明,但对缓存的数据类型也是极其讲究,这种策略一般来缓存什么合适呢?如果说缓存永不过期的数据最为合适,但不存在这样的数据,像网站的配置这种数据极少改动,但访问量巨大,如果用这种缓存策略,不管管理员怎么修改配置,估计这缓存都是更新不了了,反而用上面的缓存合适,而像文章内容这种数据,访问的随机性比较大,拿捏不准啥时候过期,但文章内容极少会被更新,而网站的访问量基本上又属内容页比较大,所以这种缓存缓存文章内容比较合适。可以有效的延长热门内容的过期时间,而冷门的文章自然而言就自动过期了。具体的代码实现只需要在上面的类的Add方面做些改动就可实现:

            /// <summary>
            /// 添加一个缓存
            /// </summary>
            /// <param name="key"></param>
            /// <param name="value"></param>
            /// <param name="timeSpan">间隔时间</param>
            public void Add(TKey key, TValue value, TimeSpan timeSpan)
            {
                lock (LockDict)
                {
                    if (_dict.ContainsKey(key))
                    {
                        _dict[key].value = value;
                        _dict[key].dateTime.Add(timeSpan);
                    }
                    else
                    {
                        _dict.Add(key, new CacheItem { value = value, dateTime = DateTime.Now.Add(timeSpan) });
                    }
                }
            }


            依赖项缓存
            依赖缓存相对以上两个来说是非常复杂的处理过程,比如文件依赖,会有相应的监测程序(FileMonitor)来管理dependency对象。这里我们便不讲解,了解其用处即可,着实因为太过复杂。有兴趣的可以看.Net源码。


            LRU(Least Recently Used)缓存
            从名字便知其意,其主要用于限定容量(比如内存大小或缓存数量)的缓存,需要在缓存容器满了之后踢出过期缓存的策略,是使用次数最少或很久没使用的缓存项策略。
            实现原理一般使用链表方式把所有缓存项连起来,每当有新的缓存进入则把缓存放入链表前端,如果缓存被使用则把他提到链表前端,那么没被使用的将慢慢趋于链表后端,所以当容量满了以后,就优先移除链表末尾的缓存项。当然,也有其他更为复杂的过期策略,比如同时使用缓存时间。虽然此策略和上面的按时间间隔延长缓存有点相像,但这个更侧重于缓存容器大小的管理,毕竟内存是有限的,此策略多用于公共缓存服务。下面的类是个简单的LRU实现,只限定的缓存的长度并没有大小限制,如果要做大小限制则需要计算每一个value的大小。
     

        /// <summary>
        /// LRUCache
        /// </summary>
        public class LRUCache<TKey,TValue>
        {
            /// <summary>
            /// 缓存项
            /// </summary>
            class CacheItem
            {
                public TKey Key { get; set; }
                public TValue Value { get; set; }
                public CacheItem Left { get; set; }
                public CacheItem Right { get; set; }
    
                public CacheItem(TKey key, TValue value)
                {
                    Key = key;
                    Value = value;
                }
            }
    
            private readonly static object LockDict = new object();
    
            private readonly IDictionary<TKey, CacheItem> _dict;
    
            public int Length { get; private set; }
    
            public LRUCache(int maxLength)
            {
                _dict = new Dictionary<TKey, CacheItem>();
                Length = maxLength;
            }
    
            //链表头部
            private CacheItem _first;
            //链表末端
            private CacheItem _last;
    
            public bool HasKey(TKey key)
            {
                return _dict.ContainsKey(key);
            }
    
            /// <summary>
            /// 添加一个缓存项
            /// </summary>
            public void Add(TKey key, TValue value)
            {
                var item = new CacheItem(key, value);
    
                lock (LockDict)
                {
                    //如果没有缓存项,则item既是first也是last
                    if (_dict.Count == 0)
                    {
                        _last = _first = item;
                    }
    
                    //如果只有一个缓存项,则item是first,first和last变为last
                    else if (_dict.Count == 1)
                    {
                        _last = _first;
                        _first = item;
    
                        _last.Left = _first;
                        _first.Right = _last;
                    }
                    else
                    {
                        //item为first,之前的前端向后移位
                        item.Right = _first;
                        _first.Left = item;
                        _first = item;
                    }
    
                    //如果超过的链表长度
                    if (_dict.Count >= Length)
                    {
                        //断开last并移除
                        _last.Left.Right = null;
                        _dict.Remove(_last.Key);
                      
                        _last = _last.Left;
                    }
    
                    //将item放入dict
                    if (_dict.ContainsKey(key))
                        _dict[key] = new CacheItem(key, value);
                    else
                        _dict.Add(key, new CacheItem(key, value));
                }
            }
    
            /// <summary>
            /// 获取一个缓存项
            /// </summary>
            public TValue Get(TKey key)
            {
                if (!_dict.ContainsKey(key))
                {
                    return default(TValue);
                }
    
                var item = _dict[key];
    
                lock (LockDict)
                {
                    if (_dict.Count == 1)
                    {
                        return item.Value;
                    }
    
                    //如果item左侧有缓存项,则将左侧的缓存指向item的右侧
                    if (item.Left != null)
                    {
                        item.Left.Right = item.Right;
                    }
                    else
                    {
                        //否则说明item是first
                        return item.Value;
                    }
    
                    //如果item右侧有缓存项,则将右侧的缓存指向item的左侧
                    if (item.Right != null)
                    {
                        item.Right.Left = item.Left;
                    }
                    else
                    {
                        //否则说明item是last
                        //将last的左侧的右侧断开,让其成为last
                        _last.Left.Right = null;
                        _last = _last.Left;
                    }
                    //断开item的左侧,让item成为first,让first成为item的右侧项
                    item.Left = null;
                    item.Right = _first;
                    _first.Left = item;
                    _first = item;
                }
                return item.Value;
            }
    
            public void Remove(TKey key)
            {
                if (!_dict.ContainsKey(key))
                {
                    return;
                }
    
                var item = _dict[key];
    
                lock (LockDict)
                {
                    //如果item左侧有值,则将左侧的右侧指向item的右侧
                    if (item.Left != null)
                    {
                        item.Left.Right = item.Right;
                    }
                    else
                    {
                        //否则item则是first,所以将item的右侧赋值给first
                        _first = item.Right;
                    }
    
                    //如果item的右侧有值,则将item的右侧的左值指向item的左侧
                    if (item.Right != null)
                    {
                        item.Right.Left = item.Left;
                    }
                    else
                    {
                        _last = item.Left;
                    }
    
                    _dict.Remove(key);
                }
            }
    
        }
    




    以上提到的是我们常用的几种缓存策略,当然还有其他的策略,我们后面也会提到。今天就先到这吧。

  • 相关阅读:
    进制
    流程控制
    运算符
    格式化输出
    数据结构-树的遍历
    A1004 Counting Leaves (30分)
    A1106 Lowest Price in Supply Chain (25分)
    A1094 The Largest Generation (25分)
    A1090 Highest Price in Supply Chain (25分)
    A1079 Total Sales of Supply Chain (25分)
  • 原文地址:https://www.cnblogs.com/mad/p/2123870.html
Copyright © 2011-2022 走看看