redis缓存设计

我们在考评系统,使用了redis, 学生来考试之前,会进行预加载,把考生的卷子存到redis中,每场考试最多两个小时,所以redis缓存过期时间设的四小时.学生到了之后,直接在redis中拿卷子,那么流程就是这样子的

redis缓存设计_第1张图片

学生登录之后,先去redis中拿数据,如果redis中没有这个数据,那么就会去数据库中,然后返回到学生界面中.

这么一看,貌似没啥问题,我们再来想: 如果我们忘记了进行预加载或者预加载了部分

那么就是这么三步了:1.缓存层不命中; 2.存储层命中,返回值; 3.存储层不命中,不将结果写回缓存,返回值;

考生们到点统一开始考试,那么要的数据在缓存中是一直不存在的,就会造成每一次请求都会查询mysql, 缓存还有意义吗, 流量大的时候, 可能mysql就挂掉了.另外如果恶意攻击或者爬虫,大量空命中,这就是一个漏洞了.

也就是"缓存穿透"

缓存穿透

所以结合上面来看,缓存穿透有两个原因,一个是本身业务代码出现了问题,一种就是恶意攻击或者爬虫了.

怎么解决: 

方案1: 

redis缓存设计_第2张图片

如果在mysql还是没有命中,那么就把这个空值放到缓存层,下次再来访问的时候,就直接从缓存来取就好了.这样就最起码保护了后端的存储层.

但是这样就也就新产生了问题: 1.空值也放到缓存中了,缓存中就存了很多键,占地方.如果这时是攻击,不断用新的key来查,缓存也会一直在存,也会受不了的; 2.比方说,我们设的过期时间是2h,在这两个小时之内,存储层,现在添加了这个数据了, 这就意味着,现在用户进行的查询结果是不对的. 缓存和存储不一致了.

这两个问题也是有一定的方式来避免的. 对于问题1: 空值key,设一个较短的过期时间,让其自动剔除;  问题2: 加一个消息系统,或者其他方式,赋值,清除掉空对象

方案2: 布隆过滤器拦截

布隆过滤器,当时在数学之美上看到过,一种数据结构具体忘记了.在访问缓存层和存储层之前,将key用布隆过滤器先保存起来.来了数据,如果布隆过滤器认为不存在,那就是不存在了,不再往下走mysql了,这种比较适合数据相对固定的,实时性低的.

来个简单对比吧: 

解决缓存穿透

使用场景

维护成本

缓存空对象

数据命中不高

数据频繁变化实时性高

代码维护简单

需要过多缓存空间

数据不一致

布隆过滤器

数据命中不高

数据相对固定,实时性低

代码维护复杂

缓存空间占用少


 上面我们就简单从命中key值来讲缓存穿透,利用上面两种方法,可以一定程度缓解这个穿透,但是缓存内部,也会有失效时间时间等这种问题,来导致命不中key值,变成直连数据库.所以下面我们来从缓存并发和缓存失效讲.

可以这么来理解,学生现在都在登录,准备抽取试卷了,如果试卷的这个缓存失效了,那么就会同时又多个进程同时查询mysql,然后设置缓存,如果并发很大,一是会造成mysql 压力过大,二是缓存频繁更新.

redis缓存设计_第3张图片


而且有时候重建缓存可能不在短时间之内完成,所以应用就有可能会崩溃.

解决方案:

1.减少重建缓存的次数

  互斥锁,只允许一个进程创建缓存,如果有key不存在,就加上锁,进mysql查询入缓存,然后再解锁,其他的就直接从缓存来拿好了.但这样就有个部分请求等待的问题了,还有就是存在死锁的风险;还有线程池堵塞的风险;

2.缓存失效--永远不过期

上面我们提到了高并发的时候,其实高并发的,可能就会有某个时间同时生成很多缓存,然后过期时间还都一样,当一过期时间到时,就通通失效了.然后所有请求依旧会走到数据库,db压力也会过大的.

所以一个超级简单的方法,如果一定要换成失效,那么就把失效时间分散开,可以在原有的失效时间上加一个随机值,这样每个缓存过期时间重复率就降低了.整体失效情况会减少一些.

其实当并发较高的时候,缓存不失效也是ok的,那么你可能就问了,数据库更新了,缓存没更新,就不一致了.那么是不是可以这么来操作: 你给每个value设一个逻辑过期时间(或者数据库把时间戳也插入到redis中,redis读取数据,先判断有没有过期),然后如果发现value中的时间过期,就单独线程去构建缓存并返回给客户端数据.缺点就是重构缓存时,还是会出现数据不一致了,那就只能看能不能容忍这种不一致了.代码复杂程度也有所增加.

上面是直接讲的确保系统不会崩掉,同时还能拿到数据.下面就来讲讲,怎么来批量拿到这个数据并且是高效率的.

这个并不是我们想的: 只要节点多了, 我的取效率就提高了.这个得这么想: redis节点是多了,但是你的key是映射在各个节点的.批量操作时,是从不同节点上来获取的.这样就设计到了网络时间了.如果你说,那我就一个节点好了,那如果数据量特别大的时候,一个节点也顶不住啊.所以在用分布式的时候,同时还来优化它,就能找到一个最优解了.下面来谈优化方面,基本就两个方面:

1.不用考虑的是,sql语句应该是优化过的;客户端连接也是采用的nio

2.减少网络通信次数

重点来讲第2条

如果,现在只有一个节点,那么有几种来取的方式:

1>n次get: n次get,n次网络,简单理解就是:逐次执行n个get命令;

2>1次pipeline get:1次网络+n次get

3>1次mget: 1次网络+1次mget

如果,现在是redis-cluster,那么怎么来获取

1>跟上面第一条一样,还是n次get+n次网络时间,实现很简单啊,来个循环就好了

2>上一篇文章, 提到了客户端怎么来获取值的,还记得吗?先把槽和节点的映射都拿到手,然后这里就可以将属于同一个节点上的key进行归档, 01 key,你是 1号 节点的, 02key ,你也是1号节点的,一会你两一块去1号节点槽中取数据. 这样就会变成了分组,有几个节点,就分成了几个组,然后再去每个节点上执行mget或者pipeline. 所以操作时间就变成了"节点数量次数的网络时间和n次命令时间" ,比起第一条来,相对优化,那如果现在节点数量特别大.也不是很好啦

3>在第二步上面优化,我把节点取值的时候,变成多线程,网络次数其实还是节点个数,但是现在是多线程了.所以多线程网络时间就变成O(1)了.

4>上一篇还提到过hash_tag, 它是将多个key强制分配到一个节点上.这样取的话,是回见就变成了1次网络时间+n次命令了.但是这样就会非常容易产生数据倾斜.数据倾斜了,负载不均,就影响了集群均衡还有运维成本了.

所以啊,在实际过程中,这四种也得结合业务场景来分析了.我觉得对于考试来说,数据量不大,哪一种都是可以接受的.



你可能感兴趣的:(数据库+缓存+消息队列)