欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-03-12 10:45:24
缓存中间件 缓存架构的实现(下) 前言 缓存架构,说白了就是利用各种手段,来实现缓存,从而降低服务器,乃至数据库的压力。 这里把之前提出的缓存架构的技术分类放出来: 浏览器缓存 Cookie LocalStorage SessionStorage CDN缓存 负载层缓存 Nginx缓存模块 Squi ......

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

前言

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

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

  • 浏览器缓存
    • cookie
    • localstorage
    • sessionstorage
  • cdn缓存
  • 负载层缓存
    • nginx缓存模块
    • squid缓存服务器
    • lua扩展
  • 应用层缓存
    • etag
    • threadlocal
    • guava
  • 外部缓存
    • redis
  • 数据库缓存
    • mysql缓存

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

应用层缓存

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

etag

之所以将etag技术放在应用层缓存,是因为用户的请求必定达到应用层。

etag的意思就是,如果连续两次请求的请求内容是一致的,那么两次响应也应该是一致的。那么第一次请求的响应,就可以充当第二次请求的响应。

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

那么应用服务器怎么判断两次请求一致呢。它可以通过两次请求的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,也并不能就说明其就是系统的用户,毕竟存在哈希冲突与取余冲突,不过概率较低)。通过这样的手段,有效避免无效请求等。

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

总结

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

如果有什么问题或者想法,可以私信或@我。

愿与诸君共进步。

参考