5.1说说什么是缓存穿透?
缓存穿透是用户要访问的数据,既不在缓存中,也不在数据库中,导致请求在访问时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么如果有大量这样的请求到来时,数据库的压力剧增,这就是缓存穿透。
【发生场景】:
(1)业务误操作,把数据库和缓存中的数据都误删除了,所以导致缓存和数据库中都没有数据;
(2)黑客恶意攻击,故意大量访问业务中不存在的数据。
【解决方案】:
(1)非法请求的限制:当有大量恶意请求访问不存在的数据的时候,也会发生缓存穿透,因此在API入口我们要判断请求参数是否合理,请求参数是否含有非法值、请求字段是否存在等。如果判断出是恶意请求就直接返回错误,避免进一步访问缓存和数据库。
(2)缓存空值或者默认值:当用户第一次访问的时候,如果在缓存中不存在该数据,在数据库中也没有该数据,就直接在缓存中缓存一个空值,这样后续请求可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。
(3)使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在了。即使发生了缓存穿透,大量请求只会查询Redis和布隆过滤起,而不会查询数据库,保证了数据库能正常运行,Redis自身也是支持布隆过滤器的。
【布隆过滤器的工作流程】:
布隆过滤器由初始值都为0的位图数组和N个哈希函数两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
(1)使用N个哈希函数分别对数据做哈希计算,得到N个哈希值。
(2)将第一步得到的N个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置。
(3)将每个哈希值在位图数组的位置位置的值设为1。
打个比方,以位图数组长度为8,哈希函数为3个的情况为例,在数据库写入x数据时,把数据x标记在布隆过滤器时,对x数据分别计算3个哈希函数取哈希值,然后取模数组的长度,得到位图数组的下标,然后将这些下标对应的值标记为1。当下次查询布隆过滤器时,直接使用这三个哈希函数,来求得位图中的下标数组是否为1,如果有一个不为1说明该数据不在数据库中,就可以直接返回错误信息给客户端。
布隆过滤器由于是基于哈希函数实现的,所以会存在哈希冲突的问题。查询布隆过滤器说数据存在,不一定证明数据库中真的存在这个数据,但是如果布隆过滤器说这个数据不存在,那就一定不存在这个数据。
5.2说说什么是缓存击穿
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,无法从缓存中读取的话,就会去访问数据库,数据库很容易就被高并发的请求压垮,这就是缓存击穿。
【解决方案】:
(1)不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期时,提前通知后台线程更新缓存以及重新设置过期时间。
(2)互斥锁方案,保证同一时间只有一个业务线程更新缓存,没能取得互斥锁的线程,要么等待锁释放重新读取缓存,要么就返回空值或者默认值。
5.3说说什么是缓存雪崩
通常我们为了保证缓存中的数据与数据库中数据的一致性,会给Redis里的数据设置过期时间,当缓存数据过期后,用户访问的数据如果不在缓存里,业务系统需要重新生成缓存,因此就会访问数据库,并将数据更新到Redis里,这样后续请求都可以直接命中缓存。
当大量缓存数据在同一时间过期(失效)或者Redis实例故障宕机时,如果此时有大量的用户请求,都无法在Redis中处理,请求全部直接访问数据库,导致数据库的压力骤增,严重的会造成数据库宕机,这就是缓存雪崩。
【大量数据同时过期】:
(1)均匀设置过期时间:如果要给缓存数据设置过期时间,应该避免大量的数据在同一时刻过期。可以在数据进行缓存时,给这些数据的过期时间加上一个随机数,保证数据不会在同一时间过期。
(2)互斥锁:比如使用ReenTrantLock的trylock(),如果尝试获取锁成功,就让这一个业务线程从数据库中去获取数据并写入到缓存中,在finally块中释放锁。其他的线程让他睡上个100ms,然后再去执行读Redis的逻辑。
(3)后台更新缓存:业务线程不负责更新缓存,缓存也不设置有效期,而是让缓存永久有效,把更新缓存的工作交给后台线程来完成。事实上,缓存数据不设置有效期,并不意味着数据一直能在内存中,因为当系统内存紧张的时候,有些缓存数据会被淘汰。所以解决方案是,如果业务线程发现缓存数据失效后,通过消息队列通知后台线程更新缓存,后台线程收到消息后,在更新缓存前可以判断缓存中是否存在该数据,不存在就读取数据库加载缓存。
缓存预热:在业务刚上线的时候,我们最好是提前把数据缓存起来,而不是等待用户访问才触发缓存构建。
【Redis故障宕机】:
(1)服务熔断、降级或请求限流:因为Redis故障宕机而导致缓存雪崩时,可以启动服务熔断机制,暂停业务应用对缓存服务的访问,直接返回友好的提示信息,不再访问数据库从而降低数据库的压力。
请求限流:只将少部分请求发送到数据库进行处理,再多的请求来就在入口直接拒绝访问,等到Redis恢复正常再解除限流机制。
【构建Redis高可靠集群】
通过配置Redis的高可靠集群和哨兵集群,如果Redis从库发生故障时,整个业务系统还可以正常运行;如果Redis主库发生故障,由Leader哨兵进行选主、通知的过程,选择一个新主库继续访问用户的写请求。
5.4说说Redis的数据淘汰策略
Redis缓存使用内存来保存数据,避免应用直接从后端数据库中读取数据,提高应用的响应性能。根据八二定律:80%的请求实际上只访问了20%的部分,所以我们可以根据这个原理,来设置Redis缓存的最大容量。另外,当Redis的缓存被写满了以后,会不可避免得淘汰掉一部分数据。
Redis提供了8种数据的淘汰策略:分别是不进行数据淘汰的noevtction,设置了过期时间的数据进行淘汰的volatileRandom、volatileTTL、volatileLRU、volatileLFU,以及在所有数据范围内进行淘汰的allkeysRandom、allkeysLRU、allkeysLFU。
【noeviction策略】
如果设置的是noevicition策略,在默认情况下,即使Redis在使用的内存空间超过maxmemory值时,也不会淘汰数据。一旦缓存被写满,Redis就不再提供服务,而是直接返回错误信息给客户端。这种策略一般都不会生产环境中使用。
【带过期时间的策略】
这四种淘汰策略,它们筛选的候选数据范围,被限制在设置了过期时间的键值对上。也正是因为如此,即使缓存没有写满,这些数据过期了也会被删除。例如,我们使用expire命令对一批键值对设置了过期时间后,无论这些键值对的过期时间快到了还是Redis的内存使用量达到了阈值,Redis都会按照这四种策略进行数据淘汰。
- volatileRandom:在设置了过期时间的键值对中,进行随机删除;
- volatileTTL:在设置了过期时间的键值对中,根据过期时间的先后顺序进行删除,越早过期的越先删除;
- volatileLRU:在设置了过期时间的键值对中,根据LRU算法进行删除;
- volatileLFU:在设置了过期时间的键值对中,根据LFU算法进行删除;
【所有数据范围内的策略】
这三种淘汰策略的备选淘汰数据范围,扩大到了所有的键值对,不管它有没有设置过期时间。如果一个键值对被淘汰策略选中了,即使它的过期时间还没到,也需要被删除。当然,如果它的过期时间到了,但未被策略选中,同样也会被删除。
【AllKeysLRU】
根据LRU算法进行数据淘汰,需要维护所有的数据维护一个链表的数据结构,当键值对被访问时或者是插入新数据时,会移动到链表的头部;当Redis的缓存容量不够时,会优先淘汰掉链表末尾的数据。
LRU算法的想法非常朴素,它认为刚刚被访问的数据,将来肯定还是会再次被访问;长久不访问的数据可能以后也不会被访问了,所以在淘汰时优先删除它。不过LRU算法在具体的实现上,需要用链表来管理所有的缓存数据,会带来额外的空间开销,而且当数据被访问时需要频繁移动链表,这样的操作会影响Redis的性能。
Redis在LRU算法的具体实现上进行了简化,以减轻数据淘汰对缓存性能的影响。Redis默认会记录每个数据的最近一次访问的时间戳(由键值对数据结构中RedisObject中的lru字段记录)。然后Redis在决定淘汰的数据时,第一次会随机选出N个数据,把它们作为一个候选集合。接下来在进行数据淘汰的时候,会比较这N个数据的lru字段,把lru字段最小的值从缓存中删除掉。
当需要再次淘汰数据时,Redis需要挑选第一次进行数据淘汰的候选集合,这时挑选数据的标准是:能进入候选集合的数据的lru字段值必须小于候选集合中最小的lru值,然后如果缓存满了就把lru值最小的键值对从缓存中删除。
【应用场景】:
(1)优先使用AllKeys-LRU策略。这样可以充分利用经典LRU算法,把最常访问的数据留在缓存中,把最少访问的数据淘汰出去,提升应用的响应速度。如果业务数据中有冷热数据区分的话,使用AllKeys-LRU策略是最合适的。
(2)如果业务中有置顶的需求,希望一部分数据尽量不要被淘汰,例如置顶新闻、置顶视频等等。可以使用volatile-LRU策略,同时不给置顶的数据设置过期时间。而其他的数据在内存不足或到期时会根据LRU规则进行数据淘汰。
(3)如果业务中的数据访问频率都差不多,且没有明显的冷热数据区分的话,可以使用AllKeysRandom策略,随机淘汰数据就行。