一提到Cache,就想到08年我为公司写的消息缓存系统的惨痛教训。当时Redis与Memcached远还没有流行,公司对使用开源项目也是慎重,于是我和另一个同事自己撸了一个系统,但做着做着就变成一个带有强业务逻辑的Cache了。后面又扩大他的使用场景,也导致了一些问题。这个系统的要满足如下场景:
- 针对消息对象缓存,每个消息都非常小,要高效地使用内存
- 存在定时消息,当定时到了,需要回到业务系统中去调度
- 消息有优先级与时序性,要支持按不同的属性来索引(消息ID,发送人,收件人等)
- 消息量非常大,缓存需要有淘汰机制,支持淘汰的消息本地文件存储(相当于多级缓存,本地文件存储要求高效索引)
从上面的场景来,它比纯Key/Value的缓存复杂,即要高效使用内存,同一个Value缓存,存在多个Key映射,而Value只能缓存一份,Value有优先级与时序性,索引时需排序处理,又有点消息队列的诉求。
今天,我们大量在使用Redis来做缓存,Redis只作为Key/Value存储,上层复杂的缓存相关业务逻辑是在其外来叠加实现。但由于对于业务系统来说,永远都是具体情况具体分析,没有最好,只有最合适,所以也不得不要考虑通用问题:缓存穿透、缓存雪崩,缓存击穿。
缓存穿透
缓存系统一般都是按照key去缓存查询,如果不存在对应的value,就应该去后端系统(如DB)查找。如果key对应的value是一定不存在的,并且对该key并发请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
解决办法:
- 对空结果缓存,缓存时间设置较短,当该key对应的数据有抛入时更新
- 对Key进行过滤,设计Key有一定的规范,当Key满足规范时才去后端查找。
- 布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统带来很大压力。
解决办法:
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
- 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。
- 做二级缓存,A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期(此点为补充)
缓存击穿
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,导致大并发的请求可能会瞬间把后端DB压垮。这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
解决办法:
- 使用互斥锁(mutex key):如使用Redis的SETNX(SET if Not eXists,只有不存在的时候才设置,可以利用它来实现锁的效果)
- “提前"使用互斥锁(mutex key):在value内部设置1个超时值(timeout1), timeout1比实际的cache timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。
- “永远不过期”:对于热点数据的Key,快过期时预先加载,难点在于热点数据如何统计。
- 过载保护:应用层的过载保护,比如API调用降级,避免对后端系统高并发访问。
参考: