缓存中间件-缓存架构的实现(下)

缓存中间件-缓存架构的实现(下)

前言

缓存架构,说白了就是利用各类手段,来实现缓存,从而下降服务器,乃至数据库的压力。html

这里把以前提出的缓存架构的技术分类放出来:前端

  • 浏览器缓存
    • Cookie
    • LocalStorage
    • SessionStorage
  • CDN缓存
  • 负载层缓存
    • Nginx缓存模块
    • Squid缓存服务器
    • Lua扩展
  • 应用层缓存
    • Etag
    • ThreadLocal
    • Guava
  • 外部缓存
    • Redis
  • 数据库缓存
    • MySql缓存

前面的《缓存中间件-缓存架构的实现(上)》已经简单说明了浏览器缓存,CDN缓存,负载层缓存。此次将会继续阐述应用层缓存,外部缓存,数据库缓存。mysql

应用层缓存

应用层的缓存,每每用户的请求最终达到了应用服务器,可是未达到数据库,其涉及应用服务器的具体开发。redis

Etag

之因此将Etag技术放在应用层缓存,是由于用户的请求一定达到应用层。算法

Etag的意思就是,若是连续两次请求的请求内容是一致的,那么两次响应也应该是一致的。那么第一次请求的响应,就能够充当第二次请求的响应。sql

固然实际业务中,也存在两次请求一致,可是响应不一致(如都是查询银行余额,可是并不同,可能两次操做中间,工资到帐了)。这就涉及到缓存的数据一致性问题,后面会提到。这里再也不深刻。数据库

那么应用服务器怎么判断两次请求一致呢。它能够经过两次请求的hash,进行对比判断。其中涉及HTTP协议,如304状态码,请求协议头If-None-Match字段,响应协议头Etag字段。后端

请求流程

服务端已经作好了对应的开发与设置(如Spring的ShallowEtagHeaderFilter())。数组

第一次请求
  1. 客户端发出请求RequestA
  2. 服务端接收到客户端的请求RequestA,进行如下处理:
    1. 在应用中,根据请求RequestA计算对应的MD5值
    2. 在返回响应ResponseA的协议头中的Etag字段设置前面计算出来的MD5值
    3. 返回对应页面
  3. 客户端接收到响应ResponseA,在浏览器中展现。并在浏览器中缓存ResponseA
第二次请求
  1. 客户端再次发出请求RequestB,而且RequestB与RequestA请求内容相同(如都是请求同一个页面等)
  2. 服务端接收到客户端的请求RequestB,进行如下处理:
    1. 根据请求计算的新ETag,并判断是否与请求RequestB协议头中的If-None-Match字段对应的值(就是以前ResponseA的ETag字段的值)一致
      1. 若是没有超限, 在Response中设置协议状态为304,向客户端返回对应ReponseB
  3. 客户端接收到响应ReponseB,确认其协议状态为304,则直接使用以前缓存的响应ResponseA,做为请求RequestB的返回响应

上述实际上是功能逻辑,若是按照代码逻辑,其实应该这样说:浏览器

客户端
  1. 客户端准备发送请求
  2. 浏览器检测该页面是否有对应的ETag字段的值
  3. 若是有对应的值,就置入请求的协议头
  4. 准备稳当后,浏览器想服务器发送请求
服务端
  1. 根据请求的协议头,判断是否具有Last-Modified/If-None-Match字段
  2. 若是有对应字段,进行如下判断
    1. 根据请求计算的新ETag,并判断是否与请求协议头中的If-None-Match字段对应的值(就是以前ResponseA的ETag字段的值一致
      1. 若是没有超限,在Response中设置协议状态为304,向客户端返回对应Reponse
  3. 若是上述2中任一条件未知足,则执行如下逻辑:
    1. 在应用中,根据请求RequestA计算对应的MD5值,保存在应用中
    2. 返回对应页面
    3. 在返回响应ResponseA的协议头中的Etag字段设置前面计算出来的MD5值

准确地说,这应该是HTTP协议提供的缓存方案,而不只仅只是ETag。由于ETag仅仅与HTTP协议的五大条件请求首部中的If-None-Match与If-Match两个首部相关。除此以外,还有If-Modified-Since,If-Unmodified-Since,If-Range三个条件请求首部。若是之后有机会专门写一篇有关HTTP协议的博客。迫切的小伙伴,也能够翻阅《HTTP权威指南》一书的第七章(尤为是7.8)。

优点

  • 下降数据库访问压力。若是ETag成功,则直接返回状态码304,没有数据库操做。
  • 下降应用服务器压力。若是ETag成功,则直接返回状态码304,无需业务操做等,如日志。
  • 下降带宽压力。根据统计代表,通常请求响应模型中,响应的报文大小远大于请求的保温大小。那么若是返回响应的主体为空,只有304状态码等协议头,则能够大大下降系统带宽压力。

缺点

  • 技术学习投入。若是想要较好利用 ,须要熟悉HTTP协议的缓存设计(包括理念,架构,步骤等)
  • 须要对现有的业务体系,进行必定的调整
  • 数据刷新问题的处理,确保数据的“新鲜度”
  • 应用系统的计算资源占用。有人提出ETag的MD5计算带来了对应的应用系统的CPU占用问题。这个须要说一下:
    • 这取决于具体请求自己是否有比MD5计算更大的CPU占用问题。
    • 合理的缓存架构设计通常不会有这样的问题(如静态资源等CPU占用少的请求,根本就在前面的浏览器,CDN,负载均衡层处理掉了)

实际应用

实际应用部分,主要有两点须要说起。

  • 因为If-None-Match的部分缺点,有须要的小伙伴最好引入Last-Modified-Since搭配使用
  • 实际开发方面,Spring提供了ShallowEtagHeaderFilter(),也能够自行扩展

PS:部分人认为只须要Last-Modified-Since便可,可是仅使用Last-Modified-Since存在如下问题:

  • 1s周期内的变化,没法处理(由于Last-Modified-Since记录的最小时间单位为秒)
  • 部分数据虽然发生了变化,但其实咱们所须要的内容并无变化(如周期性的重写等)
  • 部分应用系统的系统时间存在冲突(即集群内的应用服务器实例的绝对系统时间存在秒级差异。至于集群的时间统一相关的问题,往后有机会专门写一篇博客(感受本身立下了无数flag))。

ThreadLocal

ThreadLocal是什么,我就不在此解释了。不了解的小伙伴,能够这样理解:ThreadLocal就是一个类中的静态Map,其key就是执行线程(调用类实例的线程)的name,而value就是调用位置设置的值。

优点

  • (核心)避免接口定义污染。如应用系统中(同一JVM中)存在A->B->C这样的操做链路。但只有A和C用到了特定参数(如用户信息),那么为了可以调用C,B也必须引入该特定参数(如用户参数),即便B没有用到该特定参数。这就形成了接口定义的污染(详见线程级缓存ThreadLocalCache
  • 数据缓存。因为ThreadLocal是经过栈封闭的理念实现了线程安全,因此其在一些场景下有着特定的使用。

缺点

  • ThreadLocal缓存设计与学习,及原有系统的改动
  • (核心)因为可能涉及多线程与调用链上多个调用节点,因此设计与问题排查会有较大的难度

实际应用

在我以前接收的IOT项目中,终端系统经过传感器数据读取程序与传感器配置,得到原始数据(包括原始监测值,以及配置表中对应配置(如硬件标识,报警阈值等))。可是原始数据采集后,会进行数据清洗,数据报警评估,数据保存等多个操做。可是其中的数据清洗并不涉及硬件标识,与报警阈值等。因此采用ThreadLocal来保存对应数据(硬件配置),避免方法接口的污染。固然,后来因为该流程并不都是有先后顺序要求,因此添加了事件监听,进行异步解耦,下降系统复杂度。

GuavaCache

Guava表明着应用级缓存,更准确说是单JVM实例缓存。在原单机系统时,咱们每每并非采用Redis这样的分布式缓存(除非是但愿利用其数据处理,如GEO处理,集合处理等),而是采用GuavaCache或自定义缓存(自定义缓存的设计,后面会有一篇专门的博客)。

优点

  • 资源占用小。毕竟只是运行于单机的一种缓存工具
  • 实现了一种简便的缓存管理工具,知足了大多数单机系统对缓存的需求

劣势

  • 功能没有分布式缓存中间件完善(尤为是自定义的缓存工具)
  • 若是是采用Guava这样的第三方缓存工具,须要对工具的必定学习成本
  • 若是是自定义实现(为了更为精简,定制化),每每性能的提升对技术水平有着必定的需求(如SoftReference的利用等)
  • 对原有应用的改变

外部缓存

外部缓存的一个重要表明,就是Redis,Memcache这样的分布式缓存中间件。固然外部缓存,你要把文件系统等划分进来,也不是不行,只要能够知足对缓存的定义便可。

这里以Redis为例。

Redis

Redis做为当下最为流行的分布式缓存中间件,其应用能够说是很是普遍的,也是我很是喜欢使用的一种分布式缓存中间件。其是一个开源的,C语言编写的,基于内存,支持持久化的日志型,KV型的网络程序。

优势

  • 使用简单。Redis的单机使用不要太简单。即便是新人,也能够在很短的时间内上手,并在实际开发中应用(固然,若是项目中已经有了相关配置,并提供了相关Util就更方便了)
  • 性能强悍。即便是单机的Redis,也能够在一个普通性能的服务器上,提供每秒十万级的读写能力(固然影响的状况不少,详见redis的BenchMark
  • 功能强大。Redis提供了GEO的相关操做(计算两点距离等),集合相关操做(交集,并集等),流相关操做(相似消息队列)
  • 应用场景多。如Session服务器(分布式Session的优秀解决方案),计数器(Incr),分布式锁等

缺点

  • 须要部署Redis服务器。而且为了确保可用性,每每须要进行集群部署
  • 精通较难。
    • 功能方面。功能强大的Redis,其内部实现仍是有很多东西的,包括其持久化机制,内存管理
    • 理论方面。如Redis内存管理方面,涉及LRU,LFU算法,以及其自定义简化版的实现。又或者其哨兵机制涉及的Raft分布式选举算法等
    • 部署方面。单机部署,以及多种集群部署(生产级部署,能够看我以前的博客-Redis安装(单机及各种集群,阿里云)

实际应用

在我以前接手过的某综合系统(涵盖社交,在线教育,直播等),其Session服务器是经过Redis进行支撑的。经过将<SessionId,Session>的方式,存储在Redis,而SeesionId会保存在用户的Cookie中(至于某些小伙伴担忧的Cookie禁用问题,这就涉及Cookie的知识内容了。Cookie会保存在URL中)

再举一个例子(Redis的应用场景太多了)。以前负责的IOT项目中,其中控系统的报警模块有这么一个需求:同一个终端的同一个传感器在30min中,只报警一次,避免报警刷屏的现象。而中控系统已经采用了Redis(中控系统是能够集群部署,确保可用性,避免性能瓶颈),因此利用Redis的集合特性与expire特性,进行了对应的缓存设计。这个在以后会专门写一篇博客,进行阐述。

数据库缓存

这里说的数据库,是指Mysql,Oracle这样的数据库,而不是Redis这样的。

这里就以Mysql举例,这个你们应该是最熟悉的。

Mysql

Mysql缓存机制,就是缓存sql文本,及其对应的缓存结果,经过KV形式保存到Mysql服务器内存中。以后Mysql服务器,再次遇到一样的sql语句,就会从缓存中直接返回结果,而不须要再进行sql解析,优化,执行。

可能某些人担忧,若是数据改变了,而请求的语句是select * from xxx,那不就一直拿到旧数据了嘛。放心,mysql有这方面的处理,当对应表的数据有所修改,那么使用了这个表的数据的缓存就所有失效。因此对于常常变更的数据表,缓存并无太大价值。

优点

  • 提高性能。一样的语句,第一次执行可能须要1s,而第二次执行每每只须要几毫秒。
  • 避免索引时间。由于是经过请求的sql,直接从缓存中获取对应结果,因此没有进行索引查询操做。
  • 下降数据库磁盘操做。虽然请求到达了数据库,但若是没有进行硬盘操做(寻道,读取数据等),那么该次数据库操做对数据库的资源消耗就小了许多(由于在数据库中最消耗时间的就是索引操做与硬盘操做)
  • 下降数据库资源消耗,提升查询时间。由于其避免了数据库得到sql后的全部操做,取而代之的是从缓存获取数据(一个KV读取操做,资源消耗能够几乎能够忽略了)

缺点

  • mysql缓存的应用,及配置须要足够的专业知识(通常的后端并不会很是深刻这个层次,每每须要专门的DBA进行处理)
  • mysql缓存的判断规则不够智能,提升了查询缓存的使用门槛,下降了其效率
  • mysql缓存的检查与清理须要占用必定资源
  • mysql缓存的内存管理不够完善,会产生必定内存碎片(貌似mysql并非直接采用数据库的内存,就像JVM同样。若是有不一样意见的,能够私信或@我。毕竟我并不擅长数据库,虽然刚接手的工做是进行数据库中间件开发。囧)

扩展

实际应用

在我以前接收的IOT项目中,不管是终端系统,仍是中控系统,每每都存在大数据量的数据查询,单次的数据查询每每涉及万级,十万级数据的查询,而且可能频繁查询(就是屡次刷新页面数据)。

一方面,我经过批量写入(下降数据库链接的占用频次),下降数据库对应数据表的修改频次(从原来的几秒一次,变为一分钟一次)。另外一方面,进行数据库缓存相关配置,确保在一分钟内的数据库不须要进行索引操做与硬盘操做,直接返回内存内的结果。从而有效提升了前端页面数据展现效果。

固然后续,我为了针对这一特定业务场景与需求,对业务稍作了调整,从而大大提升了数据查询效果,大幅下降应用系统资源消耗(这个我会专门写一篇博客,甚至专门开一个系列,用来描写这种粒度的特定业务场景的方案设计)。

布隆过滤器

以前有人私信我,认为布隆过滤器应该归类于缓存架构的一部分。

我开始认为这有必定道理,由于布隆过滤器确实涉及数据的缓存,它须要以往数据的记录,来实现。可是后来我想了想,布隆过滤器并不该该划分为缓存中,由于布隆过滤器是基于缓存的,应用缓存的。就像你能够说Redis缓存属于缓存架构的一部分,可是你不能够说调用缓存的应用服务器属于缓存。因此最终,我并无将布隆过滤器划分为缓存的一部分。而是将它做为一种很是有意思的过滤器,一种限流方式,一种安全手段等。

不过做为扩展,这里简单说一下布隆过滤器。说白了,就是利用Hash的散列映射特性,进行数据过滤。如我在应用中设置一个数组Array(其全部值都为0),其长度为固定的10W。我针对每一个用户计算一个hash值,并将这个hasn值对10W进行取余操做,得到index值(如1000)。我将Array中第index位置的value设置为1。这样放在生产环境后,若是有一个用户,其计算出来的index在Array中对应位置的值为0,则说明这个用户在系统中不存在(固然,若是是1,也并不能就说明其就是系统的用户,毕竟存在哈希冲突与取余冲突,不过几率较低)。经过这样的手段,有效避免无效请求等。

后续可能会专门写一篇有关布隆过滤器的博客。

总结

以上就是缓存架构相关的知识了。固然,这些知识都是粒度比较大的,虽然我举了一些实际例子,可是须要你们针对具体应用场景,进行调整应用。另外,这些知识都是比较通用的。可能在特定业务场景下,还有一些方案没有列在这里。最后,没有最好的技术,只有最合适的技术。这里的许多技术都须要必定的业务规模(数据量,请求数,并发量等),采用比较好的性价比,须要你们仔细考虑。

若是有什么问题或者想法,能够私信或@我。

愿与诸君共进步。

参考

相关文章
相关标签/搜索