MacTalk:每个人都该懂点缓存
文/池建强
发布会在即,身体的疲劳和精神的亢奋差不多都到了一个临界点(又不是你演讲你至于吗?至于吧……),于是决定写篇文章歇歇脑子。写什么呢,就写写发布会期间会大量用到的一种技术:缓存。是的,每个程序员都该懂点缓存。
缓存的英文名称叫做 Cache,是个法国词汇,每个字符里都流淌着高贵的血统。缓存的概念最早来源于 1967 年的一篇电子工程期刊论文,论文作者将「Cache」赋予了「safe keeping storage」的概念,用于计算机领域。
缓存最早出现主要是为了老大哥 CPU 服务的,为了减少 CPU 访问内存所需平均时间,增加了缓存技术。随着硬件技术和操作系统的发展,这部分技术牢牢地掌握在一些巨型芯片公司手里,那里的程序员每天游走在 CPU、Cache、SRAM、、write-through 和 write-back 之间,仿佛掌控着天地间每一个流动字节的意义,自信而优雅,我们把他们叫做文艺程序员。如果你想做一个文艺程序员,请用 Google 百度一下「CPU 缓存」,如果你不会用 Goolge,那么,最好离文艺远一点。
普通程序员需要关注什么类型的缓存呢?比如磁盘缓存、Web 缓存、网络缓存、分布式缓存,等等,这些是我们在开发系统软件和互联网服务时常常要用到的技术。那么,二逼程序员呢?哦,他们只要知道用 Hashtable 做缓存就够了。
缓存的意义是什么?简单来说,缓存是存储数据的一个临时场所,由于获取原始数据的代价太大了,所以,我们会把一些使用频繁的数据放到一个更容易 读取,操作更快的池子(一般是内存)里,为这些数据分门别类,打上标签,这样用户发来请求的时候,我们先在缓存池里进行快速检索,如果拿到数据就直接返回 给用户;如果没有,再去数据库或其他介质获取原始数据返给用户,同时把该数据打上标签,放入缓存池。
就像你开了一个鞋店,店里永远会存放一部分热销和新款的皮鞋,如果客户每次看鞋买鞋的时候,你都要说等一哈,库房就在五公里外,我去去就回。等你热气腾腾跑完一个十公里回来,你发现,连女消费者都走了。
网络世界也是一样,如果没有缓存,用户所有的请求都会直接穿透层层网络,打到数据库和磁盘 IO 上,随着数据量的增加,用户每次请求的时间会越来越长,这样的后果是,磁盘不高兴,数据库不高兴,用户也不高兴,然后就是数据库率先罢工,用户离你而去, 你,依然木有女朋友。
既然缓存这么重要,拥有大量用户的互联网应用都应该增加缓存服务了,那么是不是搞个 Hashtable 就可以了呢?我们可以先了解一些缓存的术语。
缓存命中
用户发起了一个热点数据的请求,系统接收到这个请求之后,就需要根据用户的数据信息(key)去缓存池里寻找数据,如果根据用户提供的 Key 找到了这个条目,并返回给用户,这个过程叫一次缓存命中。
如果在缓存里没找到需要的数据,在缓存空间还有空闲的情况下,系统会去原始数据源(一般是数据库)获取信息,返回给用户,并把数据条目存储到缓存中,以备不时之用。如果缓存空间已经达到上限,那么就要根据缓存替代策略,把旧的数据对象销毁,把新的对象放入缓存池。
对于缓存服务的设计来说,命中率高的缓存系统,性能越好,命中率高,消耗的时间和资源越少。所以缓存服务并不是简单的搭建一个 Memcached、Ehcache 或 Redis 就可以了,相关的技术,应用在合适的业务场景中,才能最大化的利用缓存的价值。
缓存成本
上述场景中,缓存没有命中的时候,系统会从原始数据源中获取数据,一般是数据库或文件系统,然后再把数据放入缓存池中。这个过程需要的时间和空间,就是缓存成本。
一般为了避免缓存成本过高,系统初始化的时候,会同时进行缓存池的初始化,把我们需要的,已知的数据尽可能多的提前放入缓存池,这样可以最大程度的提升缓存命中率,降低缓存成本。
缓存失效
当缓存中的数据需要更新的时候,说明缓存中的数据已经失效了,这时候就需要有相关的服务进行实时的数据更新,同时要保证数据的一致性,不能让系统拿到已经失效的数据到处去招摇撞骗,这种情况,系统和用户的内心,都是拒绝的。
替代策略
编程小球们初入江湖的时候,一般会觉得内存是可以无限使用了,看到服务器上标着 64G 内存这样金光闪闪的配置之后,他们更觉得「广阔天地,大有可为」,于是在系统里 new 出了一个又一个的 Hashtable,然后不停的往里面加入数据读出数据,事实上,如果是三少系统(用户少、数据少,功能少),这样做一时半会还真出不了系统问题。如果是 系统级的缓存服务,要考虑的事情就比较多了。
每个缓存产品,一般都会有一个类似 maxmemory 的最大内存使用参数,这个参数肯定是小于物理内存的。一旦缓存数据达到上限,而又出现缓存没有命中的情况时,系统就会踢出一些老弱病残的缓存数据,加入新 条目。判断老弱病残的标准是什么呢?这就是替代策略。最理想的做法当然是把最没用的数据踢出去,但是,做到最理想永远是最难的,就像你永远想找到团队里最 没用的那一个将其淘汰掉,但执行的时候总是极其艰难。因为除了数据,还有情绪。
幸好,数据没情绪,我们可以通过算法搞定这件事。
常用的一些算法包括:FIFO、LFU、LRU、LRU2、ARC 等。
FIFO 就是先进先出,一种非常简单的算法,缓存数据达到上限的时候,最先进入缓存的数据会最先被踢出去。很多老员工看到这一条都义愤填膺,所以,这个算法注定是 不被喜欢的,但是由于它简单直接,很多开发者喜欢。嗯,有些企业老板也比较喜欢。Second Chance 和 CLock 是基于 FIFO 的改进,算法更加先进合理,也更复杂,微信里写了也没人看,感兴趣的话,Google「缓存算法 CLock」等。
LFU 的全称是 Least Frequently Used,最少使用算法,系统会为每个对象计算使用频率,最不常用的缓存对象会被踢走,简单粗暴。缺点是,一个传统工业时代曾经被重用过的老员工,在互联 网时代没用了,由于其前期使用频率很高,吃老本,所以数据会一直保存在缓存系统中,反而是后起之秀,往往会遭遇到误杀的不公正待遇。差评。
LRU 的全称是 Least Recently Used,也就是最近最少使用算法。基本思路是,如果一个数据最近一段时间被使用的频率很少,那将来被用到的可能性也会很低。看到这儿,那些近期没什么开发任务的童靴,你们要小心了。好评。
LRU2 和 ARC 都是基于 LRU 的改进,有兴趣的可以上网查查。
很多我们耳熟能详的缓存产品,比如 Memcached、Redis、Ehcache、OSCache 等,都参考了类似的算法,要么进行加强,要么进行简化,目标就是提升缓存的命中率,降低缓存成本。
了解了这些够不够?当然不够,你还需要进行大量的实践和业务验证,才能找到最合适你的系统的缓存策略。另外,当你的数据量、访问量和可靠性要求 越来越高的时候,你还需要考虑分布式缓存,以提升缓存容量和扩展性。但是缓存分布了,又带来更多的问题,比如单点失效,命中率,并发,数据同步等等。这样 写起来就木有完了是不是?所以给大家推荐两篇文章:
一种基于「哨兵」的分布式缓存设计:http://blog.lichengwu.cn/architecture/2015/06/14/distributed-cache/
分布式缓存的一起问题:http://timyang.net/data/cache-failure/
本来只想做个普及,没想到还是写了这么多字,年龄大了就是罗嗦,如有错误,你知道就行了……