1. 缓存的收益与成本
1. 收益:
-
加速读写
- 读缓存中的数据要比读数据存储位置的速度要快。(例如:寄存器-内存,内存-磁盘)
-
降低后端负载
- 如果不加缓存的话,并发的压力会直接加到后端数据库上,并发较大的时候,数据库可能会hold不住,而这里可以通过增加一层缓存,通过直接访问缓存不仅访问速度更快,而且减少了io次数,减少了数据库的压力。
2. 成本:
2. 缓存更新策略
1. LRU/LFU/FIFO(一致性差,维护成本低)
2. 超时剔除: 例如expire(一致性差,维护成本低)
对于一些可以容忍数据不一致性的情况,可以使用这种策略。
3. 主动更新:开发控制生命周期(一致性较强,维护成本较高)
可以利用发布-订阅这种思想,在缓存来监听数据是否发生变化,若发生变化则失效. (例如volatile关键字的实现,缓存一致性协议中的嗅探机制,就是使用的这一策略, 也可利用消息队列来维护最终一致性)
低一致性: 最大内存和淘汰策略
高一致性:超时剔除和主动更新结合,最大内存和淘汰策略兜底(防OOM)。
3. 缓存的粒度控制
缓存的粒度,以缓存mysql中的数据为例,例如查询用户信息操作频繁,那么应该缓存用户信息的所有属性(select *) 还是应该缓存用户信息的部分属性(例如 select id, name, gender, age,…) 这就是一个粒度的选择.
4. 缓存穿透 - 大量请求不命中
4.1 问题:
当请求到来时,会先去查缓存,缓存中没有,然后才会“穿过”缓存层访问数据库,如果数据库中存有请求的结果,那么会将结果数据写到缓存中。 但如果访问的是数据库中也没有的记录,那么缓存中也不会存储。 当有大量请求访问数据库中不存在的数据时,那么缓存也就形同虚设,大量的并发直接落在了数据库上。
4.2 产生的原因:
1. 业务代码自身的问题
例如对于一些不合理的查询请求在业务代码层面上没有过滤掉等。
2. 恶意攻击、爬虫等
4.3 如何发现:
1. 业务的响应时间
如果出现了穿透,请求打到了存储层上,响应时间一定会收到影响。
2. 采集相关指标(对缓存进行监控):总调用数、缓存层命中数、存储层名中数
4.4 解决方法
方法一:把查询空结果也给缓存起来
- 但这样会出现两个问题:
- 对于恶意攻击来说,他们可以通过组合不同的键来查询空结果,所以穿透依然无法避免。
- 如果在查询某个关键的key的时候,查询接口因为一些意外原因(如网络延迟过大)而导致了查询到了空结果,在把这个空结果给缓存了之后,在其失效之前,对于这个key的查询得到的结果总是空的,但这个期间,有可能查询接口又恢复正常了(但却因为缓存缓存了空结果,所以还是查询不到)。
方法二: 布隆过滤器拦截
布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data
structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
因此,在查询的时候,先去布隆过滤器查询,只有对于通过了布隆过滤器的查询,才会真正的去执行查询. 但这并不能100%的过滤掉所有空查询,因为布隆过滤器可以保证不通过的一定不存在,但不能保证通过的一定存在。
缓存穿透应该是
当数据库中没有某个key对应的value时,缓存中也不会有该value的缓存。所以大量的对该value的查询该的请求会绕过缓存,直接查询数据库。缓存中一般存的是 key+value ,但是布隆过滤器却可以告诉你 key
对应的value在数据库中存不存在,如果不存在就不用查询数据库了。拿redis为例子: 请注意,用 redis 也可以做到判断 key 对应的value
在数据库中存不在,那就是把数据库里的所有value对应的key都储存在redis
中,而value可以为空,然后判断下key.IsExists()就可以了,但是这无疑会浪费大量空间,因为存储了数据库中所有的key。而且这也不符合缓存的初衷:咱不能暴力的把所有key都存下来,而是查询了啥key,我们缓存啥key。而布隆过滤器是一种非常高效的数据结构,把所有数据库的value对应的key
存储到布隆过滤器里,几乎不消耗什么空间,而且查询也是相当的快!但是请注意,它只能判断 key 是否存在(而且会有一定的误差)。所以一个查询先通过布隆顾虑器判断key是否存在(key 对应的value是否存在数据库中),如果不存在直接返回空就好了。
那么布隆过滤器是怎么做到几乎不消耗空间来储存所有的key,并快速判断特定的key是否存在呢?
其实原理很简单,布隆过滤器 只是一个 byte数组,再加上几个映射函数。
每个key 都通过一系列映射函数,得到一系列的的值k,然后在这个byte数组上的把k下标的值变成1。
当要判断key是否存在时,通过映射函数映射得到的一系列k,查看byte数组相应下标k对应的值是否为1,如果有一个不为1,那么一定不存在。如果都是1
,那么可能存在。为什么可能而不是一定呢?因为这是一个误差问题,有可能别的key把某个k的位置变成了1,key越多时,误差越大。但是放心不会很大的,这是可以控制的,byte数组越长,误差越小。
5. 缓存雪崩
5.1 问题
一般情况下,缓存层将接受大量的服务请求,存储层只接受比较少的服务请求,但当缓存层发生异常/脱机(总之暂时无法工作)或是是指在某一个时间段,缓存集中过期失效,那么流量直接压向后端组件(例如数据库,或第三方API),造成级联故障。
级联故障的解释:
网络中,一个或少数几个节点或连线的失效会通过节点之间的耦合关系引发其他节点也发生失效,进而产生级联效应,最终导致相当一部分节点甚至整个网络的崩溃,这种现象就称为级联失效,有时也形象称之为“雪崩” 。
5.2 优化方案
1. 保证缓存的高可用性
例如 Redis的主从机制,主机挂了从机上.
- Redis Sentinel
- Redis Cluster
- 主从漂移
2. 依赖隔离组件为后端限流
- Hystrix这种隔离组件
- 使用线程池/信号量隔离组件
- 使用Guava提供的限流API(令牌桶,漏桶)
3. 提前演练:例如压力测试
4. 数据预热
数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
6. 无底洞问题优化*
6.1 问题
加机器之后,性能不但没有提升,反而下降了。(因为加的机器多了,网络请求次数也多了,开销也大了)
6.2 优化IO的几种方法
- 命令本身优化:例如慢查询keys、hgetall bigkey
- 减少网络通信次数(无底洞问题主要优化的位置)
- 降低接入成本:例如客户端长连接/连接池、NIO等
6.3 四种批量优化的方法
- 串行meget
- 串行io
- 并行io
- hash_tag