本文共 6376 字,大约阅读时间需要 21 分钟。
缓存架构,说白了就是利用各种手段,来实现缓存,从而降低服务器,乃至数据库的压力。
这里把之前提出的缓存架构的技术分类放出来:
前面的已经简单说明了浏览器缓存,CDN缓存,负载层缓存。这次将会继续阐述应用层缓存,外部缓存,数据库缓存。
应用层的缓存,往往用户的请求最终达到了应用服务器,但是未达到数据库,其涉及应用服务器的具体开发。
之所以将Etag技术放在应用层缓存,是因为用户的请求必定达到应用层。
Etag的意思就是,如果连续两次请求的请求内容是一致的,那么两次响应也应该是一致的。那么第一次请求的响应,就可以充当第二次请求的响应。
当然实际业务中,也存在两次请求一致,但是响应不一致(如都是查询银行余额,但是并不一样,可能两次操作中间,工资到账了)。这就涉及到缓存的数据一致性问题,后面会提到。这里不再深入。
那么应用服务器怎么判断两次请求一致呢。它可以通过两次请求的hash,进行对比判断。其中涉及HTTP协议,如304状态码,请求协议头If-None-Match字段,响应协议头Etag字段。
服务端已经做好了对应的开发与设置(如Spring的ShallowEtagHeaderFilter())。
上述其实是功能逻辑,如果按照代码逻辑,其实应该这样说:
准确地说,这应该是HTTP协议提供的缓存方案,而不仅仅只是ETag。因为ETag仅仅与HTTP协议的五大条件请求首部中的If-None-Match与If-Match两个首部相关。除此之外,还有If-Modified-Since,If-Unmodified-Since,If-Range三个条件请求首部。如果以后有机会专门写一篇有关HTTP协议的博客。迫切的小伙伴,也可以翻阅《HTTP权威指南》一书的第七章(尤其是7.8)。
实际应用部分,主要有两点需要提及。
PS:部分人认为只需要Last-Modified-Since即可,但是仅使用Last-Modified-Since存在以下问题:
ThreadLocal是什么,我就不在此解释了。不了解的小伙伴,可以这样理解:ThreadLocal就是一个类中的静态Map,其key就是执行线程(调用类实例的线程)的name,而value就是调用位置设置的值。
在我之前接收的IOT项目中,终端系统通过传感器数据读取程序与传感器配置,获得原始数据(包括原始监测值,以及配置表中对应配置(如硬件标识,报警阈值等))。但是原始数据采集后,会进行数据清洗,数据报警评估,数据保存等多个操作。但是其中的数据清洗并不涉及硬件标识,与报警阈值等。所以采用ThreadLocal来保存对应数据(硬件配置),避免方法接口的污染。当然,后来由于该流程并不都是有前后顺序要求,所以添加了事件监听,进行异步解耦,降低系统复杂度。
Guava代表着应用级缓存,更准确说是单JVM实例缓存。在原单机系统时,我们往往并不是采用Redis这样的分布式缓存(除非是希望利用其数据处理,如GEO处理,集合处理等),而是采用GuavaCache或自定义缓存(自定义缓存的设计,后面会有一篇专门的博客)。
外部缓存的一个重要代表,就是Redis,Memcache这样的分布式缓存中间件。当然外部缓存,你要把文件系统等划分进来,也不是不行,只要可以满足对缓存的定义即可。
这里以Redis为例。
Redis作为当下最为流行的分布式缓存中间件,其应用可以说是非常广泛的,也是我非常喜欢使用的一种分布式缓存中间件。其是一个开源的,C语言编写的,基于内存,支持持久化的日志型,KV型的网络程序。
在我之前接手过的某综合系统(涵盖社交,在线教育,直播等),其Session服务器是通过Redis进行支撑的。通过将<SessionId,Session>的方式,存储在Redis,而SeesionId会保存在用户的Cookie中(至于某些小伙伴担心的Cookie禁用问题,这就涉及Cookie的知识内容了。Cookie会保存在URL中)
再举一个例子(Redis的应用场景太多了)。之前负责的IOT项目中,其中控系统的报警模块有这么一个需求:同一个终端的同一个传感器在30min中,只报警一次,避免报警刷屏的现象。而中控系统已经采用了Redis(中控系统是可以集群部署,确保可用性,避免性能瓶颈),所以利用Redis的集合特性与expire特性,进行了对应的缓存设计。这个在之后会专门写一篇博客,进行阐述。
这里说的数据库,是指Mysql,Oracle这样的数据库,而不是Redis这样的。
这里就以Mysql举例,这个大家应该是最熟悉的。
Mysql缓存机制,就是缓存sql文本,及其对应的缓存结果,通过KV形式保存到Mysql服务器内存中。之后Mysql服务器,再次遇到同样的sql语句,就会从缓存中直接返回结果,而不需要再进行sql解析,优化,执行。
可能某些人担心,如果数据改变了,而请求的语句是select * from xxx,那不就一直拿到旧数据了嘛。放心,mysql有这方面的处理,当对应表的数据有所修改,那么使用了这个表的数据的缓存就全部失效。所以对于经常变动的数据表,缓存并没有太大价值。
在我之前接收的IOT项目中,无论是终端系统,还是中控系统,往往都存在大数据量的数据查询,单次的数据查询往往涉及万级,十万级数据的查询,并且可能频繁查询(就是多次刷新页面数据)。
一方面,我通过批量写入(降低数据库连接的占用频次),降低数据库对应数据表的修改频次(从原来的几秒一次,变为一分钟一次)。另一方面,进行数据库缓存相关配置,确保在一分钟内的数据库不需要进行索引操作与硬盘操作,直接返回内存内的结果。从而有效提高了前端页面数据展示效果。
当然后续,我为了针对这一特定业务场景与需求,对业务稍做了调整,从而大大提高了数据查询效果,大幅降低应用系统资源消耗(这个我会专门写一篇博客,甚至专门开一个系列,用来描写这种粒度的特定业务场景的方案设计)。
之前有人私信我,认为布隆过滤器应该归类于缓存架构的一部分。
我开始认为这有一定道理,因为布隆过滤器确实涉及数据的缓存,它需要以往数据的记录,来实现。但是后来我想了想,布隆过滤器并不应该划分为缓存中,因为布隆过滤器是基于缓存的,应用缓存的。就像你可以说Redis缓存属于缓存架构的一部分,但是你不可以说调用缓存的应用服务器属于缓存。所以最终,我并没有将布隆过滤器划分为缓存的一部分。而是将它作为一种非常有意思的过滤器,一种限流方式,一种安全手段等。
不过作为扩展,这里简单说一下布隆过滤器。说白了,就是利用Hash的散列映射特性,进行数据过滤。如我在应用中设置一个数组Array(其所有值都为0),其长度为固定的10W。我针对每个用户计算一个hash值,并将这个hasn值对10W进行取余操作,获得index值(如1000)。我将Array中第index位置的value设置为1。这样放在生产环境后,如果有一个用户,其计算出来的index在Array中对应位置的值为0,则说明这个用户在系统中不存在(当然,如果是1,也并不能就说明其就是系统的用户,毕竟存在哈希冲突与取余冲突,不过概率较低)。通过这样的手段,有效避免无效请求等。
后续可能会专门写一篇有关布隆过滤器的博客。
以上就是缓存架构相关的知识了。当然,这些知识都是粒度比较大的,虽然我举了一些实际例子,但是需要大家针对具体应用场景,进行调整应用。另外,这些知识都是比较通用的。可能在特定业务场景下,还有一些方案没有列在这里。最后,没有最好的技术,只有最合适的技术。这里的许多技术都需要一定的业务规模(数据量,请求数,并发量等),采用比较好的性价比,需要大家仔细考虑。
如果有什么问题或者想法,可以私信或@我。
愿与诸君共进步。
转载地址:http://fsdkz.baihongyu.com/