带你走进缓存世界(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);
            }
        }

    }




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

posted @ 2011-08-01 14:48  君之蘭  阅读(671)  评论(0编辑  收藏  举报