多级缓存

每一级缓存的意义

时效性高的数据:采取DB和redis缓存双写方案

时效性不高的数据:采取nginx本地缓存+redis分布式缓存+tomcat堆缓存的多级缓存架构

a: nginx本地缓存,抗的是热数据的高并发访问。利用nginx本地缓存,将热数据锁定在nginx的本地缓存内,那么对这些热数据的大量访问,就直接走nginx就可以,不需要走后续的各种网络开销了。

b: redis分布式大规模缓存,抗的是很高的离散访问,支撑海量的数据,高并发的访问,高可用的服务。最完整的数据和缓存。

c: tomcat jvm堆内存缓存,主要是抗redis大规模灾难的,如果redis出现了大规模的宕机,导致nginx大量流量直接涌入数据生产服务,那么最后的tomcat堆内存缓存至少可以再抗一下,不至于让数据库直接裸奔, 同时tomcat jvm堆内存缓存,也可以抗住redis没有cache住的最后那少量的部分缓存。

Redis

Redis Cluster

通过master的水平扩容,来横向扩展读写吞吐量,还有支撑更多的海量数据

redis cluster 高可用性:redis cluster 提供主备切换。slave做master的热备,一旦master故障。slave提升为master,对外提供服务,保证集群的高可用性。并且,当master恢复后,会作为 slave加入到集群中。

redis cluster水平扩容

master的水平扩容,来横向扩展读写吞吐量,还有支撑更多的海量数据

slave 自动迁移

为redis cluster 添加冗余slave

redis性能(需根据机器配置测试)

redis单机,读吞吐是5w/s,写吞吐2w/s

扩展redis更多master,那么如果有5台master,不就读吞吐可以达到总量25w/s QPS,写可以达到10w/s QPS

redis单机,内存,6G-8G,内存不易过大fork类操作的时候很耗时,会导致请求延时的问题。扩容到5台master,能支撑的总的缓存数据量就是30G

Cache Aside模式

1
2
(1)读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应
(2)更新的时候,先删除缓存,然后再更新数据库

DB和缓存双写不一致问题以及解决方案

缓存不一致场景一:

1
2
3
4
  解决思路:

先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致
因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中

缓存不一致场景二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 解决思路:
a:更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部的队列中
b:读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中
c:一个队列对应一个工作线程
d:每个工作线程串行拿到对应的操作,然后一条一条的执行
这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新
此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成
e: 多个更新缓存请求处理:这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可
待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中
如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值
f:多个读请求,进行读请求过滤:对一个商品的库存的数据库更新操作已经在内存队列中了
然后对这个商品的库存的读取操作,要求读取数据库的库存数据,然后更新到缓存中,多个读
这多个读,其实只要有一个读请求操作压到队列里就可以了
其他的读操作,全部都wait那个读请求的操作,刷新缓存,就可以读到缓存中的最新数据了
如果读请求发现redis缓存中没有数据,就会发送读请求给库存服务,但是此时缓存中为空,可能是因为写请求先删除了缓存,也可能是数据库里压根儿没这条数据
如果是数据库中压根儿没这条数据的场景,那么就不应该将读请求操作给压入队列中,而是直接返回空就可以了

大value缓存的全量更新效率低下问题

缓存数据的维度化拆分

缓存数据生产服务工作流程

(1)监听多个kafka topic,每个kafka topic对应一个服务(简化一下,监听一个kafka topic)

(2)如果一个服务发生了数据变更,那么就发送一个消息到kafka topic中

(3)缓存数据生产服务监听到了消息以后,就发送请求到对应的服务中调用接口以及拉取数据,此时是从mysql中查询的

(4)缓存数据生产服务拉取到了数据之后,会将数据在本地缓存中写入一份,就是ehcache中

​ 同时会将数据在redis中写入一份

缓存并发重建冲突解决方案

重建缓存:比如数据在所有的缓存中都不存在了(LRU算法弄掉了),就需要重新查询数据写入缓存,重建缓存

1
2
3
4
5
6
缓存重建存在的问题一:
缓存数据生产服务在多个机器节点上部署了多个实例

若没有缓存数据。12:00的时候发来一个读请求 12:01发来一个读请求(此时12:00的读请求由于网络延迟还未执行完)。12:01请求比12:00的请求执行速度快。更新了生产服务的数据并将数据写入缓存。写完后。12:00的请求将数据写入了缓存。那么此时生产服务的最新数据是12:01的,但是缓存中是服务数据是12:00的。数据不一致。

解决思路:对请求的数据ID 进行hash,让对同一个数据的请求落在同一个服务实例上
1
2
3
4
5
6
7
8
9
缓存重建存在的问题二:

生产服务发送的变更消息到kafka。由于问题一解决方案中的hash算法与kafka分区策略不一致。数据变更的消息所到的缓存服务实例,跟请求分发到的那个缓存服务实例也许就不在一台机器上了



(1)变更缓存重建以及空缓存请求重建,更新redis之前,都需要先获取对应商品id的分布式锁
(2)拿到分布式锁之后,需要根据时间版本去比较一下,如果自己的版本新于redis中的版本,那么就更新,否则就不更新
(3)如果拿不到分布式锁,那么就等待,不断轮询等待,直到自己获取到分布式的锁

缓存雪崩问题

1
2
3
4
5
6
7
8
9
10
11
12
13
缓存雪崩产生场景:

1、redis集群彻底崩溃
2、缓存服务大量对redis的请求hang住,占用资源
3、缓存服务大量的请求打到源头服务去查询mysql,直接打死mysql
4、源头服务因为mysql被打死也崩溃,对源服务的请求也hang住,占用资源
5、缓存服务大量的资源全部耗费在访问redis和源服务无果,最后自己被拖死,无法提供服务
6、nginx无法访问缓存服务,redis和源服务,只能基于本地缓存提供服务,但是缓存过期后,没有数据提供
解决思路
1、对redis访问做资源隔离
2、若redis集群崩溃,对redis进行熔断
3、对源服务的访问做限流
4、限流失败后采用stubbed fallback降级机制

缓存穿透问题

每次如果从生产查询到的数据是空,就说明这个数据根本就不存在

那么如果这个数据不存在的话,我们不要不往redis和ehcache等缓存中写入数据,我们呢,给写入一个空的数据,比如说空的productInfo的json串

因为我们有一个异步监听数据变更的机制在里面,也就是说,如果数据变更的话,某个数据本来是没有的,可能会导致缓存穿透,所以我们给了个空数据

但是现在这个数据有了,我们接收到这个变更的消息过后,就可以将数据再次从生产服务中查询出来

然后设置到各级缓存中去了

缓存失效问题

设置随机的缓存失效时间

Donate