缓存可以加速系统的读写速度,同时也可以减轻后端数据库的负载。将缓存加入系统中后,难免会出现一些问题,下面介绍相关的解决方案。
缓存穿透
缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。整个过程分为:
-
缓存层不命中。
-
存储层不命中,不将空结果写回缓存。
-
返回空结果。
缓存穿透将导致不存在的数据每次请求都要到存储层去查询,会使后端存储负载加大,由于很多后端存储不具备高并发性,甚至可能造成后端存储宕掉。通常可以在程序中分别统计总调用数、缓存层命中数、存储层命中数,如果发现大量存储层空命中,可能就是出现了缓存穿透。
造成缓存穿透的基本原因有两个:
-
自身业务代码或者数据出现问题。
-
一些恶意攻击、爬虫等造成大量空命中。
优化方案
缓存空对象
当存储层不命中后,仍然将空对象保留到缓存层中,之后再访问这个数据将会从缓存中获取,这样就保护了后端数据库。缓存空对象会有两个问题:
- 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
- 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。例如过期时间设置为5分钟,如果此时缓存层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致,此时可以利用消息系统或者其他方式清除掉缓存层中的空对象。
布隆过滤器拦截
简介
布隆过滤器(Bloom Filter)是由位数组和一组随机映射函数(哈希函数)两部分组成的数据结构。相比于平时常用的的List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
当一个元素加入布隆过滤器中的时候,会进行如下操作:
-
使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
-
根据得到的哈希值,在位数组中把对应下标的值置为1。
当需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:
-
对给定元素再次进行相同的哈希计算。
-
得到值之后判断位数组中的每个元素是否都为1,如果值都为1,那么说明这个值在布隆过滤器中;如果存在一个值不为1,说明该元素不在布隆过滤器中。
不同的字符串可能哈希出来的位置相同,这种情况可以适当增加位数组大小或者调整哈希函数。
总结:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
具体方案
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。
例如:一个推荐系统有4亿个用户id,每个小时算法工程师会根据每个用户之前历史行为计算出推荐数据放到存储层中,但是最新的用户由于没有历史行为,就会发生缓存穿透,为此可以将所有推荐数据的用户做成布隆过滤器。如果布隆过滤器认为该用户id不存在,那么就不会访问存储层,在一定程度保护了存储层。
方案对比
解决方案 | 使用场景 | 维护成本 |
---|---|---|
缓存空对象 | 数据命中率不高,而且频繁变化 | 代码维护简单,需要过多的缓存空间,数据不一致 |
布隆过滤器拦截 | 数据命中率不高,而且相对固定 | 代码维护复杂,缓存空间占用少 |
缓存雪崩
缓存雪崩是指缓存层由于某些原因不能正常提供服务,于是所有的请求都会来到存储层,导致存储层的调用量暴增,造成存储层也会级联宕机的情况。
优化方案
- 保证缓存层服务高可用性(发生前)。如果缓存层设计成高可用的,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。可以使用Redis Sentinel和Redis Cluster实现高可用。
- 依赖隔离组件为后端限流并降级,另外设置一个小缓存(发生中)。降级机制在高并发系统中是非常普遍的,可以使用Java依赖隔离工具Hystrix来实现限流和降级。同时可以在系统内部设置一个小型的本地缓存,可以用ehcache来实现这个功能。
- 恢复缓存数据(发生后)。可以使用Redis的持久化机制,利用保存的数据来恢复缓存。