带你走进缓存世界(4):缓存之缓

来源:互联网 发布:淘宝app首页解读 编辑:程序博客网 时间:2024/06/11 19:39
        缓存二字,从字面上分为两块:“缓”与“存”。上节我们提到的缓存原理,其实是在讲的一个“存”字,如何存取。大致回顾下是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);            }        }    }



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


原创粉丝点击