分布式缓存
1.为什么使用缓存
使用缓存最大的一个原因是为了提高性能,如果不用缓存每次查询都要去数据库查询,性能差,使用缓存之后可以将查询的数据放入缓存,这样下次查询不需要访问数据库。
在因学项目中,课程、教室的查询都用了缓存,因为这些信息类似商品,发布之后改动不大,更多的是用户的查询。
2.redis的数据类型及使用场景
数据类型 | 形式 | 特点 |
---|---|---|
String | Key-value | 最简单的key-value,存放数据 |
hash | key-filed-value | 一般用于存放对象,可以直接操作对象的某个字段的值 |
Set | Key-value | 无序去重,value类似set,可以用在并集的情况,例如两个人的共同好友。 |
ZSet | Key-value | 去重但排序,在add的时候可以有一个分数,然后根据分数排序,用在排行榜这种情况。 |
List | key-value | 可以用来存放例如某个人的好友、某个课程的上课学生;此外还可以用来做消息队列,实现也很简单只要用rpush和rpop命令就可以实现(因为list的数据结构就是双向链表)。 |
redis的过期策略和淘汰机制
1. 过期策略
假如一批key设置了超时时间,当到了超时时间之后redis会有两种删除过期key的策略:定时删除和惰性删除。
定时删除是redis每个100ms就随机检查一部分key,如果已经超时了就删除。这种方式是随机检查部分key,不可能对所有的key都进行检查,这样消耗太多性能了;这样也带来了可能已经过期的key没有被检查到,没有删除。
惰性删除是在查询key的时候先检查key是否过期,如果过期了就删除。
2. 淘汰机制
如果redis中很多过期但未删除的key或者不常用的key,而占用了很大的内存,那么在内存不足的时候就需要淘汰机制来处理。淘汰机制有6种(主要是第一个):
- allkey-lru:内存不足时,在所有key中移除最近最少使用的。(最常用)
- volatile-lru:内存不足时,在设置了过期时间的key中移除最近最少使用的。
- allkey-random:内存不足时,在所有key中随机删除。
- volatile-random:内存不足时,在设置了过期时间的key中随机删除。
- noeviction:内存不足时,新写入数据时会报错。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
对于lru淘汰算法,可以通过LinkedHashMap实现,lru最关键的是用链表队列的形式去将新的访问的key放在队头,旧的在末尾,然后淘汰的时候淘汰末尾的数据。
redis的单线程模型
redis的线程模型是基于reactor模型的单线程实现,主要是4个部分:多个socket、一个IO复用部分、事件分发器和事件处理器。这种方式虽然是单线程处理,但是这种IO多路复用的方式可以处理较大的连接。
为什么redis的性能这么好呢?
- 纯内存操作。
- 非阻塞的IO多路复用技术。
- 单线程模型避免了线程上下文切换的开销。
redis的集群实现
1. 高性能
单机的redis最多支持几万的QPS,在一些高并发的场景下需要用redis集群架构来实现。redis的集群设计有两个核心,分区和主从。
主从:一个master和多个slaver,master负责读写,slaver只负责读。这种设计使得读写分离,能支持更大的并发,但是一个master的读写性能还是有限,并且master的内存容量也限制了redis集群的容量,为此需要分区的设计。
分区:redis虚拟设计了16384个槽,多个master会均分16348个槽,每个master只负责一部分槽。这种设计类似哈希表,对于key,经过hash计算后能得到0~16347中的某个值,也对应某个master,这样就实现了redis整体数据的分区。
结合主从和分区的设计,redis可以达到几十万的QPS,并且如果要提高读性能,可以扩展slaver;如果要扩展写性能,或提供更大的缓存空间,可以扩展master(redis支持动态扩容)。但master如果挂了,那么一部分数据可能就查不到了,为此需要下面的哨兵来实现高可用(redis集群设计默认有高可用-哨兵的实现)。
2. 高可用-哨兵
哨兵是redis的一个组件,主要有3个功能:
- 监控集群状态。
- 如果有节点挂了,通知开发人员。
- 如果master挂了,自动将其下的一个slaver用作master。
3. redis集群
- redis集群自动将数据进行分区。
- 按照用户配置文件实现主从。
- 默认实现选举方式的高可用,master和slaver之间通过ping-pong的方式维持连接,如果某个节点ping了之后没有回应,判断另个节点死掉了,如果半数以上都认为这个节点挂了那就是挂了,会让该节点的slaver充当master继续提供服务。
- 如果redis采用6379作为客户端访问端口,那么16379是集群的总线通讯端口,节点之间通过这个端口进行通信(gossip协议)。
- key寻址方式整个集群是去中心化的设计,每个master之间互相通信,外部客户端访问redis集群时将key计算CRC16值后取模,就可以得到hash slot,然后找到对应的master/slaver主从部分进行访问。
redis的持久化方案
-
RDB
redis每隔一段时间对数据进行持久化(可能在时间间隔内数据丢失)
-
AOF
redis会将所有的写命令追加在一个日志文件(redis恢复数据慢、降低了redis的写性能)
缓存穿透和缓存雪崩
1. 缓存穿透
恶意攻击访问一个redis中不存在的key,因为缓存中没有这个key就需要去数据库查询,但数据库中并没有这条记录,导致缓存中一直没有,这样大量的访问就穿透了redis给数据库带来了压力。
解决方法:1. 就算db中查询了没有也依然把这个key放入缓存。
2. 布隆滤波器。在查询之前选取一个大的bitmap中查询是否有这个key。
2. 缓存雪崩
redis大量的key同时失效,但此时又发生了极高的并发请求,那么大量的请求要去访问数据库,给数据库带来压力。
解决方法:1. 不同的key设置不同的过期时间。
2. 服务降级和熔断。
缓存和数据库双写一致性问题
这个问题一般是对于数据修改这种操作的场景,一般不会采用对缓存修改数据,而都采用删除key这种方式,这是因为修改缓存的方案主要有两点缺陷:1.对于写多读少的应用场景,需要频繁修改数据,频繁修改缓存,不如修改DB之后,当要查询的时候才将其放入缓存来的有效。2.存在线程安全的问题。
通常对于修改数据这种,会先删除缓存,再去db修改数据;或先去db修改数据,在删除缓存,但无论是哪种对于读写都是并发的,就有可能出现缓存和数据数据不一致的问题:
-
先删缓存再修改数据
- 线程A(修改)先删除缓存。
- 线程B(查询)去查询数据,发现缓存中没有key,去DB中将原来的值取出并放入了缓存中。
- 线程A去DB修改数据,数据为新值。
可以看到最后的结果,缓存和DB中的值不一致。
-
先修改数据再删除缓存(一般采用这个)
- 线程A(修改)先去DB中修改数据。但删除缓存的时候出问题了,没有删除缓存。
- 线程B(查询)去缓存中查询,那么查询的一直是旧值。
因为修改的线程挂了,导致最终缓存和DB中的值不一致。
解决方案:
- 对key设置超时时间,但这样会在key的存活时间内有数据不一致的问题。
- 设计一个重试的队列。这样保证删除缓存失败的时候,队列能提供再次删除。