Redis面试题

196 阅读41分钟

使用 Redis 有哪些好处?

Redis(Remote Dictionary Server)是一种开源的内存数据库,被广泛用于构建高性能、高可扩展性的应用程序。使用Redis带来了许多好处,包括:

  1. 快速的读写性能: Redis将数据存储在内存中,因此可以提供非常快速的读写访问速度。这使得Redis成为高吞吐量应用程序的理想选择,如缓存层、会话存储和计数器等。
  2. 支持丰富的数据结构: Redis支持多种数据结构,包括字符串、列表、集合、散列、有序集合等。这使得Redis不仅可以用作缓存,还可以用于各种其他用途,如队列、发布/订阅系统等。
  3. 持久化选项: Redis提供了不同的持久化选项,可以将数据持久化到磁盘上,以便在重启后恢复数据。这使得Redis可以用于更持久的数据存储需求。
  4. 分布式支持: Redis支持分布式部署,可以将数据分片到多个节点上,以实现更高的容量和可扩展性。它还提供了内置的复制和故障转移机制,以增加可用性。
  5. 事务支持: Redis支持事务,允许多个命令在一个事务中执行,保证了原子性。这对于需要执行多个操作并保持数据一致性的应用程序非常有用。
  6. 发布/订阅模式: Redis支持发布/订阅模式,可以用于构建实时通知系统、消息队列和事件驱动的应用程序。

什么是缓存穿透 ? 怎么解决 ?

缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。

解决方案一:
缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存{key:1,value:null}
优点:简单
缺点:消耗内存,可能会发生不一致的问题

解决方案二:
布隆过滤器
优点:内存占用较少,没有多余key
缺点:实现复杂,存在误判

1694073555697.png

解决方案的话,我们通常都会用布隆过滤器来解决它

你能介绍一下布隆过滤器吗?

嗯,是这样~

布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是redisson实现的布隆过滤器。

它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。

当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划分了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。

布隆过滤器误判是怎么回事?

hash 算法也叫做 摘要算法,作用是对任意一组数据输入,得到一个固定长度的输出摘要。

image.png

误判的原因,主要是Hash算法的问题。布隆过滤器是由于一个二进制和一个 Hash 算法组成的,Hash 算法存在着一定的碰撞几率。Hash 碰撞的含义是不同的输入值经过 hash 得到相同的 hash 结果。

hash 算法的输入值是无限的,输出值空间是固定的,比如 16位 hash 值的之空间是 65535 这样碰撞几率就是 1/65535 ,即输入值的个数超过 65535 就一定会发生碰撞。

但是,如果使用更长的 hash 值会带来更高的存储成本和计算成本,32 位的 hash 算法,值空间长度是 2^32-1 大概 42亿,如果有 20亿用户数据,碰撞的概率高达 50%。hash 碰撞造成两个用户,A 和 B 会计算出相同的两个 hash 值,如果A 是注册用户,B不是注册用户, 但是 A 和 B 在的的数组中是相同的,然后产生误判。

布隆过滤器的误判有一个特点,只会出现 false positive 的情况,当布隆过滤器判断元素在集合中,这个元素可能不在集合中,但是布隆过滤器判断这个元素不在集合时,它一定不在集合中,这一点非常适合解决缓存穿透的问题。

布隆过滤器有一个可预测的误判率(FPP):

image.png

n 是已经添加元素的数量;
k 哈希的次数;
m 布隆过滤器的长度(如比特数组的大小);

布隆过滤器的缺点

  1. 在判断元素是否在集合中有一定的错误几率,会误判,把不是在集合中的元素判断为处在集合中。
  2. 不支持删除元素

什么是缓存击穿,怎么解决

1694073872870.png

缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。

解决方案有两种方式:

第一可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法直到缓存命中

第二种方案可以设置当前key逻辑过期,大概是思路如下:

①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间

②:当查询的时候,从redis取出数据后判断时间是否过期

③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新

当然两种方案各有利弊:

如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题

如果选择key的逻辑过期,则优先考虑高可用性,性能比较高,但是数据同步这块做不到强一致。

什么是缓存雪崩 ? 怎么解决 ?

缓存雪崩意思是设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。

与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。

解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

缓存雪崩的原因有可能是 redis 故障

image.png

《缓存三兄弟》
穿透无中生有key, 布隆过滤null隔离。
缓存击穿过期key, 锁与非期解难题。
雪崩大量过期key, 过期时间要随机。
面试必考三兄弟, 可用限流来保底。

总结

穿透: 缓存不存在,数据库不存在,高并发,少量key
击穿:缓存不存在,数据库存在,高并发,少量key
雪崩:缓存不存在,数据库存在,高并发,大量key
语义有些差异,但是,都可以使用限流互斥锁,来保证数据库的稳定。
关键:把每个key的请求量变为1个,O(1)的,所有的请求去抢锁,抢到锁的请求才可以操作数据库 O(1)的,对缓存更新也是O(1)的,其他的请求因为抢不到锁,而在本地自旋,过一会儿再从缓存取,这样的架构是比较健壮和稳定的

redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

1694077238043.png 一定、一定、一定要设置前提,先介绍自己的业务背景

允许延迟一致

嗯!就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,数据同步可以有一定的延时(符合大部分业务)

我们当时采用的阿里的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。

异步通知 1694077180758.png

1694076902746.png

一致性要求高

redis做为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

候选人:嗯!就说我最近做的这个项目,里面有xxxx(根据自己的简历上写)的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高,我们当时采用的读写锁保证的强一致性。

我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。

1698210914653.png

面试官:那这个排他锁是如何保证读写、读读互斥的呢?

候选人:其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法

面试官:你听说过延时双删吗?为什么不用它呢?

候选人:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时(数据库主从同步需要时间)删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。

redis做为缓存,数据的持久化是怎么做的?

候选人:在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF

面试官:这两种持久化方式有什么区别呢?

候选人:RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。

AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中,当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复数据

面试官:这两种方式,哪种恢复的比较快呢?

候选人:RDB因为是二进制文件,在保存的时候体积也是比较小的,它恢复的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令

RDB

RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照。简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,恢复数据

主动备份 image.png

自动备份
Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下: image.png

RDB的执行原理?
bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。
fork采用的是copy-on-write技术:
当主进程执行读操作时,访问共享内存;
当主进程执行写操作时,则会拷贝一份数据,执行写操作。 1694155457838.png

AOF

AOF全称为Append Only File(追加文件)。Redis处理的每一个写命令都会记录在AOF文件,可以看做是命令日志文件

1694155557986.png

1694155647842.png

RDB与AOF对比

1694155803491.png

Redis的数据过期策略有哪些 ?

在redis中提供了两种数据过期删除策略

第一种是惰性删除,在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。

优点 :对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查  
缺点 :对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放

第二种是 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key

定期清理的两种模式:

  • SLOW模式是定时任务,执行频率默认为10hz(每秒执行10次),每次不超过25ms,以通过修改配置文件redis.conf 的 hz 选项来调整这个次数
  • FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms
优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。
缺点:难以确定删除操作执行的时长和频率。

Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用。

Redis的数据淘汰策略有哪些 ?

(假如缓存过多,内存是有限的,内存被占满了怎么办?)

这个在redis中提供了很多种,默认是noeviction,不删除任何数据,内部不足直接报错

是可以在redis的配置文件中进行设置的,里面有两个非常重要的概念,一个是LRU,另外一个是LFU

LRU的意思就是最少最近使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。

LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高

我们在项目设置的allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis中

面试官:数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?

候选人

可以使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据

面试官:Redis的内存用完了会发生什么?

候选人

嗯~,这个要看redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的 allkeys-lru 策略。把最近最常访问的数据留在缓存中。

数据淘汰策略

1694156854638.png

数据淘汰策略-使用建议

  1. 优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。
  2. 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰。
  3. 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。
  4. 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略。

Redis分布式锁如何实现 ?

候选人:嗯,在redis中提供了一个命令setnx(SET if not exists)

由于redis的单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的

面试官:好的,那你如何控制Redis实现分布式锁有效时长呢?

1694161573659.png

候选人:嗯,的确,redis的setnx指令不好控制这个问题,我们当时采用的redis的一个框架redisson实现的。

在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了

还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。

加锁、设置过期时间等操作都是基于lua脚本完成,lua脚本保证redis多条命令执行的原子性。

image.png

image.png

面试官:好的,redisson实现的分布式锁是可重入的吗?

1694161468379.png

候选人:嗯,是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数

面试官:redisson实现的分布式锁能解决主从一致性的问题吗

1694161721721.png 候选人:这个是不能的,比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。

我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,并且要求在大多数redis节点上都成功创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。

但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁

面试官:好的,如果业务非要保证数据的强一致性,这个该怎么解决呢?

候选人: 嗯~,redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。

Redis集群有哪些方案, 知道嘛 ?

候选人:在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群~~

面试官:那你来介绍一下主从同步

1694166847153.png 候选人:嗯,是这样的,单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。

面试官:能说一下,主从同步数据的流程

候选人:嗯好!主从同步分为了两个阶段,一个是全量同步,一个是增量同步~~

全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:

第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。

第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。

第三:在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致

当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步

增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

怎么保证Redis的高并发高可用

候选人:首先可以搭建主从集群,再加上使用redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知;如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证redis的高并发高可用

1694166767944.png

哨兵选主规则
首先判断主与从节点断开时间长短,如超过指定值就排该从节点
然后判断从节点的slave-priority值,越小优先级越高
如果slave-prority一样,则判断slave节点的offset值,越大优先级越高
最后是判断slave节点的运行id大小,越小优先级越高。

面试官:你们使用redis是单点还是集群,哪种集群

候选人:嗯!,我们当时使用的是主从(1主1从)加哨兵。一般单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务

面试官:redis集群脑裂,该怎么解决呢?

1694166919546.png

候选人:嗯! 这个在项目很少见,不过脑裂的问题是这样的,我们现在用的是redis的哨兵模式集群的

有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致old master中的大量数据丢失。

关于解决的话,我记得在redis的配置中可以设置:第一可以设置最少的salve节点个数,比如设置至少要有一个从节点才能同步数据,第二个可以设置主从数据复制和同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失
redis中有两个配置参数:
min-replicas-to-write 1 表示最少的salve节点为1个
min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒

1694166963464.png

redis的分片集群有什么作用

候选人:分片集群主要解决的是,海量数据存储的问题,集群中有多个master,每个master保存不同数据,并且还可以给每个master设置多个slave节点,就可以继续增大集群的高并发能力。同时每个master之间通过ping监测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点

面试官:Redis分片集群中数据是怎么存储和读取的?

候选人

嗯~,在redis集群中是这样的

Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围, 根据key的有效部分计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分),余数做为插槽,寻找插槽所在的实例

Redis是单线程的,但是为什么还那么快?

候选人

1、Redis是纯内存操作,执行速度非常快,他的性能瓶颈是网络延迟而不是执行速度,因此多线程并不会带来巨大的性能提升

2、采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题

3、使用多路I/O复用模型来提高网络性能

例如:bgsave 和 bgrewriteaof 都是在后台执行操作,不影响主线程的正常使用,不会产生阻塞

面试官:能解释一下I/O多路复用模型?

候选人:I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。~~

其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;

在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

用户空间和内核空间

1694168499614.png

常见的I/O模型

Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度, I/O多路复用模型主要就是实现了高效的网络请求

1694168533049.png

1694168596421.png

1694168656053.png

1694168683935.png

Redis网络模型

Redis 内部使用文件事件处理器 file event handler,它是单线程的,所以Redis才叫做单线程模型。它采用IO多路复用机制同时监听多个 socket,将产生事件的 socket 压入任务队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。

文件事件处理器的结构

  • 多个 socket
  • IO 多路复用程序
  • 文件事件分派器
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。

1694168742394.png

一次客户端与Redis的完整通信过程

image.png

建立连接

  1. 首先,redis 服务端进程初始化的时候,会将 server socket 的 AE_READABLE 事件与连接应答处理器关联。
  2. 客户端 socket01 向 redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 AE_READABLE 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。
  3. 文件事件分派器从队列中获取 socket,交给连接应答处理器。
  4. 连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 AE_READABLE 事件与命令请求处理器关联。

执行一个set请求

  1. 客户端发送了一个 set key value 请求,此时 redis 中的 socket01 会产生 AE_READABLE 事件,IO 多路复用程序将 socket01 压入队列,
  2. 此时事件分派器从队列中获取到 socket01 产生的 AE_READABLE 事件,由于前面 socket01 的 AE_READABLE 事件已经与命令请求处理器关联,
  3. 因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 key value 并在自己内存中完成 key value 的设置。
  4. 操作完成后,它会将 socket01 的 AE_WRITABLE 事件与命令回复处理器关联。
  5. 如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 AE_WRITABLE 事件,同样压入队列中,
  6. 事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 ok,之后解除 socket01 的 AE_WRITABLE 事件与命令回复处理器的关联。

Redis 如何解决 key 冲突?

Redis 如果 key 相同,后一个 key 会覆盖前一个 key。如果要解决 key 冲突,可以按业务名和参数区分开取名,避免重复 key 导致的冲突。

怎么提高缓存命中率?

  1. 预热缓存: 在应用程序启动时,可以预热缓存,将常用的数据加载到缓存中。这可以避免在应用程序刚启动时出现大量的缓存未命中。
  2. 使用二级缓存: 除了Redis作为主缓存外,还可以使用二级缓存(如本地缓存,例如Guava Cache)来进一步提高命中率。这可以减少对Redis的访问次数,从而降低负载。
  3. 使用LRU算法: Redis支持LRU(最近最少使用)算法,可以自动删除最近最少使用的键,以腾出空间给新数据。通过配置适当的内存限制和最大键数,可以确保缓存中总是包含最重要的数据。
  4. 合理设置缓存过期时间: 根据业务需求和数据的更新频率,设置适当的缓存过期时间。不需要永久缓存的数据可以设置有限的过期时间,以确保缓存中的数据总是新鲜的。
  5. 合理使用数据结构: 根据数据的特性选择合适的Redis数据结构。例如,如果数据是无序的,可以使用集合;如果需要排序,可以使用有序集合;如果需要计数,可以使用计数器

假如 Redis 里面有 1 亿个 key,其中有 10w 个 key 是以某个固定的已知的前缀开头的,如果将它们全部找出来?

如果你想从Redis中找出以某个固定的已知前缀开头的所有key,你可以使用Redis的SCAN命令或者KEYS命令。

  1. 使用SCAN命令: SCAN命令可以用来迭代遍历Redis中的key集合,而不会阻塞服务器。你可以结合MATCH参数来过滤出以特定前缀开头的key。

    示例:

    SCAN 0 MATCH your_prefix*
    

    这个命令会从游标0开始扫描Redis中的key集合,匹配以"your_prefix"开头的key。Redis 将返回一个列表,其中包含所有匹配前缀的键,以及一个新的游标值。如果还有更多的匹配键需要返回。需要使用新游标值多次执行SCAN命令来遍历整个数据集,直到游标返回0为止。

  2. 使用KEYS命令(不推荐): KEYS命令可以用来查找匹配给定模式的key,但不推荐在生产环境中使用,因为它会阻塞Redis服务器,可能导致性能问题。

    示例:

    KEYS your_prefix*
    

    这个命令会立即返回以"your_prefix"开头的所有key,但可能会导致Redis服务器在处理大量key时性能下降。

Redis 报内存不足怎么处理?

  1. 检查内存使用情况: 首先,使用Redis的INFO命令或监控工具来检查Redis的内存使用情况。查看已用内存、最大内存限制、剩余内存等信息,以确定内存不足的原因。
  2. 设置合理的最大内存限制: 如果Redis没有设置最大内存限制(maxmemory),或者设置得过小,可以通过修改配置文件来设置一个合理的最大内存限制。确保将最大内存限制设置为系统可用内存的一部分,以避免Redis占用过多内存。
  3. 设置缓存淘汰策略: Redis支持LRU(最近最少使用)算法,可以自动删除最近最少使用的键,以腾出空间给新数据。
  4. 使用内存优化数据结构: 如果内存不足是由于大量数据导致的,考虑使用更紧凑或内存友好的数据结构,以减少内存使用。例如,使用Redis的哈希表数据结构可以减少存储多个小键值对的内存开销。
  5. 删除不必要的数据: 如果数据中包含不再需要的或过期的键值对,可以使用DEL命令来删除它们,释放内存。
  6. 使用数据分区: 如果数据集非常大,考虑将数据分区到多个Redis实例中,以便每个实例只处理一部分数据。这可以通过Redis的分布式特性来实现,以减轻单个实例的内存压力。
  7. 启用数据持久化: 使用数据持久化机制(如RDB快照或AOF日志)来将数据保存到磁盘上,以释放内存。当Redis重新启动时,可以从持久化文件中恢复数据。
  8. 升级硬件或扩展内存: 如果内存不足是由于硬件资源不足导致的,考虑升级服务器的内存或者在集群中添加更多的Redis节点来扩展内存容量。

Memcache与Redis的区别都有哪些?

ChatGPT

Memcached(通常称为Memcache)和Redis都是用于缓存的内存数据库,它们在某些方面相似,但也有一些重要的区别。以下是Memcached和Redis的主要区别:

  1. 数据类型支持:

    • Redis支持丰富的数据结构,如字符串、列表、集合、有序集合、散列等。这些数据结构允许更复杂的数据操作,如计数、排序、范围查询等。
    • Memcached只支持简单的键值对存储,不支持复杂的数据结构。
  2. 持久性:

    • Redis支持数据持久性,可以将数据保存到磁盘上,并在重启后恢复数据。这使得Redis更适合于用作数据库的辅助存储。
    • Memcached不提供持久性支持,数据通常在内存中,并且不会持久化到磁盘。
  3. 复制和高可用性:

    • Redis支持主从复制,可以创建多个Redis实例的副本来提高可用性和数据冗余。
    • Memcached没有内置的复制机制,需要依赖客户端库或第三方工具来实现高可用性。
  4. 数据过期策略:

    • Redis支持设置键的过期时间,可以自动删除过期的数据。这对于缓存和会话管理非常有用。
    • Memcached也支持过期时间,但其过期策略较为简单,通常是基于LRU(最近最少使用)算法的。
  5. 数据分区:

    • Redis支持数据分区和分片,可以水平扩展存储容量和吞吐量。
    • Memcached也支持分布式部署,但数据分区相对较简单。
  6. 性能:

    • Redis通常在读取和写入性能方面表现更好,因为它支持更复杂的数据结构和数据持久化。
    • Memcached在写入性能方面可能更高,因为它专注于简单的键值对操作。
  7. 社区和生态系统:

    • Redis有更广泛的社区支持,拥有丰富的客户端库和插件,以及大量的文档和教程。
    • Memcached也有一定的社区支持,但相对来说较小。

总的来说,选择使用Memcached还是Redis取决于您的具体需求。如果您需要更丰富的数据结构、数据持久性和高级功能,那么Redis可能更适合您。如果您只需要一个简单的键值对存储,并且注重性能,那么Memcached可能足够了。在某些情况下,两者也可以结合使用,以满足不同方面的需求。

Redis是单线程的,如何提高多核CPU的利用率?

可以在同一个服务器部署多个Redis的实例,并把他们当作不同的服务器来使用,在某些时候,无论如何一个服务器是不够的, 所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。

Redis 的数据类型及使用场景并举例说明

Redis支持多种数据类型,每种数据类型都有其特定的用途和场景。以下是Redis的主要数据类型及其使用场景,并附带示例说明:

  1. 字符串(String):

    • 使用场景:存储单个值,如用户会话、计数器、配置信息等。

    • 示例:

      SET username "john_doe"
      GET username
      
  2. 列表(List):

    • 使用场景:存储有序的多个值,可用于消息队列、任务队列、时间线等。

    • 示例:

      LPUSH tasks "task1"
      LPUSH tasks "task2"
      LRANGE tasks 0 -1
      

      Redis Lrange 返回列表中指定区间内的元素,区间以偏移量 START 和 END 指定。 其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。

  3. 集合(Set):

    • 使用场景:存储无序的唯一值,可用于记录用户的喜好、标签、关注者等。

    • 示例:

      SADD tags "tag1"
      SADD tags "tag2"
      SMEMBERS tags
      
  4. 有序集合(Sorted Set):

    • 使用场景:存储有序的唯一值,并为每个值分配一个分数,可用于排行榜、优先级队列等。

    • 示例:

      ZADD leaderboard 100 "player1"
      ZADD leaderboard 200 "player2"
      ZRANGE leaderboard 0 -1 WITHSCORES
      
  5. 散列(Hash):

    • 使用场景:存储字段-值对,用于表示对象或实体,如用户信息、产品信息等。

    • 示例:

      HSET user:id:123 username "john_doe"
      HSET user:id:123 email "john@example.com"
      HGETALL user:id:123
      
  6. 位图(Bitmap):

    • 使用场景:存储位操作数据,用于记录用户在线状态、签到情况等。

    • 示例:

      SETBIT user:status 1000 1
      GETBIT user:status 1000
      

      SETBIT key offset value

  7. 地理空间索引(Geospatial Index):

    • 使用场景:存储地理坐标,用于地理位置相关的应用,如附近的人、商店定位等。

    • 示例:

      GEOADD locations 13.361389 38.115556 "Palermo"
      GEODIST locations "Palermo" "Catania" km
      GEORADIUS locations 15 37 200 km
      
  8. HyperLogLog:

    • 使用场景:用于近似的基数计数,如统计网站的独立访问者数量。

    • 示例:

      PFADD visitors "user1"
      PFADD visitors "user2"
      PFCOUNT visitors
      

这些是Redis的主要数据类型,每种类型都有独特的用途和适用场景。根据您的应用程序需求,您可以选择合适的数据类型来存储和处理数据。 Redis的多种数据结构和高性能使其成为各种应用程序的理想选择,从缓存到实时分析,再到地理信息系统等各种领域。

了解Redis的事务吗

在Redis中,事务是一组Redis命令的有序执行序列,可以保证这些命令要么全部执行成功,要么全部执行失败,具有原子性。Redis的事务机制基于MULTI、EXEC、DISCARD和WATCH等命令来实现。

以下是Redis事务的主要概念和用法:

  1. MULTI: 事务的开始标志。当执行MULTI命令后,Redis会进入事务模式,接下来的命令不会立即执行,而是被放入一个队列中等待执行。
  2. EXEC: 用于执行事务中的所有命令。一旦执行EXEC命令,Redis会按照事务队列中的顺序执行所有命令,并返回执行结果。如果其中某个命令失败(例如,由于数据冲突或语法错误),则事务会回滚,所有命令都不会执行。
  3. DISCARD: 用于取消事务,将所有排队的命令清空,不执行。可以在事务开始后随时使用DISCARD来放弃执行事务。
  4. WATCH: 用于监视一个或多个键,如果在事务执行期间被监视的键发生了变化,事务将被中断(即,不会执行EXEC),以防止并发冲突。WATCH命令可以用来实现乐观锁。

Redis事务的使用示例:

MULTI
SET key1 "value1"
SET key2 "value2"
EXEC

在这个示例中,MULTI表示开始一个事务,然后紧跟着两个SET命令将键值对添加到事务队列中,最后通过EXEC命令执行事务。

Redis事务是一种强大的功能,用于确保一组命令的原子性执行。然而,需要注意的是,Redis事务并不支持回滚到保存点,一旦EXEC执行,所有命令将被执行,因此需要小心处理事务中可能导致失败的命令。

为什么Redis的操作是原子性的,怎么保证原子性的?

Redis的操作是原子性的,这意味着Redis中的每个命令都是不可分割的单个操作,要么完全执行,要么不执行,不会中间状态或部分执行。Redis保证原子性的主要原因和方法如下:

  1. 单线程模型: Redis采用单线程的模型来处理客户端请求。这意味着在任何给定时刻,只有一个命令在执行,Redis不会被并发的多个命令干扰。这可以避免竞态条件和数据不一致的问题。

  2. 事务: Redis支持事务,可以将一组命令包装在MULTIEXEC命令之间,这样它们就会以原子方式执行。在事务中,Redis会在EXEC命令执行之前将所有的命令排队,并在执行时确保没有其他客户端的命令被插入。

    示例:

    MULTI
    SET key1 "value1"
    SET key2 "value2"
    EXEC
    
  3. 保证单命令原子性: 即使不使用事务,Redis也会确保单个命令的原子性。例如,对于字符串的SET命令,要么设置成功,要么失败,不会出现部分更新的情况。同样,对于列表、集合等数据结构,Redis的命令也是原子的。

  4. WATCH命令: Redis还提供了WATCH命令,用于乐观锁控制。客户端可以在事务开始前监视一个或多个键,如果在事务执行期间被监视的键发生了变化,事务将被取消。这可以用于实现基于键的并发控制。

    示例:

    WATCH key1
    MULTI
    SET key1 "new_value"
    EXEC
    
  5. 分布式锁: Redis还可以用于实现分布式锁,通过SETNX(SET if Not eXists)命令来获取锁。这个命令是原子的,只有一个客户端能够成功获取锁。

    示例:

    SETNX lock:resource1 "locked"
    

总之,Redis通过其单线程执行模型、事务支持和保证单个命令的原子性,来确保数据操作的原子性。这意味着Redis的操作要么全部成功,要么全部失败,从而保持数据的一致性和可靠性。这对于处理并发和事务性要求高的应用程序非常重要。但要注意,原子性并不代表并发性,仍然需要考虑并发访问和竞态条件的问题。

同时有多个子系统去set一个key。这个时候要注意什么呢?

不推荐使用redis的事务机制。因为我们的生产环境,基本都是redis集群环境,做了数据分片操作。你一个事务中有涉及到多个key操作的时候,这多个key不一定都存储在同一个redis-server上。因此,redis的事务机制,十分鸡肋。

  1. 如果对这个key操作,不要求顺序: 准备一个分布式锁,大家去抢锁,抢到锁就做set操作即可
  2. 如果对这个key操作,要求顺序: 分布式锁+时间戳。 假设系统B先抢到锁,将key1设置为 {valueB 3:05}。接下来系统A抢到锁,发现自己的valueA的时间戳早于缓存中的时间戳,那就不做set操作了。以此类推。
  3. 利用队列,将set方法变成串行访问也可以,redis遇到高并发,如果保证读写key的一致性 对redis的操作都是具有原子性的,是线程安全的操作,你不用考虑并发问题,redis内 部已经帮你处理好并发的问题了。

Redis 常见性能问题和解决方案?

(1) Master 最好不要做任何持久化工作,如 RDB 内存快照和 AOF 日志文件
(2) 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
(3) 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内 (4) 尽量避免在压力很大的主库上增加从库
(5) 主从复制不要用图状结构,用单向链表结构更为稳定