百万QPS下的缓存架构与设计
多级缓存
本地缓存
ConcurrentHashMap
存最简单的键值对,coder自己维持过期策略
guava的LocalCache
仅支持LRU,需要重写load
和 loadAll
方法,支持指定过期时间和重刷时间
caffeine
Caffeine 因使用了 Window TinyLfu 回收策略,提供了一个近乎最佳的命中率。
三种填充策略:手动, 同步填充指定一个从key到value的function, 异步填充buildAsync.
三种回收策略:
- 基于大小回收:指定个数或者权重。可以为每个kv指定一个权重
- 基于时间回收:访问之后多久, 创建之后多久, 自定义
- 基于引用回收:将key和value指定为WeakReference,当没有强引用时被gc掉。配置soft-reference来启动lru的gc.存储大对象的时候,即使外部程序没有在用,但大对象还是被缓存里的map强引用着,不能被回收掉。所以使用弱引用就是当外部没有在用这个k-v时,将它们清理掉。这两种是可以跟基于时间的回收策略结合使用的。
分布式缓存 - memcached
Easy to use, distributed, in-memory key-value store for use as a high performance cache or session store.
数据结构简单,只支持单纯的k-v结构
多线程
没有持久化
分布式:因为不同server间无感知,所以高可用由客户端实现
高可用:因为不同server间无感知,所以高可用由客户端实现
分布式缓存 - redis
风险及应对
缓存穿透
请求一个缓存中不存在的值,这样每次请求都会落到db里去。于是就对DB造成了压力,一般来说这种要分两种情况。正常的请求和黑客的请求。
缓存控制
对于正常的请求,请求内容也就是key一般是不变的,这个key在db中确实也没有数据。这种情况我们可以把空值缓存下来。
BloomFilter
对于黑客的请求,比如爬虫或者网络攻击,如果我们大量的缓存不存在的值很可能会把我们的缓存容量打满,导致正常值被逐出。这不是我们想要看到的。所以最好是在前端就能将非法请求过滤掉。假设我们有一个服务是根据用户id请求用户信息,我们首先要保证黑客拿到的用户id是加密过的也就是无法进行累加遍历,即使可累加遍历也可以在前面就被我们过滤掉。这部分再深入些就是安全方面的知识了,暂且不谈。
在前面提到的反爬的基础上,我们再请求缓存或db之前加一层布隆过滤器,如果这个key存在于缓存或db中就进行查询,否则就不进行查询。
缓存雪崩
缓存承担了大部分的请求,当缓存层由于某些原因失效时,大量请求就会直接到db层,db扛不住就挂掉了。
预防缓存层挂掉:
- 缓存的高可用,不让他挂掉
- 多级缓存做备份:一个缓存层挂了,另一个缓存层顶上
- 缓存失效时间随机,避免同一时刻大量缓存失效。
缓存层挂掉之后产生的问题:大量线程进入缓存层重建的代码,进行海量的请求db.那么在缓存重建时我们最重要的目的就是减少重建缓存的次数,为此可以牺牲部分可用性。
即使用互斥锁将重建缓存的代码保护起来,也即是保证同一时刻只有一个key的请求访问db.
缓存热点
大量请求访问同一个条目,即缓存集群中的某一台机器承担了大量的流量。想一下导致的后果:大量请求把这台缓存实例打挂了,如果是memcached,一致性hash之后到达了另一台机器,另一台机器大量读db,因为我们做了缓存雪崩的预防我们抗住了,但是流量还是扛不住于是又打挂了,以此往复,整个缓存集群都挂了。。。
上面举的例子当然是一个比较极端的case.实际上memcached单机支持百万QPS问题是不太大的,就算是redis支持十万QPS也是足够的。而在真实场景中,除了像什么明星离婚结婚,大型活动开始,应该是比较难遇到。
如果真的遇到了那么我们该怎么办?
- 通过流式计算技术(storm)计算出热点key并存放的zookeeper中
- 代码中监听zookeeper的对应节点变化来加载到本地缓存,设置一个较长的过期时间,直接用本地缓存扛。
如上极端的热点场景实际并没有遇到过,纸上得来终觉浅,以上仅供参考
大key
分散