1 为什么使用缓存?
高性能、高并发。
缓存主要是用来提高获取数据的速度,通过将一些热点数据存储在缓存中,可以大大提高业务处理的速度,因此可以提高系统的性能和并发能力。
在实际业务场景中,也可以用来缓存一些特殊数据,例如登录用户的token
、分布式锁等。
2 Redis有哪些数据类型?
string,list,hash,set,zset,位图,HyperLogLog,布隆过滤器。
string可以存储字符串:
set name Xianhuii
list可以存储有序列表:
rpush books Java JavaScript Spring
hash可以存储多个键值对:
hset person name Xianhuii
hset person age 18
set可以存储去重的无序列表:
sadd books python
sadd books python
zset可以存储去重的排序列表:
zadd books 10 Java
zadd books 8 Spring
zadd books 9 Java
位图可以按位存储信息:
setbit bitMap 1 1
setbit bitMap 2 0
HyperLogLog可以用于统计大量数据:
pfadd users Xianhuii
pfadd users CHUAN
……
pfcount users
布隆过滤器可以用于大量数据的过滤:
bf.add users Xianhuii
……
bf.exists user CHUAN
3 Redis的过期策略?
定期删除+惰性删除,内存淘汰策略。
Redis中的key都可以设置过期时间,对于这类数据,会采用定期删除+惰性删除
策略:
- 定期删除:Redis将设置过期时间的key存储在一个独立字典中,会定时扫描这个字典,根据过期时间来判断是否删除该key。
- 惰性删除:如果某个key过期了,但是还没进行定期删除扫描,此时客户端获取这个过期key时,Redis会先判断过期时间,如果过期了就立即删除。
定期删除+惰性删除
策略在日常工作中也有很多应用,它的原理就是使用定时任务进行轮询,保证整体业务的稳定性;对于轮询间隔的少量数据,使用即使处理的兜底策略。
Redis中的key也可以不设置过期时间,这部分数据不能用定期删除+惰性删除
策略进行处理。如果内存中存在大量未设置过期时间的key,以及大量设置过期时间但未过期的key,造成内存达到了上线,此时定期删除+惰性删除
策略就派不上用场了。
此时,Redis会采取内存淘汰策略:
noeviction
(默认):不再处理写请求,可以继续执行读请求和删除请求。volatile-lru
:优先淘汰最少使用的、设置过期时间的key。volatile-ttl
:优先淘汰ttl
小的、设置过期时间的key。volatile-random
:随机淘汰设置过期时间的key。allkeys-lru
(建议使用):优先淘汰最少使用的全体key。allkeys-random
:随机淘汰全体key。
简单来说,淘汰策略:
- 不再写入,需要手动删除。
- 从
设置过期时间的key集合/全体key集合
中进行最少使用淘汰/最小ttl淘汰/随机淘汰
。
4 Redis如何持久化?
RDB,AOF。
Redis的数据全部存储在内存中,如果宕机会造成数据丢失。为此,Redis提供了持久化功能:
- RDB:定时(周期较长)将内存中的全部数据保存到磁盘文件中,每次都会新生成该时刻的数据文件。
- AOF:记录对内存进行修改的指令记录,由于可能会有覆盖操作,需要定时对该日志进行瘦身重写,即定时将内存数据转换成操作指令。
RDB是对内存数据的整体备份,生成的文件通常比较少,恢复速度快。但是由于备份周期长,可能会丢失一部分未及时备份的数据。
AOF是对操作指令的记录,随着运行时间增长会变得越来越大,恢复速度慢。
实际项目中通常使用RDB+AOF混合持久化
策略,使用RDB定期备份内存中的数据,使用AOF记录上个RDB备份周期至今的操作指令。由此既保证了数据的完整性,又大大减小了AOF指令文件,加快了恢复速度。
5 如何保证Redis的高可用和高并发?
主从复制,哨兵,集群。
Redis高可用,简单来说就是要保证某个服务器宕机后不能影响客户端的业务,Redis提供了主从复制+哨兵
功能。
Redis高并发,简单来说就是要能够快速处理客户端的请求,所以通常需要增加服务器实例的数量,Redis提供了集群
功能。
实际上主从复制
和哨兵集群
是不可分的,它们往往会结合到一起工作。
5.1 主从复制
快照同步,增量同步。
可以为Redis服务器设置一个master
节点和多个slaver
节点,master负责与客户端进行交互,slaver则会使用快照同步+增量同步
的方式,定时从master(主从同步)或slaver(从从同步)同步数据。
快照同步类似于RDB持久化方式,它会将内存中的数据持久化到磁盘文件,然后将该磁盘文件数据同步给slaver。
增量同步类似于AOF持久化方式,它会将修改操作指令记录到缓存中,然后将该缓存数据同步给slaver。
5.2 哨兵Sentinel
哨兵是Redis主从节点的管理器,它会持续监控主从节点,当master宕机时,会选择最优的slaver作为新的master。
使用哨兵的流程:
- 客户端连接哨兵,请求master地址。
- 客户端连接master进行操作。
如果master宕机重新选取后,客户端需要重新上述步骤,获取最新的master地址。
5.3 Redis Cluster
Redis Cluster是官方的集群化方案,它本身提供类似主从复制和哨兵的功能。
Redis Cluster是去中心化的,它将所有数据划分为16384个槽位,每个节点负责一部分槽位,保存一部分数据。当客户端连接集群时,会返回一份集群的槽位配置信息。客户端需要查找某个key时,可以直接定位到目标节点。
Redis Cluster可以为每个主节点设置若干从节点,当从节点发生故障时,集群会自动将其中某个从节点提升为主节点。如果某个主节点没有从节点,那么宕机时就会造成整个集群不可用。
Redis Cluster通过Gossip协议广播
来保证集群中实例间的信息同步。如果某个节点发现节点A失联了(PFail,Possible Fail),会向整个集群广播。各个节点会判断节点A的连接情况,并广播判断结果。如果集群中大部分节点都判断该节点失联,就会将节点A下线(Fail),并且进行主从切换。
6 如何保证缓存和数据库的数据一致性?
简单双删策略,延迟双删策略,串行化策略。
在使用Reids缓存数据库数据时,就会出现缓存数据和数据库数据不一致问题。
根据对数据一致性的不同程度要求,我们可以采取递进的措施。
6.1 简单双删策略
如果对数据一致性要求不严格,可以使用以下简单的双删策略:
读取数据:
public Result read() {
// 读取缓存
// 如果缓存没有,读取数据库
// 更新缓存
return res;
}
更新数据:
public void update() {
// 删除缓存
// 更新数据库
// 删除缓存
}
正常情况下,update
和read
操作会按顺序执行。每次update操作都会更新数据库数据,删除缓存。每次read操作都会获取数据库中的最新数据作为缓存。
但是在实际线上环境中,read
和update
可能会并发执行,在小概率情况下可能会按以下顺序执行,造成数据不一致:
- read读取缓存
- 缓存没有,read读取数据库
- update删除缓存
- update更新数据库
- update删除缓存
- read更新缓存(旧值)
6.2 延迟双删策略
为了避免这种并发执行情况,保证数据最终一致性,可以在update操作中增加一个延迟删除操作。通过延迟删除,可以使删除缓存操作位于并发执行的最后,保证缓存在数据库更新后被删除,下次读取时就可以获取最新数据:
public void update() {
// 删除缓存
// 更新数据库
// 删除缓存
// 延迟删除:如DelayQueue或MQ
}
6.3 串行化策略
上述延迟双删策略保证了数据的最终一致性,但是并发read到延迟删除这一时间段的数据仍可能是不一致的。
如果需要保证数据的强一致性,就需要将update和read操作串行化了。
例如可以使用BlockingQueue
,在update和read时将实际逻辑添加到队列中,然后按顺序进行执行。
7 如何解决缓存雪崩、缓存穿透和缓存击穿?
缓存雪崩、缓存穿透和缓存击穿都是指,在高峰期系统处理大量请求时,由于缓存中获取不到数据,从而直接请求数据库,造成数据库崩溃的现象。
7.1 缓存雪崩
缓存雪崩是指在高峰期,由于缓存服务器宕机或者大量key同时过期,大量业务请求数据库,造成数据库崩溃。
为了避免缓存服务器宕机造成的缓存雪崩,需要开启Redis的主从复制、哨兵模式和集群功能,保证系统的高可用。为了宕机后的数据恢复,还需要开启持久化功能。
在业务中,应该设置不同的过期时间,避免大量key过期后被同时删除。
如果缓存服务器已经宕机,则需要开启本地缓存,通过限流或服务降级,避免数据库崩溃。后续通过Redis重启从磁盘恢复数据,在恢复原先的架构。
7.2 缓存穿透
缓存穿透是指大量请求的数据在缓存和数据库中都不存在,每次请求都会越过缓存请求数据库,但是数据库中也不存在数据,因此不会更新缓存。
这种情况往往是恶意的,我们可以通过缓存一个空值状态
到数据库,下次请求时,直接响应该空值:
public Result find() {
// 查询缓存
// 缓存如果存在,直接返回
// 缓存不存在,查询数据库
// 数据库如果存在,直接返回
// 数据库不存在,更新对应空值状态缓存并返回
}
由于不存在的数据是无穷多个的,我们没办法缓存每个空值状态
。可以采取一种反向思维,使用布隆过滤器缓存已存在的数据,请求查询时,先判断布隆过滤器中是否存在:
- 存在:从对应缓存或数据库中获取数据。
- 不存在:直接返回
空值状态
。
新增数据时:
public void add(Data data) {
// 更新数据库
// 添加到布隆过滤器
}
查询数据时:
public Result find() {
// 查询布隆过滤器
// 布隆过滤器不存在,直接返回空值状态
// 布隆过滤器存在,查询缓存
// 缓存如果存在,直接返回
// 缓存不存在,查询数据库,更新缓存并返回
}
7.3 缓存击穿
缓存击穿是指某个热点key过期后,大量请求集中式并发访问,此时这些请求都会直接请求数据库,容器造成数据库崩溃。
对于这种热点数据,可以将其设置成永不过期。或者使用定时任务,在过期前适当延长过期时间或更新缓存。
8 Redis为什么那么快?
Redis是个单线程程序,它的所有数据都存储在内存中,使用NIO多路复用与客户端进行交互,保证客户端的高并发请求;使用单线程处理指令,避免了线程的上下文切换。
当客户端发送请求时,请求指令会被缓存到指令队列中,Redis工作线程会轮询指令队列进行执行,将指令结果缓存到响应队列,Redis工作线程轮询会响应队列通过对应套接字响应给指定客户端。
这里的指令队列和响应队列,都是NIO的操作系统内核缓存,依赖底层操作系统。
9 Redis分布式锁
Redis分布式锁是日常项目中常用的工具。
所谓锁,就是一种全局唯一的状态标识
,并且对其操作必须是原子性的。
在单机场景下,我们可以使用同一个Java对象保证它是全局唯一的,由于Java的synchronized
等操作保证了加锁/解锁操作是原子性的,所以它可以作为锁。
在分布式场景下,由于Redis是单线程操作的,所有指令都会按顺序执行,天然保证了加锁/解锁的原子性。同时Redis的key是唯一的(就算是集群中也是唯一的),这就保证了使用Redis作为分布式锁的可能性。
我们可以通过set
指令(原子操作)设置分布式锁:
# 1、获取分布式锁
set disKey disValue ex 5 nx
# 2、如果响应成功,说明加锁成功,进行业务操作
# 3、业务操作完成,解锁
del disKey
需要注意分布式锁的过期时间,如果分布式锁过期了,但是业务还没有完成,那么该分布式锁就失效了。
此时可以考虑使用Redisson
,它会使用看门狗机制,定时检查客户端是否仍持有锁,并且适当延长过期时间。