缓存的基础

缓存的基础

该文档编写的目的主要是让开发者明白缓存的相关概念,在使用缓存的时候清楚本身的在作什么事,避免盲目使用形成项目的可维护性变差。本文将从几个方面的来阐述缓存的相关基础概念,包括缓存解决的问题、缓存的弊端、缓存的相关概念、缓存的使用误区。数据库

1、 缓存解决的问题

互联网项目区分于传统的企业软件开发最大的不一样点就是互联网项目须要应对更多的用户。这觉得的应用必须提供更高的并发量支持、同时还须要有更高的性能来提升用户体验。这两个特色致使了互联网项目对缓存的依赖特别重。由于缓存偏偏就是为了解决这两个问题而诞生的。编程

1. 高并发

高并发须要解决的问题从 Web 系统来说主要有两个方面。Web 服务器的的 IO 数和数据库的 IO 数。Web 服务器的 IO 数在设计良好的系统上能够经过简单的增长实例来解决,大多数状况主要是数据库的 IO 问题。一个请求发送到 Web 服务器上到数据库上可能就会放大到几倍,再加上数据库在一些场景上也很能经过增长实例来解决问题。因此大多数状况下只能经过减小对数据库的请求来解决问题。这里就是缓存的一个重要使用场景,经过直接读写缓存来减小对数据库的请求,提升系统的负载能力。后端

2. 高性能

高性能能够经过缓存解决能够从两个角度来思考。一个复杂计算的结果缓存。对于须要消耗不少的时间和资源计算,咱们能够经过缓存结果来快速响应请求。另一个角度就是通信时间。不少时间,花费在计算上的时间并无多少,更多的是数据经过网络传输太耗时。经过读取更近的缓存来减小请求的响应时间也是一个很重要的用途。缓存

2、缓存的弊端

理想状况下咱们确定是指望缓存是对系统的一个补充,更多的提升系统的上限。可是现实状况是,缓存在系统设计之处就不得不认真考虑。由于互联网项目的须要面对的问题决定了咱们的系统下限就已经容忍度很低了,在没有新技术诞生的状况下,现有的架构和中间件就难以达到下限。缓存成了系统中必不可少一部分,因此咱们也要认清缓存会带来的问题,尽最大努力下降加缓存给开发带来的难度。服务器

1. 缓存对业务逻辑的侵入

原来的单体应用,缓存都是存储在本地、须要缓存的内容大多也就是数据库的数据,这其实是比较好搞定的一种缓存使用场景。而到了更复杂的系统中,服务都要求是无状态的、可分布式部署的,这致使原有的缓存理念实际上不能知足如今的使用场景。原有的缓存框架也不适应如今的开发面临的问题。不少时候咱们须要在业务逻辑上手动操做缓存,而不仅仅依靠框架来解决问题。同时为了让缓存应用起来更简单,也要改变原来的一些开发理念,因此缓存也会影响咱们的业务逻辑。网络

2. 缓存让架构更复杂

前文也提到了互联网应用的下限容忍度很低,不少时间一个系统若是没有缓存可能根本就没法提供服务。所以咱们的架构上更加复杂了,须要加上更多的中间件,对中间件的可靠性要求也更高了。同时根据咱们选择的缓存的策略的不一样,整个系统可能就并不是原来的页面、应用、数据库这三大件这么简单了。咱们须要使用 MQ、Redis 来作缓存,计算过程更加复杂,调用链难以追踪,错误难以排查。数据结构

3. 牺牲了数据的一致性

使用缓存必然致使数据很难作到实时一致性,只能作到最终一致性。这也是不少单体架构进行服务拆分时碰到的问题。在使用缓存之后咱们不得不考虑到数据不一致对业务逻辑的影响,甚至为此系统用户的使用体验。架构

3、缓存的相关概念

若是你只作业务逻辑开发,前面的内容可能对你无关紧要,有其余人帮你考虑这些问题,可是对于缓存的分类是须要清楚的,这关系到具体的代码应该怎么写。并发

1. 缓存的概念

前面说了缓存的利弊、说了使用缓存的缘由,可是并无对缓存作一个定义。一方面是缓存的概念没那么复杂,就是存储计算结果从而让下次计算能够直接跳过计算步骤直接获取结果。框架

事实上在更多领域对缓存的使用其可能有更复杂的定义。可是从咱们应用系统开发的层面来讲,这种理解是足够的。更多的是咱们须要了解缓存的一些具体的分类,这有助于咱们更清楚本身作的事,更清楚本身应该使用那种缓存。

2. 缓存的分类

从几个不一样的维度,咱们对缓存作了一些分类。清楚本身的业务场景,选择合适的缓存方式能够下降应用的难度。

  • 从存储位置分类

    • 本地缓存

      存储在本地的缓存。通常是存储在内存中,仅供本进程读取的数据。这种技术使用的很广泛,传统的缓存框架已经针对这种缓存作了很好的封装。咱们更多的须要考虑缓存的边界问题,缓存在本进程的共享范围、缓存的生命周期可否和进程保持一致。

    • 分布式缓存

      由于多实例部署的需求,只把数据缓存在本地已经很难知足咱们的业务场景了。分布式缓存上咱们花费了更多的精力。简单场景咱们能够继续使用原来的缓存套路,把分布式缓存当作本地缓存来用。可是这么作其实并无什么意义,这样的缓存应用模式很难被其余实例共享。更多的只是为了解决应用服务的内存占用问题。

      在使用分布式缓存时咱们须要清楚使用分布式缓存的代价,将缓存内容放在 Redis 等中间件里咱们须要更多的通信时间、读取缓存的速度也降低了。放弃性能更好的本地缓存不用,确定是由于这些缓存使用分布式存储性价比更高。这些缓存须要多实例共享、生命周期没法和进程保持一致、须要预加载等缘由都是咱们使用分布式缓存的缘由。咱们能够彻底用缓存服务器代替本地缓存,可是要清楚代价。

  • 从更新方式分类

    • Cache Aside 更新模式

      这是咱们接触比较多的一种模式,逻辑也比较简单。它的更新模式以下:

      失效:应用程序先从 cache 取数据,没有获得,则从数据库中取数据,成功后,放到缓存中。
      命中:应用程序从 cache 中取数据,取到后返回。
      更新:先把数据存到数据库中,成功后,再让缓存失效。

      咱们在本身写缓存操做逻辑的时候记住遵循这种模式就行了。具体为何要这么作就再也不赘述的,这种缓存更新模式的相关资料不少。

    • Read/Write Through 更新模式

      在上面的 Cache Aside 套路中,应用代码须要维护两个数据存储,一个是缓存(cache),一个是数据库(repository)。因此,应用程序比较啰嗦。而 Read/Write Through 套路是把更新数据库(repository)的操做由缓存本身代理了,因此,对于应用层来讲,就简单不少了。能够理解为,应用认为后端就是一个单一的存储,而存储本身维护本身的 Cache。
      Read Through
      Read Through 套路就是在查询操做中更新缓存,也就是说,当缓存失效的时候(过时或 LRU 换出),Cache Aside 是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务本身来加载,从而对应用方是透明的。
      Write Through
      Write Through 套路和 Read Through 相仿,不过是在更新数据时发生。当有数据更新的时候,若是没有命中缓存,直接更新数据库,而后返回。若是命中了缓存,则更新缓存,而后由 Cache 本身更新数据库(这是一个同步操做)。

      这种缓存使用场景咱们可能使用的比较少,更多的是中间件本身提供的功能。

    • Write Behind Caching 更新模式

      Write Back 套路就是,在更新数据的时候,只更新缓存,不更新数据库,而咱们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的 I/O 操做飞快无比(由于直接操做内存嘛)。由于异步,Write Back 还能够合并对同一个数据的屡次操做,因此性能的提升是至关可观的。

      这种缓存通常须要本身开发中间件来作,对性能和并发要求极高的场景能够考虑投入必定的精力来搭建缓存中间件。

  • 从缓存内容分类

    • Action 缓存

      这类缓存使用的很普遍。从运维的角度咱们就有 CDN 缓存和反向代理缓存,咱们主要讨论后端应用的缓存。Action 缓存是后端应用最先能够加缓存的地方,若是场景足够清晰彻底能够在这一层就把缓存加上来。固然由于考虑到项目的分层问题,可能更多的只把相关逻辑挪到紧邻 controller 的 service 处。这里的缓存建议采用分布式缓存。

    • 方法缓存

      方法缓存在传统项目开发中使用的很广泛,到了互联网项目里虽然应用场景减小了,可是依然使用的很普遍。这种缓存的特征是缓存的 Key 是方法的入参,Value 是方法的结果。这是一个范围比较广的分类,和其余分类多有重合。可是实际上有本质的不一样,能够自行体会一下。

    • 数据缓存

      这种缓存主要是缓存 repository 的数据。比较多的使用场景是为了较少 DB 的压力。在性能要求比较的高的场景能够关闭 repository 的缓存功能,彻底用数据缓存来代替。这样 DB 须要的内存更多,响应速度也更快了。

    • 对象缓存

      把对象缓存放在最后讲不是由于对象缓存不重要。实际上对象缓存很是重要,一个系统若是有良好的对象缓存,那么它缓存应用难度会下降不少,读取和更新会很是简单。可是使用对象缓存的前提是开发是以面向对象的方式来架构系统的,对于对象的建模能力要求很高,这才是使用对象缓存最大的困难点。

4、缓存的使用误区

这里咱们援引了一篇博客(使用缓存的9大误区)上总结的误区,在这个基础上我补充了本身对这些使用误区的理解。

  • 过于依赖默认的缓存机制

    由于直接使用语言提供的序列化方式比较简单,因此不少时候咱们直接使用这种序列化方式并无什么问题。可是不少场景咱们能够本身定制序列化的方式,或者是使用的数据量更少、又或者是这种结构让咱们更新的时候比较方便。一个很典型的场景,咱们可使用 Redis 的 Hash 结构来缓存对象,这样若是咱们想更新缓存的某一条属性的时候是很是方便的。

  • 缓存大对象

    这个更可能是开发者没有正确的认知到本身缓存的对象究竟是什么样的数据结构。好比缓存 Spring 管理的 Bean,世界上咱们在使用这些 Bean 时操做的更可能是本身对象的代理类,这里面可能有不少框架附加上去的信息。咱们觉得本身的对象结构很简单就直接缓存了,等实际序列化之后是很庞大的。缓存毕竟仍是很消耗资源的,对于本身到底缓存了什么东西要内心有数。

  • 使用缓存机制在不一样服务间共享数据

    这里我对原做者的内容作了一些修改,加上了不一样服务间的前提。由于微服务架构中一个服务部署多个实例是一件很正常的事情,同一个服务间共享同一份缓存并无什么问题。可是不一样服务间共享缓存就问题不少了,这让原来辛辛苦苦拆分出来的服务一会儿又被耦合到一块儿了,同时可能不一样服务直接不了解对方的逻辑,还可能致使缓存的内容被修改为错误的值。这是绝对须要避免的错误。可是这么作又是极具诱惑力的,缓存的良好性能让、方便的操做让经过缓存共享数据很省事,可是这对于架构的破坏性很是大。

  • 认为调用缓存 API 以后,数据会被马上缓存起来

    这个误区更多的是出如今使用了 MQ 作缓存的场景,或者是并发量极大的场景。并无什么很好的方式来避免这种错误,更多的是在写缓存逻辑的时候注意到这种状况是有可能发生的。

  • 缓存大量的数据集合,而读取其中一部分

    也是一个比较常见的误区。不少时候是由于从数据库获取的数据就是一个集合,因此直接缓存了这个集合。等到要读取数据的时候不得不反序列化整个集合的数据再从集合里找本身想要的内容。这也致使本身的缓存更新很是麻烦,明明只是更新了集合中某一个缓存就不能不让整个集合的缓存失效,缓存的命中率大大下降了。

    这种状况下试试将集合中的缓存一条一条的存储,也就是将大的缓存对象拆分红多个缓存。

  • 缓存大量具备图结构的对象致使内存浪费

    在面向对象的编程思惟中,咱们很容易就会设计出层级比较多的对象。ORM 框架通常会帮咱们作懒加载,这其实能够很好的利用到数据库的缓存机制。可是这却不利于咱们作分布式缓存。因此说原来的架构并无很好的贴合分布式系统的使用场景。这里 MyBatis 框架由于它的灵活性却是提供了一些方法来作缓存。

  • 缓存应用程序的配置信息

    这也是一种极具诱惑力的使用方式。在缓存服务器中存储配置,这样当须要更新配置时只要修改缓存的值就能够统一修改全部实例的配置。可是这样作风险很大。缓存服务器虽然已经作了不少可靠性保障,可是其本意并非像数据库这类中间件同样必须100%可用,缓存服务器是容许挂掉(理论上)的。若是咱们把配置放在缓存服务器上,这致使咱们不得不把缓存服务器的可靠性也提升到100%。在分布式架构中,配置的分发更推荐使用专业的中间件,例如 zookeeper、etcd 等。它们在设计上就是要作到100%可靠,同时也提供了推送机制,配置更新更及时。

  • 使用不少不一样的键指向相同的缓存项

    这个是在使用方法缓存的时候比较常犯的错误,使用不一样的参数进行计算可是结果实际上是同一个。参考数据库里的索引设计,实际上是同样的道理。若是缓存的 Key 比较复杂,其实能够经过维护一份 Key 的缓存,最后都指向缓存的惟一性标示,类型数据库的主键的设计。这样数据咱们只须要维护一份,只须要维护 Key 的缓存就行了。

  • 没有及时的更新或者删除再缓存中已通过期或者失效的数据

    这个是在使用缓存的时候不多注意到的问题。由于各类缓存框架、缓存服务器通常会帮咱们作一些缓存剔除操做。可是若是须要本身操做缓存的时候就须要特别注意这个问题。一旦缓存里出现了非预期的脏数据,不但清理起来很麻烦,找到出现问题的地方有时候也很难。对于这点更多的是想清楚本身缓存的生命周期,在生命周期结束上记得加上清理逻辑。好比服务关闭的时候进行缓存清理。

相关文章
相关标签/搜索