面试-redis
详情直达:视频
1. 缓存
1.1 缓存穿透
查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库

- 解决方案:
- 缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存
- 优点:简单
- 缺点:消耗内存,可能会发生不一致的问题
- 布隆过滤器
- 缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存
1.1.1 布隆过滤器
1.1.1.1 介绍
bitmap:相当于是一个以(bit)位为单位的数组,数组中每个单元只能存储二进制数0或1
布隆过滤器可以用于检索一个元素是否在一个集合中。

使用的是redisson实现的布隆过滤器。
1.1.1.2 底层原理
- 初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。
- 误判率:数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。通常误判率在5%。
1.1.1.2 优点
- 内存占用少
- 没有多余Key
1.1.1.3 缺点
- 实现复杂
- 存在误判
1.2 缓存击穿
给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮

1.2.1 互斥锁

- 当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法
- 优点:数据一致性
- 缺点:性能差
1.2.2 逻辑过期

- 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
- 当查询的时候,从redis取出数据后判断时间是否过期
- 如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新
- 优点:高可用,性能较好
- 缺点:数据不能达到强一致。
1.3 缓存雪崩
指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库。
解决:
- 给不同的Key的TTL添加随机值
- 给缓存业务添加降级限流策略
- 搭建redis集群。
1.4 缓存预热
指系统上线后,提前将相关的缓存数据加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据。
解决:
- 数据量不大的时候,工程启动的时候进行加载缓存动作;
- 数据量大的时候,设置一个定时任务脚本,进行缓存的刷新;
- 数据量太大的时候,优先保证热点数据进行提前加载到缓存。
1.5 双写一致
即:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致
1.5.1 数据强一致
- 对于这类问题,因为要求时效性比较高,我们当时采用的读写锁保证的强一致性。
- 可以采用redisson实现的读写锁,在读的时候添加共享锁(readLock),可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁(writeLock),它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
- 排他锁底层使用setnx,这样就保证了当前只有一个线程操作锁住的方法。
- 延迟双删:如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性。
1.5.2 时效性高
- 对于这类问题,因为需要满足高并发,因此允许有一定的延时。
- 可以采用MQ的方式,当有修改数据操作的时候,同时发布消息,在缓存监听服务中监听消息,一旦收到消息就立刻更新缓存。
- 还可以采用阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。
1.6 数据持久化
1.6.1 RDB
RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
在redis客户端使用bgsave就可以开启备份
可以在redis的配置文件redis.conf文件中找到

执行原理:
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
在linux中所有进程不能直接操作物理内存的数据,os会给每个进程一个虚拟内存,os会维护一个虚拟内存与物理内存的一个关系表(页表)。
主进程操作虚拟内存,通过页表,关联到真正的物理内存,就完成了对数据的读写操作。
开启bgsave的时候fork出一个子进程,同时也复制了页表,那么在子进程与主进程的内存空间共享。

子进程在写RDB文件的时候,主进程接收用户请求实现修改内存数据。此时就形成主进程修改数据,但是子进程在备份数据,有脏数据风险。
避免脏数据发生,使用copy-on-write的技术:
当主进程执行读操作的时候,访问共享内存
当主进程执行写操作的时候,会拷贝一份数据,专门用来进行写操作。

1.6.2 AOF
AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据。
默认是关闭的,需要修改redis.conf文件下的appenonly配置由no改为yes。
AOF的命令记录的频率也可以通过redis.conf文件来配:


1.6.3 区别
通常对数据安全性较高的都会使用RDB+AOF

1.7 数据过期策略
- Redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)。
- 通常使用惰性+定期删除
1.7.1 惰性删除
- 惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key
- 优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查
- 缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放
1.7.2 定期删除
- 每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。
- slow模式:定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的hz 选项来调整这个次数
- fast模式:执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms
- 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。
- 缺点:难以确定删除操作执行的时长和频率。
1.8 数据淘汰策略
当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。
有8种模式:
- noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
- volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰。
- allkeys-random:对全体key ,随机进行淘汰。(数据访问频率不大使用)
- volatile-random:对设置了TTL的key ,随机进行淘汰。
- allkeys-lru: 对全体key,基于LRU算法进行淘汰。(优先使用)
- volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰。(业务指定需求使用)
- allkeys-lfu: 对全体key,基于LFU算法进行淘汰。(短时高频访问)
- volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰。(短时高频访问)
LRU(Least Recently Used)最近最少使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFU(Least Frequently Used)最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。
2. 分布式锁
使用场景:集群情况下的定时任务、抢单、幂等性场景
2.1 超卖问题

在这种情况下,一定会出现超卖的。在单系统下,使用synchronized或者reentrantlock加锁可以放置超卖,但是在集群模式下,每个JVM的锁是不一样的,因此还会出现超卖问题。
如果使用redis作为分布式锁,使用setnx就可以完成,当然最后一定要手动的删除锁。

2.2 锁时长
2.2.1 预估
根据业务的平时耗时来给出一个预估加锁时长,但是如果出现网络抖动或者IO异常时,此方法不可靠。
2.2.2 看门狗
只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程一还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了锁过期释放,业务没执行完问题。
1、如果我们指定了锁的超时时间,就发送给Redis执行脚本,进行占锁,默认超时就是我们制定的时间,不会自动续期;
2、如果我们未指定锁的超时时间,就使用lockWatchdogTimeout = 30 * 1000【看门狗默认时间】
2.3 可重入
redission实现的分布式锁是可重入的,使用Hash结构记录线程Id和重入次数

2.4 主从一致
即:当线程1获得锁操作master节点后,master发生了宕机,此时在从节点中选举出新的master节点,线程2获取锁在新的master上,这样就造成两个线程同时获得锁
2.4.1 红锁
RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n / 2 + 1),避免在一个redis实例上加锁。
但是:红锁实现复杂,性能差,运维繁琐。
3. 集群
3.1 主从复制
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离。

3.1.1 主从数据同步
全量同步(首次):
- 从节点与主节点建立连接。
- 发起请求数据同步,会携带自己的repId和offest。
- 主节点接收到之后,根据repid判断是否是首次,repid不一致即是首次同步。
- 首次同步是将自己的repid和offset返回给从节点。
- 从节点保存主节点数据信息。
- 主节点执行bgsave。
- 将生成的rdb文件发送给从节点。
- 从节点接收后清空自己本地的rdb文件,加载主节点发送的rdb文件。
- 如果主节点在执行bgsave的时候,接收到用户请求,将会记录下这段备份时间所有的指令至repl_baklog中。
- 最后将repl_baklog发送给从节点,从节点执行该文件中所有的命令。
- 完成数据同步。

增量同步(非首次):发生在salve重启或者数据变化时
- 从节点发生宕机重启。
- 从节点发起请求,携带repid和offset。
- 主节点接收请求,根据repid判断是自己的,进行增量同步。
- 去repl_baklog中寻找offset后的执行命令。
- 主节点将这些命令发送给从节点。
- 从节点执行命令。
- 完成同步

3.2 哨兵模式
Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复。

- 监控:Sentinel 会不断检查您的master和slave是否按预期工作。
- 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。
- 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端
3.2.1 检测
Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:
主观下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主观下线。
客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。
3.2.2 选举规则
- 首先判断主与从节点断开时间长短,如超过指定值就排该从节点。
- 然后判断从节点的slave-priority值,越小优先级越高。
- 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高。
3.2.3 脑裂
正常的哨兵模式:

如果此时出现某一个主节点与哨兵由于网络问题出现不通信,被哨兵检测为下线,那么哨兵会在接下的从节点中重新选举出一个新的master节点。由于服务状态变更通知不能立刻通知到client,用户还在向原master写操作。

最终原master会被降为从节点,就进行了重新的主从数据同步操作,因此用户的写操作执行命令就造成丢失。
因此可以通过修改redis的两个配置参数防止出现该问题
- min-replicas-to-write 1 表示最少的salve节点为1个。即:在执行写操作的时候,必须要有一个从节点。
- min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒。即:如果主从数据同步操作5秒,达不到要求就拒绝请求,这样就可以防止丢失数据。
3.3 分片集群
分片集群用来解决:
- 海量数据存储问题
- 高并发写的问题

3.3.1 特征
- 集群中有多个master,每个master保存不同数据。
- 每个master都可以有多个slave节点。
- master之间通过ping监测彼此健康状态。
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点。
3.3.2 数据读写
Redis 分片集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。

4. Redis事务
4.1 事务基本概念
- Redis事务中如果有某一条命令执行失败,之前的命令不会回滚,其后的命令仍然会被继续执行。鉴于这个原因,所以说Redis的事务严格意义上来说是不具备原子性的。
- Redis事务中所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
- 在事务开启之前,如果客户端与服务器之间出现通讯故障并导致网络断开,其后所有待执行的语句都将不会被服务器执行。然而如果网络中断事件是发生在客户端执行EXEC命令之后,那么该事务中的所有命令都会被服务器执行。
4.2 Redis事务三个阶段
- multi 开启事务
- 大量指令入队
- exec执行事务块内命令,截止此处一个事务已经结束。
- discard 取消事务
- watch 监视一个或多个key,如果事务执行前key被改动,事务将打断。unwatch 取消监视。
事务执行过程中,如果服务端收到有EXEC、DISCARD、WATCH、MULTI之外的请求,将会把请求放入队列中排队.
4.3 Redis事务相关命令
Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的
4.3.1 WATCH
- WATCH 命令是一个乐观锁,可以为 Redis 事务提供 check-and-set (CAS)行为。
- 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。
4.3.2 MULTI
- MULTI命令用于开启一个事务,它总是返回OK。
- MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
4.3.3 EXEC
- 执行所有事务块内的命令。
- 返回事务块内所有命令的返回值,按命令执行的先后顺序排列。
- 当操作被打断时,返回空值 nil 。
- 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
4.3.4 UNWATCH
- UNWATCH命令可以取消watch对所有key的监控。
4.4 Redis事务支持隔离性吗?
- Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的。
4.5 Redis为什么不支持事务回滚?
- Redis 命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面,这些问题不能在入队时发现,这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。
- 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。
4.6 Redis事务其他实现
- 基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,
其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完。
5. 其他
5.1 redis是单线程会什么这么快?
- Redis是纯内存操作,执行速度非常快
- 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
- 使用I/O多路复用模型,非阻塞IO
- Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度
5.2 解释一下IO多路复用
IO多路复用就是实现了高效的网络请求。
5.2.1 IO模型
Linux系统中一个进程使用的内存情况划分两部分:内核空间、用户空间。
用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源必须通过内核提供的接口来访问。
内核空间可以执行特权命令(Ring0),调用一切系统资源。
Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:
- 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
- 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区

4.2.1.1 阻塞IO
两个阶段都必须阻塞等待。
- 阶段一:
- 用户进程尝试读取数据(比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 此时用户进程也处于阻塞状态
- 阶段二:
- 数据到达并拷贝到内核缓冲区,代表已就绪
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据

5.2.1.2 非阻塞IO
- 阶段一:
- 用户进程尝试读取数据(比如网卡数据)
- 此时数据尚未到达,内核需要等待数据
- 返回异常给用户进程
- 用户进程拿到error后,再次尝试读取
- 循环往复,直到数据就绪
- 阶段二:
- 将内核数据拷贝到用户缓冲区
- 拷贝过程中,用户进程依然阻塞等待
- 拷贝完成,用户进程解除阻塞,处理数据

- 非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。
- 虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,CPU使用率暴增。
5.2.1.3 IO多路复用
利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。
阶段一:
- 用户进程调用select,指定要监听的Socket集合
- 内核监听对应的多个socket
- 任意一个或多个socket数据就绪则返回readable
- 此过程中用户进程阻塞
阶段二:
- 用户进程找到就绪的socket
- 依次调用recvfrom读取数据
- 内核将数据拷贝到用户空间
- 用户进程处理数据

由于在监听某个socket数据就绪的时候,会去寻找对应的socket,因此衍生出poll和epoll
select+poll
select和poll只会通知用户进程有Socket就绪,但不确定具体是哪个Socket ,需要用户进程逐个遍历Socket来确认
select+epoll
epoll则会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间
5.3 redis网络模型
Redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装, 提供了统一的高性能事件库

5.4 redis消息队列
在消息队列中包含3个角色:
- 消息队列:存储和管理消息,也被称为消息代理。
- 生产者:发送消息到消息队列。
- 消费者:从消息队列获取消息并处理消息。
5.4.1 List
- redis的list结构是一个双向链表,很容易模拟出队列效果。
- 队列是入口和出口不在一边,因此可以用LPUSH结合RPOP、或者RPUSH结合LPOP实现
- 但是,当队列没有消息时pop就会返回null,并不会jvm堵塞队列那样堵塞并等待消息,因此这里应该使用BRPOP或者BLPOP来实现堵塞队列。

- 优点:
- 利用Redis存储,不受限于JVM内存上限
- 基于Redis的持久化机制,数据安全性有保证
- 可以满足消息有序性
- 缺点:
- 无法避免消息丢失。从消息队列取到消息,还没来得及处理就挂掉了,这个消息就消失了。
- 只支持单消费者。
5.4.2 PubSub
- PubSub(发布订阅)是redis2.0版本引入的消息传递模型,消费者可以订阅一个或多个channel(频道),生产者向对应channel发送消息后,所有订阅者都能收到相关消息。

- 相关命令:
- SUBSCRIBE channel [channel] :订阅一个或多个频道
- PUBLISH channel msg :向一个频道发送消息
- PSUBSCRIBE pattern[pattern] :订阅与pattern格式匹配的所有频道
- 优点:
- 采用发布订阅模型,支持多生产、多消费
- 缺点:
- 不支持数据持久化。
- 无法避免消息丢失。
- 消息堆积有上限,超出时数据丢失。(缓存空间是有上限的)
5.4.3 Stream
- Stream是redis5.0引入的一种新数据类型,可以实现一个功能非常完善的消息队列。
5.4.3.1 单消费模式
发送命令:

使用XREAD读取:

读取最新消息:

注意:当我们指定起始ID为$时,代表读取最新的消息,如果我们处理一条消息的过程中,又有超过1条以上的消息到达队列,则下次获取时也只能获取到最新的一条,会出现漏读消息的问题。
特点:
- 消息可回溯。不消失永久保存在队列里。
- 一个消息可以被多个消费者读取。读完不消失的,可以多个读
- 可以堵塞读取
- 有消息漏读的风险
5.4.3.2 消费者组
消费者组(Consumer Group):将多个消费者划分到一个组,监听同一个队列。



消费者监听的思路:

特点:
- 消息可回溯
- 可以多消费者争抢消息,加快消费速度
- 可以阻塞读取
- 没有消息漏读的风险
- 有消息确认机制,保证消息至少被消费一次
5.4.4 比较
