32

关于缓存这件事

 4 years ago
source link: https://www.tuicool.com/articles/qMZz6nJ
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

缓存在计算机领域中应用的非常广泛,在很多系统中,缓存是实现高性能的关键。当然,缓存不是万能药,如果没有恰当使用好缓存,可能最终达不到期待的效果,反而带来很多问题。这里,我来简单聊一下关于缓存的一些认识。

、缓存 的定义

缓存最初是指CPU和内存之间的高速存储,用于让数据访问的速度适应CPU的处理速度。而目前“缓存”的含义已经被扩充,引用维基百科的定义:凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存(Cache)。

缓存有很多实际的场景,在硬件上有CPU与内存之间的高速缓存、硬盘缓存等,软件上有文件系统的页缓存(Page Cache)、浏览器页面缓存,后台服务将热数据存放于KV系统或者本地内存也是缓存的一种形式。

一般缓存有这几种表现形式:

1. 使用更快的存储介质,提升数据访问速度

例如CPU的高速缓存、文件系统的Page Cache,或者使用KV系统存储数据库的热点数据,都是利用更快速的存储介质,来提升数据的访问速度,降低CPU处理数据时的等待时延。

2. 让数据离处理器更近,使得访问数据耗时更少

例如浏览器的页面缓存,或者程序中使用到的本地缓存,又或者我们网站中使用到的CDN技术,都是通过将数据存放于更近的地方,降低网络延迟带来的时延,使得数据访问耗时更少。

当然,很多的缓存应用场景是结合了两者,例如使用本地内存来缓存来自于数据库的数据,就是使用了更高速的存储介质(内存 vs 磁盘),并且缩短了数据与处理器的距离(省去了与数据库交互的网络时延)。

二、缓存存储的设计模式   

1、 Cache Aside 模式

Cache Aside模式的基本步骤如下:

  • 读操作:优先从cache读取,如果cache没有命中就从数据库读取数据,成功后协会到cache中

bQFZvyf.png!web

  • 写操作:先把数据写入到数据库中,写入成功之后,将缓存 失效 (而不是写入cache)

qmIviui.png!web

这里为什么在写数据库之后不直接将数据写入cache,而是让cache数据失效?这是为了避免写并发问题,当两个写操作几乎同时进行的时候,有可能后进行的写操作先返回,这样最后cache中的数据就变成是先进行的写操作的值,这个值是旧数据,会导致跟数据库不一致了。

当然,在Cache Aside模式下,读写并发的时候也可能存在问题。例如先来一个读请求A,然后有一个写请求B。读请求A没有命中cache,转而从数据库读取到数据,然后B请求写入一个新的数据到数据库中。此时如果B请求比A请求先返回的话,那么在B请求让cache失效之后,A请求将数据库中的旧数据写入到cache中,cache也会存在脏数据。不过这种情况发生概率较小,因为一般读请求处理较快,而写请求需要加锁等操作,速度会相对慢一些。为了避免这种情况导致cache一直存放着脏数据,需要 对cache的数据加上过期时间 ,那么即使存在脏数据,那么在过期时间之后也会恢复。

2、Read/Write Through 模式

在Cache Aside模式中,需要应用程序维护cache和数据库两个数据库,在读和写两种场景中都需要判断是否命中cache,并且需要自行将数据同步到数据库,因此应用程序的代码逻辑比相对复杂一些。

Read/Write Through模式中,则由cache作为一个类似代理的角色,由cache自动实现与数据库的数据交互:

  • Read Through模式

    应用程序向缓存代理发起读请求。如果命中缓存,则立刻返回;如果没有命中,则由缓存代理向数据库读取数据返回,并将数据写入Cache中。 对于应用程序来说,没有缓存和数据库之分,由缓存代理统一了存储入口

uYf6NnI.png!web

  • Write Through模式

    Write Through和Read Through类似。应用程序的写操作,都是跟缓存代理进行交互。缓存代理收到写请求后,如果没有命中缓存,直接写数据库,然后返回;如果命中缓存,那么会同步写入数据库和缓存。

3、Write Behind Caching 模式

Write Behind Caching模式在读操作上和Read Through一样,但是在写操作上策略会更加激进。Write Behind Caching模式下,写操作是先写cache,然后就返回。至于什么时候将cache中的数据刷新到数据库(或者cache对应的可靠的数据源),则根据缓存代理中的策略来定,一般是定期刷新,或者当写入数据量占cache的比例达到一定数值,就会进行数据刷新。

这种模式在写入的时候,因为都是写cache,因此性能是非常高的。但是,在数据写入到cache之后,还没有刷新到数据库之前,如果机器宕机,或者进程被杀,那么cache中写入的数据就可能会丢失。

其实文件系统中的Page Cache,就是这种模式,平时我们调用系统调用write,数据不会立即写入到磁盘上,而是首先写到Page Cache中,然后文件系统会定期将Page Cache中的脏页刷新到磁盘上。如果我们对数据一致性要求非常高,那么就需要在write之后调用一些`fsync`,主动将数据进行刷新到磁盘上,但这样会导致性能不高。这也是数据一致性与性能之间的权衡。

4 、离线缓存

对于数据源是SQL数据库的情况下,还有离线缓存这种模式。离线缓存就是缓存更新并非实时的,是通过离线脚本或者消息队列的形式,将数据库的变更离线更新到cache中。

一般来说,使用离线更新缓存的方式的情况下,对应的缓存是全量缓存,即缓存中包含了所需要的全量的数据,应用程序直接从缓存中读取数据,而不会回源数据库。 对于读操作来说,缓存就是数据库

离线缓存大体上有两种实现方式:

(1) 定时脚本扫描

如果使用定时脚本进行扫描,一般来说,数据库表里面,会有一个类似于update_time的字段,这个字段会在一行记录被更新的时候自动更新为当前时间。通过这个字段,定时脚本就可以知道最近哪些数据被更新,就可以将这些数据写入到cache中。

bM7FZ32.png!web

这种方式优点是:

  • 基本实现比较简单,门槛较低;

  • 不需要依赖第三方组件即可实现。

但也存在这些缺点:

  • 通用性较低。 对于数据表的结构要求较高,必须要有一个自动更新的update_time字段,通用性较低。 另外,数据表的行记录不能_硬删除_, 只能通过类似del_flag的字段进行作为删除标记,不然定时扫描脚本无法感知删除的操作

  • 容错性较低。 对于机器的时钟要求比较高,如果定时程序的机器时间与数据库的机器时间不一致,就会导致定时程序加载不到最新的变更

  • 需要扫描数据库,会增大数据库的压力。

(2)使用binlog

数据库的每个更新操作,都会记录一行binlog日志,从数据库master同步到slave中,用于数据的主从同步。 我们可以起一个程序“伪装”为master的一个slave,接收来自master的binlog,这样就可以得到数据库的最新变更了。 这个接收binlog的程序通过解析binlog,将变更数据写入到cache中。

下图就是使用binlog进行同步数据的架构图(图片来自fynn大神~)。

RrMRfuJ.png!web

使用binlog方式进行同步的优点有这些:

  • 通用性较高。 对表的结构没有特殊要求。

  • 容错性较高。 不需要依赖机器时间的一致,也可以避免定时脚本扫描方式中存在的漏数据问题。

目前已经有一些现有的工具可以作为binlog接收程序,来实现数据库数据同步,例如canal、mypipe、maxwell 和腾讯云的DTS等。 在线教育后台这里,fynn大神搭建了一个基于腾讯云DTS + kafka的数据同步系统,使用腾讯云DTS接收MySQL binlog,然后使用kafka进行消息广播,业务服务需要关注数据库数据变更的话,从kafka消费消息即可。

离线缓存的方式中,业务程序是直接读取缓存数据,性能上相比起读数据库是高很多的,也不需要考虑如何业务考虑如何更新缓存。 不过,离线缓存的话, 数据是不能及时更新到cache ,这种也是一种为了性能牺牲一定的数据实时性的策略。 对于对数据一致性和实时性要求高的场景,离线缓存就不适用了。

当然,如果的确需要实时性,可以使用数据库代理,或者业务代码手动写入缓存等方法,对写入操作实时写入cache。 但是由于写入口难以统一收拢,同时保证写数据库和写缓存是事务性的,在实现上存在难度。 当使用离线缓存更新,而不是实时缓存更新,一般就是考虑到 降低实现复杂度和逻辑的耦合度

三、缓存的使用陷阱

缓存的使用会存在一些隐藏的陷阱,这些陷阱会导致一些严重的后果。我们有必要了解缓存中存在的陷阱,并且加上一些措施来规避。缓存中存在的陷阱主要有这几个:

  • 缓存穿透

  • 缓存雪崩

  • 缓存击穿

这些陷阱都是在一些特定的情况下,请求会都直接透传到后端存储系统中,会导致系统不可用。下面逐一介绍这些陷阱出现的场景以及对应的解决方案。

1. 缓存穿透

缓存穿透的意思是请求中访问的key,在后端数据库中本来就不存在,因此缓存中也肯定不存在的。如果系统在发现缓存中不存在这些数据之后回源到数据库获取数据,当这类不存在数据的请求量很大,就会有大量请求到达数据库,对数据库造成很大的压力,并且可能造成系统不可用。

系统的逻辑错误或者 外部的恶意请求 都可能会引发大量访问不存在的数据,从而导致缓存穿透。

解决方案:

(1) 对于不存在的key也进行缓存

当我们从后端数据库发现一个key是不存在,也把这个key记录在缓存中,并且标记为key不存在,那么下次使用相同的key访问数据的时候,就可以知道这个key是不存在的,可以直接在缓存层返回。 不过这个方案也存在缺陷, 如果是外部恶意构造的请求,可以构造很多不一样并且不存在的key ,那么这个方案就会失效,因为如果恶意构造的key只出现一次,那么请求还是会穿透到数据库,同时,缓存里面会被塞满很多这种无效的key,甚至有效的key都会被淘汰掉。

BrUbQn6.png!web

(2)使用布隆过滤器

baQRvy2.png!web

布隆过滤器的基本思想是使用一个大位图,以及几个哈希函数。

  • 写入一个key:分别使用这几个哈希函数对key算出哈希值,在位图上哈希值对应的位改为1。

  • 查询一个key是否存在:分别使用这几个哈希函数对key算出哈希值,并查看位图上哈希值对应的位是否 被设置为1。只要有一个位是没有被设置为1,说明这个key是不存在的。

优点:

  • 相对于把所有不存在的key记录下来,会比较节省空间

  • 大部分情况下,可以判断出一个不存在的key,不会轻易被一个从来没有请求过的key穿透

缺点:

  • 不保证100%准确。在布隆过滤器位图大小不变的情况下,当数据量越大,准确率就越低。

  • 对于防止缓存穿透的场景下,在服务启动的时候需要用全量数据来建立布隆过滤器。

2. 缓存雪崩

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。

当然,我们平时较少会将缓存的失效时间设置为相同。 不过在批量读取接口可能会出现一批数据的过期时间是一样的,那么当后面访问这批数据的时候,可能都需要全部从后端存储获取,造成短时间内对数据库压力激增。

解决方案 在设置过期时间的时候,可以使用一定范围内的随机值作为过期时间,这样就 可以 让key的过期时间比较分散,不至于很多的key同时缓存失效。

3. 缓存击穿

如果系统中有一部分key被很高频访问,当这些热门key失效的时候,对这些key的读取访问就会都落到后端数据库,可能会瞬间压垮数据库。 例如微博上,热门话题的明星的资料就是热门的key,如果这些明星资料缓存失效了,大量的请求就会穿透到数据库上。 这种热门key失效导致的问题,就是缓存击穿。

解决方案:

(1)使用互斥锁

  • 发现一个key在缓存中没有的时候,先获取针对这个key的互斥锁;

  • 获取到锁之后进行double check;

  • 如果double check之后key在缓存中还是没有,才访问数据获取这个数据

VRRvUjY.png!web

优点:

  • 这种方案,等于将大量透出到后端数据库的请求进行聚合,最后只有少数的请求真正需要访问到数据库。

缺点:

  • 需要用到互斥锁,因为热门key都会抢占同一个互斥锁,会降低CPU的利用率,锁可能会成为新的性能瓶颈。

(2) 对于热门key,使用异步线程刷新缓存

如果我们有能力识别哪些key是热门key,可以对于这些热门key起独立的线程,异步刷新对应的缓存,这样所有请求都不需要访问到数据库。 当然,对于整体数据量不大的情况,可以对所有的key都异步刷新缓存(类似上面介绍到的离线缓存模式)。

优点:

  • 所有请求都无需请求数据库,直接访问缓存,可以完全避免热门key对于数据库的冲击。

缺点:

  • 牺牲了数据实时性

  • 引入了复杂性,需要增加识别热门key的方案,一旦热门key的判断出现问题,那么就可能导致热门key的访问又穿透到数据库

四、总结

本文大致阐述了缓存的定义、相关设计模式以及使用缓存可能遇到的陷阱。

缓存在日常的工程开发中应用广泛,恰当地使用缓存可以对我们系统的整体性能有很大的提升。我们在使用缓存的时候,最好结合系统实际情况进行选择和取舍,应用适合的设计模式。同时,我们也需要考虑在缓存失效的情况下,系统会受到多大的冲击,并添加相应措施避免缓存失效带来的一系列问题。

参考文献:

缓存更新的套路-coolshell

缓存-维基百科

布隆过滤器-维基百科


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK