20道Redis面试题

概述

1. redis是什么?

key-value、内存、持久化
Remote Dictionary Server 远程字典服务,这是一个基于键值对的非关系型数据库,开源,使用ANSI C 语言编写,支持网络,存在内存中,因此读写速度很快,可用于缓存,也支持分布式锁,事务持久化,多种数据结构,并提供多种语言的 API。
好处
速度快、数据类型丰富、支持事务、可持久化、特性丰富

特点
  • 丰富的数据类型
  • 内存存储
  • 持久化功能

    2. 数据结构及应用场景

    简单动态字符串SDS
  • 优点

  • 和普通字符串相比
  • 应用场景

缓存、计数器、共享session

  • 最大可储存512M。

字符串类型的使用场景:信息缓存、计数器、分布式锁等等。
常用命令:get/set/del/incr/decr/incrby/decrby

hash

Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。
商品信息、用户信息等
当对象的某个属性需要频繁修改时,不适合用string+json,因为它不够灵活,每次修改都需要重新将整个对象序列化并赋值;如果使用hash类型,则可以针对某个属性单独修改,没有序列化,也不需要修改整个对象。比如,商品的价格、销量、关注数、评价数等可能经常发生变化的属性,就适合存储在hash类型里。

List

列表类型是用来储存多个有序的字符串,列表中的每个字符串成为元素,一个列表最多可以储存 2 ^ 32 – 1 个元素,可以队列表两端插入和弹出获取指定范围的元素列表和指定索引下的元素等。
比如微博的关注列表, 粉丝列表, 消息列表等功能都可以用Redis的 list 结构来实现。 基于 list 实现分页查询。
定时排行榜

set

用来保存多个字符串的元素,不允许有重复的元素,集合中的元素是无序的。不能通过索引下标获取元素,Redis 除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
共同关注、共同喜好等 收藏夹

Sorted Set

和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
实时排行榜

3. 有MySQL不就够用了吗?为什么要用Redis这种新的数据库?

高性能和高并发。
内存中,快。一致性。并发数可以更高。

4. 为什么不用map/guava做缓存,而用redis?

说缓存分为本地缓存分布式缓存
那以 C++ 语言为例,我们可以使用 STL 下自带的容器 map 来实现缓存,但只能实现本地缓存,它最主
要的特点是轻量以及快速,但是其生命周期随着程序的销毁而结束,并且在多实例的情况下,每个实例
都需要各自保存一份缓存,缓存不具有一致性。
使用 Redis 或 Memcached 之类的称为分布式缓存,在多实例的情况下,各实例共享一份缓存数据,缓存
具有一致性。这是Redis或者Memcached的优点所在,但它也有缺点,那就是需要保持 Redis 或
Memcached服务的高可用,整个程序架构上较为复杂。
对比:
1、Redis 可以用几十 G 内存来做缓存,Map 不行,一般 JVM 也就分几个 G 数据就够大了;
2、Redis 的缓存可以持久化,Map 是内存对象,程序一重启数据就没了;
3、Redis 可以实现分布式的缓存,Map 只能存在创建它的程序里;
4、Redis 可以处理每秒百万级的并发,是专业的缓存服务,Map 只是一个普通的对象;
5、Redis 缓存有过期机制,Map 本身无此功能;Redis 有丰富的 API,Map 就简单太多了;
6、Redis可单独部署,多个项目之间可以共享,本地内存无法共享;
7、Redis有专门的管理工具可以查看缓存数据。

5. memcached和redis区别,优势

  • 数据类型:Memcached所有的值均是简单的字符串,Redis支持更为丰富的数据类型,支持string(字符串),list(列表),Set(集合)、Sorted Set(有序集合)、Hash(哈希)等。
  • 存储方式:Redis支持数据落地持久化存储,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。 memcache全部在内存中,断电后会挂掉,不支持数据持久存储 ,数据不能超过内存大小。
  • 底层模型不同。底层实现方式 以及与客户端之间通信的应用协议不一样。

Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
其他

  • 集群模式:Redis提供主从同步机制,以及 Cluster集群部署能力,能够提供高可用服务。Memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据
  • 性能对比:Redis的速度比Memcached快很多。
  • 网络IO模型:Redis使用单线程的多路 IO 复用模型,Memcached使用多线程的非阻塞IO模式。
  • value值大小不同,redis最大可以达到 512MB;Memcached 只有 1MB。

    6. 缓存中常说的热点数据和冷数据是什么?

    热数据就是访问次数较多的数据,冷数据就是访问很少或者从不访问的数据.
    数据更新前至少读取两次,缓存才有意义。如果缓存还没有起作用就失效了,就没有太大价值了。

    7、分布式Redis是前期做还是后期规模上来了再做好?为什么?

    Redis轻量(单实例只使用1M内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。
    对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。
    这样的话,当你的数据不断增长,需要更多的Redis服务器时仅仅将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。一旦添加了另一台服务器,你需要将你一半的Redis实例从第一台机器迁移到第二台机器。

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

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

    9、怎么保证缓存和数据库数据的一致性?

    一般有如下四种方案:
  1. 先更新数据库,后更新缓存
  2. 先更新缓存,后更新数据库
  3. 先删除缓存,后更新数据库
  4. 先更新数据库,后删除缓存

第一种和第二种方案没有使用。目前主要用第三和第四种方案。
第一种方案存在问题是:并发更新数据库场景下,会将脏数据刷到缓存。考虑两个写线程。
第二种方案存在的问题是:如果先更新缓存成功,但是数据库更新失败,则肯定会造成数据不一致。

双写一致性方案一:先删除缓存,后更新数据库

问题:缓存脏数据。
解决一:延时双删
(1)先淘汰缓存
(2)再写数据库(这两步和原来一样)
(3)休眠1秒,再次淘汰缓存,这么做,可以将1秒内所造成的缓存脏数据,再次删除。确保读请求结束,写请求可以删除读请求造成的缓存脏数据。自行评估自己的项目的读数据业务逻辑的耗时,写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。
Mysql 的读写分离的架构,主从同步之间也会有时间差。导致同步没有完成,读到缓存中的旧数据。解决:是对 Redis 进行填充数据的查询数据库操作,那就强制将其指向主库进行查询。
解决二: 更新与读取操作进行异步串行化
异步串行化:将缓存更新的请求发送到队列中,排在刚才更新库的操作之后,然后同步等待缓存更新完成,再读库。
读操作去重:如果发现队列中已经有了该数据的更新缓存的请求了,那么就不用再放进去了,直接等待前面的更新操作请求完成即可。如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
最经典的缓存+数据库读写的模式,就是 预留缓存模式Cache Aside Pattern

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
  • 更新的时候,先删除缓存,然后再更新数据库,这样读的时候就会发现缓存中没有数据而直接去数据库中拿数据了。

在高并发的业务场景下,数据库的性能瓶颈往往都是用户并发访问过大。所以,一般都使用Redis做一个缓冲操作,让请求先访问到Redis,而不是直接去访问MySQL等数据库,从而减少网络请求的延迟响应。

双写一致性方案二:先更新数据库,后删除缓存

也会出现问题,比如更新数据库成功了,但是在删除缓存的阶段出错了没有删除成功,那么此时再读取缓存的时候每次都是错误的数据了。
解决:利用消息队列进行删除的补偿。删除错误时将key作为消息体发送到消息队列中,系统收到消息后再次进行删除。
缺点是会对业务代码造成大量的侵入,深深的耦合在一起。
优化方案为对 Mysql 数据库更新操作后再 binlog 日志中我们都能够找到相应的操作,那么可以订阅 Mysql 数据库的 binlog 日志对缓存进行操作。
image.png

10、数据为什么会不一致?

主要是在并发读写访问的时候,缓存和数据相互交叉执行。
单库情况下,因最后才把写操作数据入DB,并没同步。cache里面一直保持脏数据。
脏数据是指源系统中的数据不在给定的范围内或对于实际业务毫无意义,或是数据格式非法,以及在源
系统中存在不规范的编码和含糊的业务逻辑。
主从同步,读写分离的情况下,读从库而产生脏数据。
是主从同步的时延问题(假设1s),导致读请求读取从库读到脏数据导致的数据不一致。
总结
单库下,逻辑处理中消耗1s,可能读到旧数据入缓存 ;
主从+读写分离,在1s的主从同步时延中,到从库的旧数据入缓存。

11、hash冲突怎么办?

Redis 通过链式哈希解决冲突:也就是同一个 桶里面的元素使用链表保存。但是当链表过长就会导致查找性能变差可能,所以 Redis 为了追求快,使用了两个全局哈希表。用于 rehash 操作,增加现有的哈希桶数量,减少哈希冲突。
开始默认使用 「hash 表 1 」保存键值对数据,「hash 表 2」 此刻没有分配空间。当数据越来越多触发 rehash 操作,则执行以下操作:

  1. 给 「hash 表 2 」分配更大的空间;
  2. 将 「hash 表 1 」的数据重新映射拷贝到 「hash 表 2」 中;
  3. 释放 「hash 表 1」 的空间。

值得注意的是,将 hash 表 1 的数据重新映射到 hash 表 2 的过程中并不是一次性的,这样会造成 Redis 阻塞,无法提供服务。
而是采用了渐进式 rehash,每次处理客户端请求的时候,先从「 hash 表 1」 中第一个索引开始,将这个位置的 所有数据拷贝到 「hash 表 2」 中,就这样将 rehash 分散到多次请求过程中,避免耗时阻塞。

12、单副本与多副本都是什么?

单副本

Redis单副本,采用单个Redis节点部署架构,没有备用节点实时同步数据,不提供数据持久化和备份策略,适用于数据可靠性要求不高的纯缓存业务场景。
优点:

  • 架构简单,部署方便;
  • 高性价比:缓存使用时无需备用节点(单实例可用性可以用supervisor或crontab保证),当然为了满足业务的高可用性,也可以牺牲一个备用节点,但同时刻只有一个实例对外提供服务;
  • 高性能。

缺点:

  • 不保证数据的可靠性;
  • 在缓存使用,进程重启后,数据丢失,即使有备用的节点解决高可用性,但是仍然不能解决缓存预热问题,因此不适用于数据可靠性要求高的业务;
  • 高性能受限于单核CPU的处理能力(Redis是单线程机制),CPU为主要瓶颈,所以适合操作命令简单,排序、计算较少的场景。也可以考虑用Memcached替代。

    多副本

    Redis多副本,采用主从(replication)部署结构,相较于单副本而言最大的特点就是主从实例间数据实时同步,并且提供数据持久化和备份策略。主从实例部署在不同的物理服务器上,根据公司的基础环境配置,可以实现同时对外提供服务和读写分离策略。
    优点:

  • 高可靠性:一方面,采用双机主备架构,能够在主库出现故障时自动进行主备切换,从库提升为主库提供服务,保证服务平稳运行;另一方面,开启数据持久化功能和配置合理的备份策略,能有效的解决数据误操作和数据异常丢失的问题;

  • 读写分离策略:从节点可以扩展主库节点的读能力,有效应对大并发量的读操作。

缺点:

  • 故障恢复复杂,如果没有RedisHA系统(需要开发),当主库节点出现故障时,需要手动将一个从节点晋升为主节点,同时需要通知业务方变更配置,并且需要让其它从库节点去复制新主库节点,整个过程需要人为干预,比较繁琐;
  • 主库的写能力受到单机的限制,可以考虑分片;
  • 主库的存储能力受到单机的限制,可以考虑Pika;
  • 原生复制的弊端在早期的版本中也会比较突出,如:Redis复制中断后,Slave会发起psync,此时如果同步不成功,则会进行全量同步,主库执行全量备份的同时可能会造成毫秒或秒级的卡顿;又由于COW机制,导致极端情况下的主库内存溢出,程序异常退出或宕机;主库节点生成备份文件导致服务器磁盘IO和CPU(压缩)资源消耗;发送数GB大小的备份文件导致服务器出口带宽暴增,阻塞请求,建议升级到最新版本。

    13、Redis常用的客户端有哪些?

    Jedis:是老牌的Redis的Java实现客户端,提供了比较全面的Redis命令的支持。
    Redisson:实现了分布式和可扩展的Java数据结构。
    Lettuce:高级Redis客户端,用于线程安全同步,异步和响应使用,支持集群,Sentinel,管道和编码器。
    优点:
    Jedis:比较全面的提供了Redis的操作特性。
    Redisson:促使使用者对Redis的关注分离,提供很多分布式相关操作服务,例如,分布式锁,分布式集合,可通过Redis支持延迟队列。
    Lettuce:基于Netty框架的事件驱动的通信层,其方法调用是异步的。Lettuce的API是线程安全的,所以可以操 作单个Lettuce连接来完成各种操作。

    14、redis如何做内存优化?

    1、控制key的数量:当使用Redis存储大量数据时,通常会存在大量键,过多的键同样会消耗大量内存。Redis本质是一个数据结构服务器,它为我们提供多种数据结构,如hash,list,set,zset 等结构。使用Redis时不要进入一个误区,大量使用get/set这样的API,把Redis当成Memcached使用。对于存储相同的数据内容利用Redis的数据结构降低外层键的数量,也可以节省大量内存。
    2、缩减键值对象,降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长度。

  • key长度:如在设计键时,在完整描述业务情况下,键值越短越好。

  • value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。

3、编码优化。Redis对外提供了string,list,hash,set,zet等类型,但是Redis内部针对不同类型存在编码的概念,所谓编码就是具体使用哪种底层数据结构来实现。编码不同将直接影响数据的内存占用和读写效率。

15、常见的数据优化方案你了解吗?

缓存双淘汰法 (延时淘汰)
1. 先淘汰缓存
2. 再写数据库
3. 往消息总线esb发送一个淘汰消息,发送立即返回。写请求的处理时间几乎没有增加,这个方法淘汰了缓存两次。因此被称为“缓存双淘汰法“,而在消息总线下游,有一个异步淘汰缓存的消费者,在拿到淘汰消息在1s后淘汰缓存,这样,即使在一秒内有脏数据入缓存,也能够被淘汰掉。
异步淘汰缓存
上述的步骤,都是在业务线里面执行,新增一个线下的读取binlog异步淘汰缓存模块,读取binlog总的数据,然后进行异步淘汰。
这里简单提供一个思路:
MySQL binlog增量发布订阅消费+消息队列+增量数据更新到Redis
1)读请求走Redis:热数据基本都在Redis
2)写请求走MySQL: 增删改都操作MySQL
3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis
Redis更新时数据操作主要分为两块: 全量(将全部数据一次写入到Redis) ,增量(实时更新)
这里说的是增量,指的是mysql的update、insert、delate变更数据。这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新,就无需在从业务线去操作缓存内容。

16、介绍以下redis自研

Redis自研的高可用解决方案,主要体现在配置中心、故障探测和failover的处理机制上,通常需要根据企业业务的实际线上环境来定制化。
优点:

  • 高可靠性、高可用性;
  • 自主可控性高;
  • 贴切业务实际需求,可缩性好,兼容性好。

缺点:

  • 实现复杂,开发成本高;
  • 需要建立配套的周边设施,如监控,域名服务,存储元数据信息的数据库等;
  • 维护成本高。