缓存种类与缓存更新策略总结

您所在的位置:网站首页 缓存更新算法 缓存种类与缓存更新策略总结

缓存种类与缓存更新策略总结

2024-07-09 01:29| 来源: 网络整理| 查看: 265

数据库一般在服务面临高并发压力时最先出现瓶颈,缓存往往是基于内存的,这要比DB读数据快两个数量级。使用缓存能够有效提高服务响应速度,降低数据库压力 数据读取耗时:

类型约耗时内存读取微秒级数据库读取十几毫秒redis请求0.5毫秒 是否需要缓存

缓存的引入会增加成本和技术复杂度,也可能导致数据不一致,在使用缓存前需要确定是否需要缓存 1.高qps的服务,数据库连接池比较繁忙,可以考虑使用缓存 2.高cpu占用的任务:如果某些任务需要消耗大量的cpu去计算,其结果又能重复利用,一般可以考虑使用缓存 3.尽量在不追求数据强一致场景使用,⼤多数业务场景能接受短时间的数据不⼀致,但⾦额操作等重要数据的场景就不要使⽤了。 4.数据的变更过于频繁也不建议使用缓存,过于频繁则可能导致缓存不断重建,反而降低效率。

缓存种类 本地缓存

进程缓存或者说本地缓存将数据存储在站点、服务的进程内。

请求 请求 请求 查询 查询 查询 浏览器/其他服务 节点1 节点2 节点3 进程缓存 进程缓存 进程缓存 D B 本地缓存的优缺点

本地缓存的优点很明显 1、实现简单:最简单的本地缓存使用Map就可以实现,原则上常用的数据结构,都可以放入进程内缓存。甚至可以直接存放对象实例。基本的数据结构,例如字符串、数值等,可以直接key/value存储。 2、延时更低:不依赖服务外的数据,相比于使用进程外缓存,读取速度更快。

本地缓存的缺点 1.受限于内存的大小的限制 2.进程缓存更新后其他缓存无法得知,缓存间一致性难以保障 3.在服务重启后会丢失。

除此之外,进程内缓存违背了分层架构设计中,应用无状态设计的原则: 云原生背景下,希望提升应用的弹性,尽量要将服务设计成不依赖本地运行环境的无状态应用,应用把“有状态”部分(如缓存、数据库、对象存储等)全部交给云服务,加上进程内存中全局对象的持有小型化和应用快速重构能力(比如基于快照快速恢复到最新状态),那么应用本身就会变成更轻量的“无状态”应用。增加本地缓存毫无疑问违背这一原则,它增加本地依赖,也拖慢了应用的启动。

分布式缓存

分布式缓存通常指redis或者memcached等缓存服务,最常用的kv结构的缓存是redis。

请求 请求 请求 查询 查询 查询 浏览器/其他服务 节点1 节点2 节点3 redis D B 分布式缓存的优缺点

1.redis自带多种优化过的复杂结构 包含字符串、哈希,列表,集合,有序集合这类复杂的数据结构。支持各种场景,如信息列表,用户消息,共同好友等。 2、支持持久化 redis的所有数据都是保存在内存中,然后不定期的通过异步方式保存到磁盘上(RDB模式);或者效率稍低的append only file(aof)模式 当然,尽量不要把redis当作数据库用,如果真的需要持久化数据,建议可以走MySQL 3、具备高可用特性 redis天然支持集群功能,可以实现主动复制,读写分离。 官方也提供了sentinel集群管理工具,能够实现主从服务监控,故障自动转移。 4、存储的内容比较大 5、 支持事务 分布式缓存的缺点即相较于本地缓存,有成本/更复杂/读取慢,同时两者都存在数据一致性等问题

多级缓存

本地缓存和redis能满足我们大部分需求,但是当系统需要追求更高访问速度、数据一致性、以及可用性时,也会使用多级缓存。多级缓存通常使用一个本地缓存加一个redis缓存,参考计算机系统中的多级缓存,优先读取本地缓存,其次是分布式缓存,最后才是DB

请求 请求 请求 查询 查询 查询 浏览器/其他服务 节点1 节点2 节点3 进程缓存 进程缓存 进程缓存 redis D B 优缺点

多级缓存兼顾本地缓存和分布式缓存的优点,正常情况下,本地缓存的的读取进一步加快访问速度,同时也能规避redis宕机或缓存同时失效以及机器重启后产生的缓存雪崩,同时本地缓存只存储热key,基本不再需要关注内存大小的影响。

但是同时它也继承了两者的部分缺点:实现更加复杂,意味着需要维护和关注的地方变多了,由于缓存变多,一致性问题变得更加突出。显然,添加分级缓存也会违背无状态设计的原则。

缓存种类选择 缓存类型选择本地缓存场景简单,数据量不大,数据更新频率较低,最好是只读数据,同时要做好(在进程重启时)缓存丢失和多个实例缓存不一致的准备,例如存储配置信息、基础静态数据、统计信息等分布式缓存常规场景,数据量较大,不允许多个实例缓存不一致(大多数场景都不允许),并且如果有特殊要求(排序等)也更适合使用redis。例如评论列表、商品排行多级缓存超高并发场景,希望热点数据获得内存级别的访问速度和高可用性,但可能牺牲掉一部分一致性(用了本地缓存),这里是一种权衡,例如秒杀系统,短暂的不一致性问题对用户的体验影响并不大(支付除外) 更新策略

不同场景下,常用的缓存更新方式也不同,这关系到缓存和DB的一致性以及访问DB的频率。

主动缓存与被动缓存

主动缓存一般是通过定时任务或者脚本,在用户访问之前,将预先的一些数据放到缓存去,使得用户访问的永远是缓存中的数据。 被动缓存指当用户第一次访问时,缓存中没有该份数据,用户的访问触发缓存,在该缓存的有效期内,后续用户访问相同内容,都是从缓存中取数据。

这两者都有各自的优缺点,主动缓存容易缓存非热点数据,当热点数据可预测时由运营人员主动触发较为合适,适用于热点数据可预测、数据量不大且更新不太频繁的场景。但数据量比较大且热点数据不可预测的场景一般只能使用被动缓存,其可能会导致缓存击穿、缓存穿透、缓存雪崩问题

读写穿透策略(Read/Write Through)

读写穿透策略通常一起使用,其原则是应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互。因为程序只和缓存交互,编码会变得更加简单和整洁,当需要在多处复用相同逻辑时这点就变得格外明显。

读穿透策略通常直接访问缓存,若不存在,则存在两种做法 1.直接返回,认为数据不存在 2.由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用 这取决于服务对数据不一致的容忍程度(但是使用读写穿透策略时容忍度一般都比较高了)

同样的写操作也存在两种做法: 1.只写数据库,缓存由缓存组件更新(No-write allocate) 2.只写缓存,由缓存组件同步至数据库(Write Allocate) 通常情况下,一般会用No-write allocate,少一次写操作,而且把写数据库交给缓存组件大家很不放心

常见的读写穿透更新包括 更新订阅/缓存更新推送以及定时任务更新

更新订阅/缓存更新推送

更新订阅/缓存更新推送能够保证本地缓存在更新时的一致性。

节点2 节点1 update update 缓存更新通知/更新消息推送 缓存更新通知/更新消息推送 本地缓存 消息订阅/MQ消费者 本地缓存 消息订阅/MQ消费者 zookeeper/Mq

通常做法是在写操作发生时发送缓存更新通知到其他节点,从而一定程度上保证节点数据一致(高并发场景下同样可能会有一致性问题发生)

定时任务更新

定时更新也可以用于保证本地缓存的一致性

request read read update 浏览器/其他服务 节点 缓存 定时任务调度服务 D B

当然,定时服务牺牲了更多的缓存与数据库间的一致性,换来更少的数据库访问次数

读写穿透策略常用场景

使用读写穿透策略场景常存在以下特点: 1.主要是一些基础配置类的数据。这些数据实时性要求很低 2.读多写很少的数据 3.定时任务更新可以用作与时间挂钩的数据,如定时开始的活动信息 4.多处修改的数据,更新逻辑需要在多处书写,使用读写穿透策略可以避免这种情况

写回(Write Back)策略

这个策略的核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中

服务 redis 数据库 写策略 更新缓存 标记数据为脏 读策略 查询缓存 返回 将脏数据写入数据库 标记数据未脏 返回 alt [命中] [未命中] 服务 redis 数据库

这种策略带来的写入性能提升是毋庸置疑的,但是几乎不会应用在数据库缓存(更多是计算机架构中的设计),因为只写入缓存有很大的丢失风险,例如缓存crash时脏数据都会丢失。

旁路更新(Cache Aside) 经典旁路更新

旁路更新是最常用的缓存更新策略,因为这种策略实现并不困难,同时很大程度上降低了缓存与数据库不一致的问题。 写策略: 先更新数据库中的数据,再删除缓存中的数据。 读策略的步骤: 如果读取的数据命中了缓存,则直接返回数据; 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

服务 redis 数据库 写策略 更新数据库 删除缓存 读策略 查询缓存 返回 读数据库 回写缓存 返回 alt [命中] [未命中] 服务 redis 数据库

1.这里写数据时删除缓存是因为不能保证并发场景下更新数据库的顺序和更新缓存的顺序一致,其次是不能保证写入的数据会被读取。 2.先删缓存后更新数据库会导致数据一致性问题:缓存删除完之后,有一个读请求,这个时候由于缓存被删除所以直接会读库,读操作的数据是老的并且会被加载进入缓存当中,后续读请求全部访问的老数据。 3.先更新数据库后删除缓存同样会导致数据一致问题:当某一个数据没有缓存时,一个读请求先于一个写请求到达,读请求读取数据库数据,写请求更新数据库数据后删除缓存,读请求最后回写,后续读请求会访问老数据。但是这种情况条件比较苛刻,触发几率很小,需要:数据无缓存+读请求先写请求到+读请求回写在写请求删除之后。 4.2、3两种策略都是分为两步,所以第二步都有失败的风险。缓存先被删除,数据库更新失败,问题不大。数据库先更新,缓存删除失败也会导致数据不一致。

延时双删

针对上述并发场景下的缓存一致性问题,延时双删做了优化,进一步降低缓存一致性问题出现的概率

服务 redis 数据库 读未命中策略 删除缓存 更新数据库 一段延时后删除缓存 服务 redis 数据库

其主要优化就是在先删除缓存后更新数据库的策略后新增了延时删除缓存的一步,这样能防止缓存被更新为旧数据。延时的目的是等待旧数据更新到缓存中(如果有的话) 但是这种优化并不能保证强一致,只能说将最终一致的时间提前,并且延时的具体数值要根据服务不同来定,是一个预估值,不能确保 mysql 和 redis 数据在这个时间段内都实时同步或持久化成功了。其适合的场景:

1.延时双删,有等待环节,如果系统要求低延时,这种场景就不合适了。 2.延时双删,不适合频繁修改数据和要求数据强一致的场景(比如秒杀服务中要求强一致的数据)。

强一致更新策略

要做到强一致,必须要解决我们上述提到的旁路更新的两种不一致原因: 1.高并发读写导致的不一致 2.删除缓存失败导致的不一致 通常的做法有以下几种:

Mysql日志监听+Mq重试

其基本原理是把自己伪装成MySQL slave,模拟MySQL slave的交互协议向MySQL Mater发送 dump协议,MySQL mater收到canal发送过来的dump请求,开始推送binary log给canal,然后canal解析binary log,再发送到存储目的地,比如MySQL,Kafka,Elastic Search等等。

1.伪装成slave监听 2.send 3.update request read 日志监听server D B M Q 缓存 浏览器/其他服务 节点

直接监听binlog,并发安全问题由数据库解决,同时使用MQ的重试机制解决删除缓存失败的问题。目前这种方案阿里已经有开源框架(canal)了。

分布式锁+重试机制

这种方法的原理也很简单,将淘汰缓存与更新库表放入同一把写锁中,与其他读请求互斥,防止其间产生旧数据,从而保证不会出现并发安全问题

分布式锁 request 带重试机制的update read redis D B 浏览器/其他服务 节点

这种方法还顺便解决了缓存击穿的问题==,但是太重了

更新策略总结

首先只要使用缓存就要做好出现一致性问题的准备,即使是强一致更新策略也无法保证完全一致,之后可以根据自己的需要选择对应的更新策略

更新策略选择读写穿透策略配置类信息,对一致性要求不高、读多写少的数据或者在多处修改的数据,使用读写穿透策略可以降低复杂度,简化代码,一致性要求不高的本地缓存也同样适用,可以解决缓存间不一致的问题旁路更新策略常规场景,最常用的缓存更新策略,能很大程度上解决缓存不一致问题,可以覆盖除读写穿透策略的使用情况外的绝大多数场景强一致更新策略对一致性要求极高,牺牲一部分性能获取高的一致性,事实上需要思考,是否值得使用这么重的更新策略,如果需要强一致,是否需要使用缓存 缓存常见问题

缓存穿透:缓存和数据库都没有的数据,被大量请求,直接穿透到数据库 通常解决方法: 1.最常见的是使用布隆过滤器,针对一个或者多个维度过滤请求数据 2.另外一个常见的方法,则是针对数据库与缓存都没有的数据,对空的结果进行缓存

缓存击穿:热key过期时,大量请求访问数据库 通常解决方法: 1.对可以预测热key的场景,设置主动缓存,永不过期 2.上述多级缓存可以解决,一级缓存过期可以由其他缓存兜底 3.上述分布式锁可以解决,DB读取和缓存更新只由一个线程进行

缓存雪崩:数据同时过期或者redis崩了,大量请求访问数据库 通常解决方法: 1.多级缓存 2.过期时间加入随机数,增加波动 3.使用redis集群模式,将数据分散在不同机器

总的来说,缓存种类、更新策略需要综合考虑:

后端请求量、读写数据比例是否可以接受缓存内容冲突(不同机器缓存间短暂不一致)是否能接受空数据(能否使用读穿透策略返回空/使用超时降级返回空)是否可以接受旧数据(缓存与DB间不一致)关于存储的数据类型(是否要使用redis的特殊数据结构)是否可以接受阻塞(需不需要使用性能换取强一致性)关于存储的数据类型(是否要使用redis的特殊数据结构)是否需要考虑穿透、击穿、雪崩等特殊场景


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3