缓存
2022-09-14 本文已影响0人
格林哈
1 缓存基础
1 缓存的读写模式
1.1 Cache Aside(旁路缓存)
- 适合场景
- 数据一致性要求高,缓存数据更新比较复杂的业务。
- 缺点
- 需要同时维护 缓存 和 DB 两个数据存储方,过于繁琐
- 写操作
- 先更新数据库,直接将key从缓存中删除,然后由数据库驱动缓存数据的更新。
- 数据库驱动缓存的更新如
- 使用一个 Trigger 组件,读取并解析mysql的binlog,然后进行一些业务逻辑处理,更新缓存数据
- 可以先删除缓存,再更新数据,再队列方式公共更新缓存,去除重复。
- 数据库驱动缓存的更新如
- 先更新数据库,直接将key从缓存中删除,然后由数据库驱动缓存数据的更新。
- 读操作
- 先读缓存,如果缓存没有,则读数据库,同时将 数据库中读取的数据回写到缓存。
1.2 Read/Write Through(读写穿透)
- 适合场景
- 数据有冷热区分
- 描述
- 业务应用只关注一个存储服务即可,业务方的读写 cache 和 DB操作。 的操作,都由存储服务代理
- 存储服务 写操作
- 先查 缓存,
- 缓存存在 先更新 缓存,再更新db
- 缓存不存在 只更新db
- 先查 缓存,
- 存储服务读
- 先读缓存,缓存没有,读db,同时将db中数据回写到数据库。
1.3 Write Behind Caching(异步缓存写入)
- 适合场景
- 写频率超高,需要合并写请求的业务,一致性要求不高。
- 缺点
- 即数据的一致性变差,甚至在一些极端场景下可能会丢失数据,
- 描述
- 由数据存储服务来管理 cache 和 DB 的读写,数据更新 只更新缓存,不直接更新db,改为异步批量的方式更新db。
- 存储服务 写操作
- 先查 缓存,
- 缓存存在 先更新缓存,异步批量更新db。
- 缓存不存在 只更新db
- 先查 缓存,
- 存储服务读
- 先读缓存,缓存没有,读db,同时将db中数据回写到数据库。
1.4 好的db缓存方案
-
实时一致性方案
- 采用“先写 MySQL,再删除 Redis”的策略,这种情况虽然也会存在两者不一致,但是需要满足的条件有点苛刻,所以是满足实时性条件下,能尽量满足一致性的最优解。
- image.png
-
最终一致性方案
- 采用“先写 MySQL,通过 Binlog,异步更新 Redis”,可以通过 Binlog,结合消息队列异步更新 Redis,是最终一致性的最优解。
- 这种方案有个前提,查询的请求,不会回写 Redis。
- image.png
-
先删除 Redis,再写 MySQL,再删除 Redis(负责方案不推荐) 缓存双删
- image.png
- “删除缓存 10”必须在“回写缓存10”后面,那如何才能保证一定是在后面呢?网上给出的第一个方案是,让请求 A 的最后一次删除,等待 500ms。完全不行。
- image.png
3 缓存穿透,缓存击穿,缓存雪崩解决方案分析
1 缓存穿透
- 含义: 查询一个一定不存在的数据,然后导致查数据库,导致DB挂掉,有人利用不存在的key频繁攻击我们的应用,这就是漏洞
- 原因
- 系统设计,更多考虑的是正常路径,对特殊访问路径考虑欠缺。
- 解决方案:
- 布隆过滤器,所有可能存在的key hash到一个足够大的bitmap中。不存在的key,被bitmap拦截掉。
- 问题 如果数据量特别大,不合适
- 解决
- 10亿以内最佳(1.2GB),可以使用布隆过滤器
- 10亿 用10倍大小的位图存储, 也就是 100 亿的二进制
- 数据量过大,用布隆过滤器,缓存非法key
- 会导致key持续高速增长
- 要定期清零处理
- 10亿以内最佳(1.2GB),可以使用布隆过滤器
- 如果数据库查询为空(不管数据是不存在,还是系统故障),缓存都对空结果key缓存,过期时间会很短,最长不超过5分钟。
- 问题 如果访问大量不存在key,也会占用大量存储空间
- 解决
- 设计的时间短一些,让它尽快过期
- 设计一个独立的公共缓存非法key
- 查询先查 正常缓存组件,如果没有再查非法key的缓存。
- DB 查出来为空,就记录 非法key缓存
- 布隆过滤器,所有可能存在的key hash到一个足够大的bitmap中。不存在的key,被bitmap拦截掉。
2 缓存雪崩
-
问题描述 部分缓存节点不可用,导致整个缓存体系甚至服务系统不可用的情况。
-
解决办法
- 对业务db的访问增加读写开关
- 当db 请求变慢,阻塞,慢请求超过阈值时,就关闭读开关。
- 部分或所有读db的请求进行 failfast 立即返回,待db恢复后再打开读开关。
- 对缓存增加多个副本
- 缓存异常,或请求miss后,再读取其他缓存副本。
- 多个副本 尽量部署在不同 机架,保证可用。
- 缓存异常,或请求miss后,再读取其他缓存副本。
- 对缓存体系进行实时监控
- 当请求访问的慢速比超过阀值时,及时报警,通过机器替换、服务替换进行及时恢复;也可以通过各种自动故障转移策略,自动关闭异常接口、停止边缘服务、停止部分非核心功能措施,确保在极端场景下,核心功能的正常运行。
- 对业务db的访问增加读写开关
-
实际方案
- redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
- 本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
- redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
-
含义: 某一时刻缓存全部失效,可能是 设置了相同的过期时间,可能redis挂掉
-
解决方案:
- 大多数 加锁,或者队列方式
- 简单方案 在原有失效时间基础上增加一个随机值 比如1-5分钟随机。
- 做好集群
3 缓存击穿
- 含义 某一个key,在某个时间点被超高并发访问,恰好这个时间点过期了。 与雪崩区别这里 雪崩是很多key。
- 解决方案:
- 使用互斥锁 redis分布式锁
- 设置永不过期
- 如 我解析规则 一直不过期,如果修改了, 手动清除缓存。
- 通过后台定时刷新,根据缓存失效时间节点去批量刷新缓存数据
- 这个适合 Key 失效时间相对固定的场景。
- hystrix 做资源隔离
4 如何保证缓存与数据库双写的一致性
- 读
- 先读缓存
- 缓存没有 读数据库,然后取出数据放入缓存,同时响应
- 先读缓存
- 更新
- 先更新数据库,然后删除缓存
- 为什么是删除缓存,而不是更新缓存
- 在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值
- 问题:
- 先更新数据库,删除缓存失败
- 导致 数据库是新数据,缓存时旧的数据
- 解决思路:
- 先删除缓存,再更新数据库
- 如果数据库修改失败,缓存时空的,数据库是旧的。
- 先删除缓存,再更新数据库
- 先更新数据库,删除缓存失败
- 为什么是删除缓存,而不是更新缓存
- 最终 先删除缓存,再更新数据库
- 问题:
- 大并发,还没修改,但是缓存时空的,查到缓存时空的。
- 解决思路
- 还是要串行,考虑如何串行
- 队列
- 更新缓存 不管是读,还是写, 生成唯一标识,放入java队列中。
- 队列还可以做过滤,相同的更新缓存没有意义。
- 如 写到缓存后,后面读 可以直接从缓存中拿。
- 队列还可以做过滤,相同的更新缓存没有意义。
- 更新缓存 不管是读,还是写, 生成唯一标识,放入java队列中。
- 锁实现呢?
- 队列
- 还是要串行,考虑如何串行
- 问题:
- 先更新数据库,然后删除缓存
5 缓存失效
- 因为很多key 设置了相同过期时间,导致一起失效
- 解决
- 过期时间=baes时间+随机时间
6 如何设计一个动态缓存热点数据的策略
- 由于数据存储受限,系统并不是将所有数据都需要存放到缓存中的,而只是将其中一部分热点数据缓存起来
- 策略思路
- 判断数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据
- 如商品数据
- 先通过缓存系统做一个排序队列(如存放1000个商品),系统根据商品访问时间 正序排序
- 同时缓存系统定期过滤掉最后 200 个商品, 再从数据中 随机读取200 个商品,加入队列。
- 这样 请求每次到达的时候,先从队列获取商品ID,如果命中,再根据ID 从另一个缓存结构 读取商品信息读取实际的商品信息,并返回。
- 如商品数据
- 判断数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据
7 热key 问题
-
问题
- 热key,大流量,直接打到一个缓存机器,这个缓存机器很容易到达 CPU,网卡,带宽极限,从而导致缓存访问变慢。
-
解决
- 找到热key
- 提前评估可能出现的热key
- 突发的可以用 spark,flink 进行事实分析
- 之前发生的,可以通过hadoop 离线计算。 找到最近历史数据中的热 key
- 将热 key 分散,
- 如 hotkey 被分散成 hotkey#1, hotkey#2, ... hotkeyn。 n 就是key分散的多个缓存节点。
- 客户端访问 随机 hotkey 1-n 的后缀。
- 也可以
- key 名字不变,对缓存提前进行多副本,多级结合的缓存架构
- 对缓存监控,快速扩容来减少热key的冲击
- 业务端将热key记录在本地缓存。
- 找到热key
8 大key 问题
- 方案
- 设计缓存阈值,当value 超过阈值,进行压缩
- 大value 对于结构元素很多 如set, 可以进行序列化构建,通过restore 一次性写入。
- 将大value拆分,,尽量减少大 key 的存在
- 由于一些大key 一旦穿透到DB,加载耗时,可以设置过期时间长一些。
9 数据不一致
-
问题
- 如 更新 DB 后,写缓存失败,导致缓存的是老数据
- 采用 rehash 自动漂移策略,多个副本数据不一致
-
解决
- 缓存更新失败以后,进行重试
- 如果重试失败 ,缓存服务出了问题, 写入MQ服务, 缓存服务恢复,从MQ 删除。
- 缓存设置较短的时间,让缓存及时过期。
- 不采用rehash 飘逸策略,而采用缓存分层策略。
- 缓存更新失败以后,进行重试
10 懒加载的缓存过期方案,性能毛刺 23讲搞定后台架构实战
- 懒加载的缓过过期方案
- Redis 作为主存储,MySQL 作为兜底来构建
- 当读服务接受请求时,会先去缓存中查询数据,如果没有查询到数据,就会降级到数据库中查询,并将查询结果保存在 Redis 中,以供下一次请求进行查询。保存在 Redis 中的数据会设置一个过期时间,防止数据库的数据变更了,请求还一直读取缓存中的脏数据
- 性能毛刺
- 当缓存过期时,读服务的请求都会穿透到数据库中,对于穿透请求的性能和使用缓存的性能差距非常大,时常是毫秒和秒级别的差异。
- 解决方案
- 全量缓存(适合读类型的业务)
- 将数据库种的所有数据都存储在缓存种,同时在缓存种不设置过期时间的一种实现方式。
-
image.png
- 没有解决分布式事务问题,反而把问题放大了。
- 基于 Binlog 的全量缓存的基本架构
- binlog 的开源工具
- Canal
- mysql_Streamer
- Maxwell
- Databus
- image.png
- image.png
-
image.png
- 优化
- image.png
- 优化
- 缺点
- 提升了系统的整体复杂度
- 整个资源同步 的流程变长,且关注点合出错点由一个中间件变成了两个
- 缓存的容量会成倍上升,响应的资源成本也大幅上升
- 在一些对性能要求极致且是实时性高的场景下,只能进行取舍。
- 优化
- 缓存数据进行筛选,有业务含义且被查询
- 存储在缓存种的数据可以进行压缩。
- 数据json 序列号时候,字段上添加替代标识。 - image.png
- 如果使用的redis hash 结构。hash结构 field 字段 也可以用 json 标识一样的模式
- 使用全量缓存 承接所有请求时候,会出现无法感知缓存丢失问题。
- image.png
- image.png
- image.png
- 提升了系统的整体复杂度
- 技巧
- redis 还是可能丢失数据,使用 异步校准加报警及自动化补齐的方式来应对
- 从缓存获取数据
- 通过mq通知对比程序
- 对比程序根据条件 查询数据库
- 进行对比,不一致进行告警,并自动把数据刷新至缓存。
- redis 还是可能丢失数据,使用 异步校准加报警及自动化补齐的方式来应对
- binlog 的开源工具
- 全量缓存(适合读类型的业务)
11 数据并发竞争
- 描述
- 在高并发访问场景,一旦缓存访问没有找到数据,大量请求就会并发查询 DB,导致 DB 压力大增的现象
- 主要是同一个key
- 解决
- 1 使用全局锁
- 2 对缓存数据保持多个备份,即便其中一个备份中的数据过期或被剔除了,还可以访问其他备份,从而减少数据并发竞争的情况
4 常见系统设计
1 秒杀系统缓存解析
- 设计原则
- 尽力将请求拦截在系统上游,减轻后端压力。
- 充分利用缓存,提高系统性能和可用性。
- 架构设计
-
前端静态资源 cdn 前置
-
前端请求限制
-
负载均衡分发请求
-
web 服务预先处理
- 权限检测
- 服务前置检查
-
业务请求处理
- 所有处理交给缓存
- 后续事务操作 通过MQ 缓存,降低 db压力。
-
2 海量计数缓存解析
- chang