赞
踩
缓存通常指存储在计算机上的一个原始数据复制集,以便于提升系统的性能,系统性能的指标一般包括如下几点:
不涉及操作系统和硬件的缓存,根据在软件系统中所处位置的不同,缓存大体可以分为三类:
根据规模和部署方式缓存也可以分为:
浏览器缓存是一种常见的客户端缓存。
浏览器缓存是根据一套与服务器约定的规则进行工作的,工作规则很简单:检查以确保副本是最新的,通常只要一次会话。
http提供了一些基本缓存特性:
Expires
的HTTP头来告诉客户端在重新请求文件之前缓存多久是安全的e-tag
询问服务器某个文件是否有变化,如果无变化则返回304-Not Modified,否则返回200以及最新的文件APP缓存没有统一的标准,但是其更新机制通常与浏览器缓存类似。APP缓存通常将内容缓存在内存、文件或本地数据库(例如SQLite)中。
如何把APP缓存对于业务组件透明,以及APP缓存数据的及时更新,是APP缓存能否成功应用起来的关键。
网络中的缓存位于客户端和服务端之间,代理或响应客户端的网络请求,从而对重复的请求返回缓存中的数据资源。同时,接受服务端的请求,更新缓存中的内容。
Web代理几乎是伴随着互联网诞生的,常用的Web代理分为正向代理、反向代理和透明代理。
正向代理中,客户端通常知道目标服务器地址,但由于网络限制等原因,无法直接访问,这时候需要先连接代理服务器,然后再由代理服务器访问目标服务,典型的应用就是VPN。正向代理可以算做是客户端的代理,可以用于隐藏真实的客户端。
反向代理与正向代理相反,对于客户端而言代理服务器就像是源服务器。客户端向反向代理发送普通请求,接着反向代理将判断向何处转发请求,并将从源服务器获得的内容返回给客户端。比如 haproxy 就是典型的反向代理。反向代理可以看作是服务器的代理,可以用于保护和隐藏源服务器,并做一些负载均衡的工作。
透明代理的意思是客户端根本不需要知道有代理服务器的存在,由代理服务器改变客户端请求的报文字段。透明代理的例子就是时下很多公司使用的行为管理软件。
Web代理缓存是指使用正向代理的缓存技术。Web代理缓存的作用跟浏览器的内置缓存类似,只是介于浏览器和互联网之间。
反向代理缓存可以缓存原始资源服务器的资源,而不是每次都要向原始资源服务器请求数据,特别是一些静态的数据,比如图片和文件,很多Web服务器就具备反向代理的功能,比如Nginx。如果这些反向代理服务器能够做到和用户来自同一个网络,那么用户访问反向代理服务器,就会得到很高质量的响应速度,所以这样多的反向代理缓存也称为边缘缓存。
边缘缓存中典型的商业化服务就是 CDN(Content Delivery Network,内容分发网络)了,例如我国的ChinaCache等。
服务端缓存是整个缓存体系中的重头戏。
数据库属于IO密集型的应用,主要负责数据的管理及存储。数据库缓存是一类特殊的缓存,是数据库自身的缓存机制。数据库调优的时候,缓存优化是一项很重要的工作。
在数据库的Query Cache功能中,MySQL在接收到一条select语句的请求后,如果该语句满足Query Cache的要求,MySQL会直接根据预先设定好的HASH算法将接收到的select语句以字符串方式进行hash,然后到Query Cache中直接查找是否已经缓存。如果已经有结果在缓存中,该select请求就会直接将数据返回,从而省略了后面所有的步骤(如SQL语句的解析,优化器优化以及向存储引擎请求数据等),从而极大地提高了性能。
平台级缓存通常指用来写带有缓存特性的应用框架,或者可用于缓存功能的专用库。
应用级缓存,需要开发者通过代码来实现缓存机制。这里是 NoSQL 的主场,不论是Redis还是MongoDB,以及Memcached都可以作为应用级缓存的重要技术。
在设计分布式缓存架构时,通常需要从以下几方面考虑:
缓存引入前,首先要分析业务的应用规模、访问量级。
比如对于规模不大的小系统或系统发展初始阶段,数据量和访问量不大,引入缓存并不能带来明显的性能提升,反而会增加开发、运维的复杂性。
通常可以从以下几方面考虑是否引入缓存:
读多写少
的业务场景,反之,使用缓存的意义其实并不大,命中率会很低。缓存组件的选型要考虑数据模型、访问方式、缓存成本甚至开发人员的知识结构,从而进行因地制宜的取舍,不要盲目引入不熟悉、不活跃、不成熟的缓存组件。
下面是常用的缓存组件对比:
缓存组件 | 数据类型 | 访问方式 | 数据容量 | 同步方式 | 内存效率 | 持久化 |
---|---|---|---|---|---|---|
Memcached | 简单KV | GET SET DEL 等常规接口 | 100GB以下 | Client多写 | 一般 | 不支持 |
Redis | 丰富 | 更丰富的常规接口 | 30GB以下 | 主从复制 | 一般 | 支持 |
缓存与数据库的操作关系根据同步、异步以及操作顺序可以区分为下面几类。
这是最常用的缓存模式了,具体的流程如下图:
注意上图中缓存更新时必须先更新数据库,然后再让缓存失效。我们可以来思考其他时序可能有的问题。
这种做法最大的问题就是两个并发的写操作导致脏数据,如下图:
两个并发更新操作,数据库先更新的反而后更新缓存,数据库后更新的反而先更新缓存。这样就会造成数据库和缓存中的数据不一致,应用程序中读取的都是脏数据。
这种做法下,因为两个并发的读和写操作可能导致脏数据,如下图:
假设更新操作先删除了缓存,此时正好有一个并发的读操作,没有命中缓存后从数据库中取出老数据并且更新回缓存,这个时候更新操作也完成了数据库更新。此时,数据库和缓存中的数据不一致,应用程序中读取的都是原来的数据(脏数据)
这种情况下,严格来说也会有并发问题:
查询操作没有命中缓存,然后查询出数据库的老数据。此时有一个并发的更新操作,更新操作在读操作之后更新了数据库中的数据并且删除了缓存中的数据。然而读操作将从数据库中读取出的老数据更新回了缓存。这样就会造成数据库和缓存中的数据不一致,应用程序中读取的都是原来的数据(脏数据)
但是,仔细想一想,这种并发的概率极低。因为这个条件需要发生在读缓存时缓存失效,而且有一个并发的写操作。实际上数据库的写操作会比读操作慢得多,而且还要加锁,而读操作必需在写操作前进入数据库操作,又要晚于写操作更新缓存,所有这些条件都具备的概率并不大。但是为了避免这种极端情况造成脏数据所产生的影响,我们还是要为缓存设置过期时间。
Cache-As-SoR顾名思义就是把Cache当作SoR(system-of-record,记录系统),业务代码只对Cache操作,而对于SoR的访问在Cache组件内部。具体又分为Read/Write-Through、Write-Behind两种实现。
Read Through 模式就是在查询操作中更新缓存,当缓存失效的时候,Cache Aside 模式是由调用方负责把数据加载入缓存,而 Read Through 则由缓存组件自己来加载。
Write Through 模式和 Read Through 相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由缓存自己更新数据库(这是一个同步操作)。
Write Behind Caching 更新模式就是在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。
Cache Aside实现起来最为简单,且利用了数据库比较成熟的高可用机制,数据一致性高;缺点是需要在业务代码中同时操作cache和数据库。
Read/Write Through 优点是使用简单,业务代码只需要操作cache;缺点是cache组件实现较为复杂一些,且数据安全性和一致性不如Cache Aside。
Write Behind 优点是性能最快,因为其读写都是直接操作内存,而且更新到数据库的操作是异步的,可以把多次操作可以合并再更新数据库;其缺点是实现最为复杂(增量更新、合并操作、异步更新等机制、操作失败时的回滚都更为复杂),且由于其异步特性,其数据安全性(缓存服务器可能宕机)与一致性(异步更新延时较高)都是最差的。
如果我们的缓存服务器是多节点,那么我们希望缓存的key能均匀分布在各节点中,有如下几种分配策略。
哈希方式是最常见的数据分布方式,实现方式是通过可以描述记录的业务的id或key,通过Hash函数的计算求余。余数作为处理该数据的服务器索引编号处理。
这样的好处是只需要通过计算就可以映射出数据和处理节点的关系,不需要存储映射。难点就是如果id分布不均匀可能出现计算、存储倾斜的问题,在某个节点上分布过重。另外在调整数据存储,比如把2个库扩展成4个库,数据迁移是一个比较麻烦的事情。
一致性哈希算法是在1997年由麻省理工学院提出的一种分布式哈希(DHT)实现算法。主要解决单调性(Monotonicity)和分散性(Spread)的问题。单调性简单描述是哈希的结果应能够保证原有已分配的内容可以被映射到原有缓冲中去,避免在节点增减过程中导致不能命中。
一致性哈希的优点在于可以任意动态添加、删除节点。
我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就回源,然后再将结果进行缓存。如果此时数据库和缓存中都没有要查询的数据,就会导致每次查询都要查询缓存和数据库,从而导致数据库压力过大,这就是缓存穿透。缓存穿透甚至能成为他人攻击我们应用的漏洞。
一种解决方法就是为数据库和缓存中都不存在的key预设一个空值(比如设为"&&"表示其是空值)然后可以将其有效时间设置短点(设置太长会导致正常情况也没法使用),这样便可以防止缓存穿透。
设计缓存失效策略时需要考虑的是如何防止缓存雪崩。缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
一个解决方案是随机缓存过期时间,比如我们可以在原有的失效时间基础上增加一个随机范围这样每一个缓存的过期时间的重复率就会降低,降低缓存集体失效的概率。
为更有效利用内存,并维持缓存中对象数量不会过多,缓存组件应该具有灵活的淘汰策略,以保证缓存及时淘汰冷数据,保留热数据。常见的淘汰算法如下。
替换掉最近被请求最少的对象,这种传统策略在实际中应用最广。
方法就是把最新被访问的缓存对象放到缓存池的顶部,当缓存达到了容量极限,去除底部的对象。
替换掉访问次数最少的缓存,这一策略意图是保留最常用的、最流行的对象,替换掉很少使用的那些数据。
FIFO通过一个队列去跟踪所有的缓存对象,最近最常用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被踢走,然后把新的缓存对象加进去。
替换占用空间最大的对象,这一策略通过淘汰一个大对象而不是多个小对象来提高命中率。
还有很多的缓存算法,例如Second Chance、Clock、Simple time-based等等,各种缓存算法没有优劣之分,不同的实际应用场景,会用到不同的缓存算法。在实现缓存算法的时候,通常会考虑使用频率、获取成本、缓存容量和时间等因素。
如果使用redis作为缓存组件,则redis内置了几种缓存淘汰算法。
通常缓存数据的安全性越低则越需要持久化。比如Cache Aside模式下可以不需要缓存持久化,但是Write Behind模式则通常需要持久化,否则可能会造成数据丢失。
如果使用redis作为缓存组件,则可以直接使用redis自带的持久化功能。
访问缓存,需要进行序列化和反序列化。序列化带来的性能损耗是非常可观的,其可能导致CPU运行过高。
序列化性能主要考虑如下:
常用的序列化方式有json,二进制等,也可以直接采用缓存组件的序列化方式。
二进制序列化性能最好,但是可读性差;json可读性好,但是序列化性能差。
如何保障数据一致性是一个不得不面对的问题。这里的一致性包括缓存数据与数据库数据的一致性,亦包括多级缓存数据之间的一致性。在绝大部分场景下,追求最终一致性。
强一致性
要求每次操作缓存和数据库都是原子性的,如果某个步骤失败则需要回滚操作。
最终一致性
的含义是,给定足够长的一段时间,不再发送更新,则认为所有更新会最终传播到整个系统,且所有副本都会达到一致。
大部分情况下对于缓存数据与数据库数据的一致性没有绝对强一致性要求,那么在写缓存失败的情况下,可以通过补偿动作(比如重试)进行,达到最终一致性。
在实际应用中,需要不断的改进缓存的功能和性能,需要对缓存进行关键数据的监控。常见的监控项目如下表:
监控项 | 说明 |
---|---|
uptime | 启动时间 |
cpu | cpu使用情况 |
conns | 连接数 |
total mem | 总分配内存 |
used mem | 使用内存 |
free mem | 空余内存 |
keys count | 缓存对象总数 |
total commands | 总执行命令数 |
hits/s | 每秒命中数 |
miss/s | 每秒未命中数 |
expire/s | 每秒过期数 |
slow query | 慢查询 |
miss/s | 每秒未命中数 |
如果使用redis作为缓存组件,则redis自带的监控面板已基本覆盖了以上指标
集群方式主要有客户端sharding、服务集群等方式
客户端通过对key做一致性hash进行sharding。该种方案服务端运维简单,但是需要客户端实现动态的扩缩容等机制。
主要依赖服务端自带的集群机制,比如Redis的cluster机制。客户端和运维都要了解此种集群模式的使用。
提前把数据读入到缓存的做法就是数据预热处理。数据预热处理要注意一些细节问题:
在具体的代码编写中,一定要注意缓存组件的易用率。
通常需要将缓存操作和数据库操作封装在一个模块中,避免出现弹散式代码。
可以通过工具生成缓存代码,以减少操作缓存的复杂度和错误率,尽量避免手动编写操作缓存的代码。
缓存所有的策略均是优先访问离自己最近的数据
关键链路中,如果无法容忍缓存不可用带来的致命危机,那么还是应该把缓存仅作为提升性能的手段,如果缓存不可用,可以访问数据库兜底
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。