面试题:什么是 Redis?可以做什么?与Memcached异同?常见数据结构与使用场景

2023年 7月 10日 61.5k 0

面试题:什么是 Redis?可以做什么?与Memcached异同?常见数据结构与使用场景

来源 | 小白在挨踢 (ID:gh_07ce59a80110)

Redis 在当今的计算机行业,可以说是使用的最为广泛的内存数据库,几乎所有的后端技术面试都会涉及到 Redis 相关的知识,正所谓知己知彼,百战百胜。小白今天精心整理的超全的 Redis 面试题,希望可以帮助到在路上的你们~

什么是 Redis

Redis 是完全开源免费的,遵守 BSD 协议,是一个高性能的 key-value 数据库

是一个使用 C 语言开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的,它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向

Redis 除了做缓存之外,也经常用来做分布式锁,甚至是消息队列

Redis 还支持事务 、持久化、Lua 脚本、多种集群方案等,可以使用众多复杂的业务场景

常见的分布式缓存技术

缓存使用的比较多的主要是 Memcached 和 Redis。不过现在使用 Memcached 做缓存的比较少

Memcached 是分布式缓存最开始兴起的时候比较常用的方案,后来随着 Redis 的发展,Redis 逐渐成为了人们的首选

分布式缓存主要解决的是单机缓存的容量受服务器限制并且无法保存通用信息的问题。因为本地缓存只在当前服务里有效,比如如果你部署了两个相同的服务,他们两者之间的缓存数据是无法共享的

Memcached 与 Redis 的异同

共同点

  • 都是基于内存的数据库,一般都用来当作缓存使用
  • 都有过期策略
  • 两者的性能都非常高
  • 异同点

  • Redis 支持更丰富的数据类型,Memcached 只支持最简单的 k/v 数据类型,所有的值均是简单的字符串
  • Redis 支持数据的持久化,而 Memecache 把数据全部存在内存之中,重启之后数据丢失
  • Redis 有灾难恢复机制,因为可以把缓存中的数据持久化到磁盘上
  • Redis 在服务器内存使用完之后,可以将不用的数据放到磁盘上。但 Memcached 在服务器内存使用完之后,就会直接报异常
  • Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但 Redis目前是原生支持 cluster 模式的
  • Memcached 是多线程,非阻塞 IO 复用的网络模型;Redis 使用单线程的多路 IO 复用模型
  • Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持。并且,Redis 支持更多的编程语言
  • Memcached 过期数据的删除策略只用了惰性删除,而 Redis 同时使用了惰性删除与定期删除
  • Redis 的数据类型

    Redis 支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及 zsetsorted set:有序集合)

    实际中我们最为常用的还是 string 类型,不过还有几种高级数据类型我们也需要了解掌握:BloomFilter,RedisSearch,Redis-ML,还有更高级的数据类型,BloomFilter,RedisSearch,Redis-ML 如果使用过,那么在面试官眼里都是加分项!

    缓存数据的处理流程

    缓存的简单处理流程如下:

  • 如果用户请求的数据命中缓存,就直接返回
  • 缓存中不存在的话,查看数据库中是否存在
  • 如果数据库中存在,则更新缓存中的数据
  • 如果数据库中不存在,则返回空数据
  • 面试题:什么是 Redis?可以做什么?与Memcached异同?常见数据结构与使用场景

    上图就是一个最为简易的缓存流程图,从图中我们也可以看出在使用缓存时的一系列注意事项,比如当缓存失效时,如何更好的保护数据库,如何保证数据库与缓存数据一致等

    Redis 运行模式(单进程还是单线程)

    Redis 是单线程,利用队列技术将并发访问变为串行访问,消除了传统数据库串行控制的开销问题

    一个字符串能存储的最大容量

    512 M

    为什么要使用缓存

    高性能

    将高频访问的数据放进缓存,保证高频操作可以快速响应,提高系统响应速度和用户体验

    高并发

    一般像 MySQL 这类数据库的 QPS 大概都在 1w 左右(4 核 8g) ,但是使用 Redis 缓存之后很容易达到 10w+,甚至最高能达到 30w+(redis 集群的话会更高)

    QPS(Query Per Second):服务器每秒可以执行的查询次数

    所以增加缓存可以大大提高系统的并发能力

    具体说说 Redis 可以做什么

    缓存

    缓存机制几乎在所有的大型网站都有使用,合理地使用缓存不仅可以加 快数据的访问速度,而且能够有效地降低后端数据源的压力。Redis提供了键值过期时间设置,并且也提供了灵活控制最大内存和内存溢出后的淘汰策略。可以这么说,一个合理的缓存设计能够很好的为一个网站的稳定保驾护航

    排行榜系统

    排行榜系统几乎存在于所有的网站,例如按照热度排名的排行榜,按照 发布时间的排行榜,按照各种复杂维度计算出的排行榜,Redis提供了列表和有序集合数据结构,合理地使用这些数据结构可以很方便地构建各种排行榜系统

    计数器应用

    计数器在网站中的作用至关重要,例如视频网站有播放数、电商网站有 浏览数,为了保证数据的实时性,每一次播放和浏览都要做加1的操作,如果并发量很大对于传统关系型数据的性能是一种挑战。Redis天然支持计数功能而且计数的性能也非常好,可以说是计数器系统的重要选择

    社交网络

    赞/踩、粉丝、共同好友/喜好、推送、下拉刷新等是社交网站的必备功能,由于社交网站访问量通常比较大,而且传统的关系型数据不太适合保存这种类型的数据,Redis 提供的数据结构可以相对比较容易地实现这些功能

    消息队列系统

    消息队列系统可以说是一个大型网站的必备基础组件,因为其具有业务 解耦、非实时业务削峰等特性。Redis 提供了发布订阅功能和阻塞队列的功能,虽然和专业的消息队列比还不够足够强大,但是对于一般的消息队列功能基本可以满足

    哪些是 Redis 不可以(不擅长)做的

    我们可以站在数据规模和数据冷热的角度来进行分析

    站在数据规模的角度看,数据可以分为大规模数据和小规模数据,我们 知道 Redis 的数据是存放在内存中的,虽然现在内存已经足够便宜,但是如果数据量非常大,例如每天有几亿的用户行为数据,使用 Redis 来存储的话,基本上是个无底洞,经济成本相当的高

    站在数据冷热的角度看,数据分为热数据和冷数据,热数据通常是指需 要频繁操作的数据,反之为冷数据,例如对于视频网站来说,视频基本信息基本上在各个业务线都是经常要操作的数据,而用户的观看记录不一定是经常需要访问的数据,这里暂且不讨论两者数据规模的差异,单纯站在数据冷热的角度上看,视频信息属于热数据,用户观看记录属于冷数据。如果将这些冷数据放在 Redis 中,基本上是对于内存的一种浪费,但是对于一些热数据可以放在 Redis 中加速读写,也可以减轻后端存储的负载,可以说是事半功倍

    Redis 常见数据结构与使用场景

    String

    String 数据结构是简单的 key-value 类型,也是我们平时使用的最多的数据类型

    常用命令:set,get,strlen,exists,decr,incr,setex 等等

    应用场景:一般常用在需要计数的场景,比如用户的访问次数、热点文章的点赞转发数量等等

    基本操作

    127.0.0.1:6379> set key value # 设置 key-value 类型的值
    OK
    127.0.0.1:6379> get key # 根据 key 获得对应的 value
    "value"
    127.0.0.1:6379> exists key # 判断某个 key 是否存在
    (integer) 1
    127.0.0.1:6379> strlen key # 返回 key 所储存的字符串值的长度
    (integer) 5
    127.0.0.1:6379> del key # 删除某个 key 对应的值
    (integer) 1
    127.0.0.1:6379> get key
    (nil)Copy to clipboardErrorCopied
    

    批量设置

    127.0.0.1:6379> mset key1 value1 key2 value2 # 批量设置 key-value 类型的值
    OK
    127.0.0.1:6379> mget key1 key2 # 批量获取多个 key 对应的 value
    1) "value1"
    2) "value2"Copy to clipboardErrorCopied
    

    计数器设置

    127.0.0.1:6379> set number 1
    OK
    127.0.0.1:6379> incr number # 将 key 中储存的数字值增一
    (integer) 2
    127.0.0.1:6379> get number
    "2"
    127.0.0.1:6379> decr number # 将 key 中储存的数字值减一
    (integer) 1
    127.0.0.1:6379> get number
    "1"Copy to clipboardErrorCopied
    

    过期设置

    127.0.0.1:6379> expire key 60 # 数据在 60s 后过期
    (integer) 1
    127.0.0.1:6379> setex key 60 value # 数据在 60s 后过期 (setex:[set] + [ex]pire)
    OK
    127.0.0.1:6379> ttl key # 查看数据还有多久过期
    (integer) 56Copy to clipboardErrorCopied
    

    list

    list 即是链表,链表是一种非常常见的数据结构,特点是易于数据元素的插入和删除并且可以灵活调整链表长度,但是链表的随机访问困难

    常用命令:rpush,lpop,lpush,rpop,lrange,llen 等

    应用场景:发布与订阅或者说消息队列、慢查询等

    队列和栈的实现

    面试题:什么是 Redis?可以做什么?与Memcached异同?常见数据结构与使用场景

    通过 rpush/lpop 实现队列

    127.0.0.1:6379> rpush myList value1 # 向 list 的头部(右边)添加元素
    (integer) 1
    127.0.0.1:6379> rpush myList value2 value3 # 向 list 的头部(最右边)添加多个元素
    (integer) 3
    127.0.0.1:6379> lpop myList # 将 list 的尾部(最左边)元素取出
    "value1"
    127.0.0.1:6379> lrange myList 0 1 # 查看对应下标的 list 列表, 0 为 start,1为 end
    1) "value2"
    2) "value3"
    127.0.0.1:6379> lrange myList 0 -1 # 查看列表中的所有元素,-1表示倒数第一
    1) "value2"
    2) "value3"Copy to clipboardErrorCopied
    

    通过 rpush/rpop 实现栈

    127.0.0.1:6379> rpush myList2 value1 value2 value3
    (integer) 3
    127.0.0.1:6379> rpop myList2 # 将 list的头部(最右边)元素取出
    "value3"Copy to clipboardErrorCopied
    

    hash

    hash 类似于 JDK1.8 前的 HashMap,内部实现也差不多(数组 + 链表),不过 Redis 的 hash 做了更多优化。另外 hash 是一个 string 类型的 field 和 value 的映射表,特别适合用于存 储对象,比如我们可以用 hash 数据结构来存储用户信息,商品信息等等

    常用命令:hset,hmset,hexists,hget,hgetall,hkeys,hvals 等

    应用场景:系统中对象数据的存储

    127.0.0.1:6379> hmset userInfoKey name "guide" description "dev" age "24"
    OK
    127.0.0.1:6379> hexists userInfoKey name # 查看 key 对应的 value 中指定的字段是否存在
    (integer) 1
    127.0.0.1:6379> hget userInfoKey name # 获取存储在哈希表中指定字段的值
    "guide"
    127.0.0.1:6379> hget userInfoKey age
    "24"
    127.0.0.1:6379> hgetall userInfoKey # 获取在哈希表中指定 key 的所有字段和值
    1) "name"
    2) "guide"
    3) "description"
    4) "dev"
    5) "age"
    6) "24"
    127.0.0.1:6379> hkeys userInfoKey # 获取 key 列表
    1) "name"
    2) "description"
    3) "age"
    127.0.0.1:6379> hvals userInfoKey # 获取 value 列表
    1) "guide"
    2) "dev"
    3) "24"
    127.0.0.1:6379> hset userInfoKey name "GuideGeGe" # 修改某个字段对应的值
    127.0.0.1:6379> hget userInfoKey name
    "GuideGeGe"Copy to clipboardErrorCopied
    

    set

    set 类似于 Java 中的 HashSet,Redis 中的 set 类型是一种无序集合,集合中的元素没有先后顺序。当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。如:可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis 可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能

    常用命令:sadd,spop,smembers,sismember,scard,sinterstore,sunion 等

    应用场景:需要存放的数据不能重复以及需要获取多个数据源交集和并集等场景

    127.0.0.1:6379> sadd mySet value1 value2 # 添加元素进去
    (integer) 2
    127.0.0.1:6379> sadd mySet value1 # 不允许有重复元素
    (integer) 0
    127.0.0.1:6379> smembers mySet # 查看 set 中所有的元素
    1) "value1"
    2) "value2"
    127.0.0.1:6379> scard mySet # 查看 set 的长度
    (integer) 2
    127.0.0.1:6379> sismember mySet value1 # 检查某个元素是否存在set 中,只能接收单个元素
    (integer) 1
    127.0.0.1:6379> sadd mySet2 value2 value3
    (integer) 2
    127.0.0.1:6379> sinterstore mySet3 mySet mySet2 # 获取 mySet 和 mySet2 的交集并存放
    在 mySet3 中
    (integer) 1
    127.0.0.1:6379> smembers mySet3
    1) "value2"Copy to clipboardErrorCopied
    

    sorted set

    和 set 相比,sorted set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列,还可以通过 score 的范围来获取元素的列表

    常用命令:zadd,zcard,zscore,zrange,zrevrange,zrem 等

    应用场景:需要对数据根据某个权重进行排序的场景。比如在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息等信息

    127.0.0.1:6379> zadd myZset 3.0 value1 # 添加元素到 sorted set 中 3.0 为权重
    (integer) 1
    127.0.0.1:6379> zadd myZset 2.0 value2 1.0 value3 # 一次添加多个元素
    (integer) 2
    127.0.0.1:6379> zcard myZset # 查看 sorted set 中的元素数量
    (integer) 3
    127.0.0.1:6379> zscore myZset value1 # 查看某个 value 的权重
    "3"
    127.0.0.1:6379> zrange myZset 0 -1 # 顺序输出某个范围区间的元素,0 -1 表示输出所有元素
    1) "value3"
    2) "value2"
    3) "value1"
    127.0.0.1:6379> zrange myZset 0 1 # 顺序输出某个范围区间的元素,0 为 start 1 为 stop
    1) "value3"
    2) "value2"
    127.0.0.1:6379> zrevrange myZset 0 1 # 逆序输出某个范围区间的元素,0 为 start 1 为
    stop
    1) "value1"
    2) "value2"Copy to clipboardErrorCopied
    

    bitmap

    bitmap 存储的是连续的二进制数字(0 和 1),通过 bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身。我们知道 8 个 bit 可以组成一个 byte,所以 bitmap 本身会极大的节省储存空间

    常用命令:setbit 、 getbit 、 bitcount 、 bitop

    应用场景:适合需要保存状态信息(比如是否签到、是否登录...)并需要进一步对这些信息进行分析的场景。比如用户签到情况、活跃用户情况、用户行为统计

    # SETBIT 会返回之前位的值(默认是 0)这里会生成 7 个位
    127.0.0.1:6379> setbit mykey 7 1
    (integer) 0
    127.0.0.1:6379> setbit mykey 7 0
    (integer) 1
    127.0.0.1:6379> getbit mykey 7
    (integer) 0
    127.0.0.1:6379> setbit mykey 6 1
    (integer) 0
    127.0.0.1:6379> setbit mykey 8 1
    (integer) 0
    # 通过 bitcount 统计被被设置为 1 的位的数量。
    127.0.0.1:6379> bitcount mykey
    (integer) 2Copy to clipboardErrorCopied
    

    Redis 为什么不用多线程以及为什么 Redis6.0 之后又引入了多线程

    使用单线程的原因

  • 单线程编程更容易并且更容易维护
  • Redis 的性能瓶颈不在 CPU ,主要在内存和网络
  • 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能
  • Redis6.0 引入多线程主要是为了提高网络 IO 读写性能,因为这个是 Redis 中的一个性能瓶颈

    虽然 Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行

    设置缓存过期的作用

    因为内存是有限的,如果缓存中的所有数据都是一直保存的话,很快就会 Out of memory 了

    Redis 中除了字符串类型有自己独有设置过期时间的命令 setex 外,其他方法都需要依靠 expire 命令来设置过期时间 。另外 persist 命令可以移除一个键的过期时间

    还有一种情况需要设置过期时间,当我们的业务场景就是需要某个数据只在某一时间段内存在,比如我们的短信验证码可能只在 1 分钟内有效,用户登录的 token 可能只在 1 天内有效,此时设置过期时间就能比较好的处理,如果使用传统的数据库来处理的话,一般都是自己判断过期,这样更麻烦并且性能要差很多

    Redis 过期删除策略

  • 定时删除:在设置键的过期时间的同时,创建一个定时器 time,让定时器在键的过期时间来临时,立即执行对键的删除操作
  • 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键
  • 定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定
  • 更新策略对比

    面试题:什么是 Redis?可以做什么?与Memcached异同?常见数据结构与使用场景

    Redis 内存淘汰机制

    Redis 提供 6 种数据淘汰策略:

  • volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  • allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  • no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错
  • 4.0 版本后增加以下两种:

  • volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  • allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
  • 使用策略的规则:

    (1)如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用 allkeys-lru

    (2)如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用 allkeys-random

    Redis 持久化

    Redis 的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file, AOF)

    快照(snapshotting)持久化(RDB)

    Redis 可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis 创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis 主从结构,主要用来提高 Redis 性能),还可以将快照留在原地以便重启服务器的时候使用

    AOF(append-only file)持久化

    与快照持久化相比,AOF 持久化的实时性更好,因此已成为主流的持久化方案,是指所有的命令行记录以 Redis 命令请求协议的格式完全持久化存储)保存为 aof 文件

    默认没有开启 AOF,可以通过如下命令开启

    appendonly yesCopy to clipboardErrorCopied
    

    两种持久化方案对比:

    面试题:什么是 Redis?可以做什么?与Memcached异同?常见数据结构与使用场景

    Redis 常见的性能问题

  • Master 最好不要写内存快照,如果 Master 写内存快照,save 命令调度 rdbSave 函数,会阻塞主线程的工作,当快照比较大时对性能影响会非常大,会间断性暂停服务
  • 如果数据比较重要,需要在某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次
  • 为了主从复制的速度和连接的稳定性,Master 和 Slave 最好在同一个局域网
  • 尽量避免在压力很大的主库上增加从库
  • 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master
  • 相关文章

    服务器端口转发,带你了解服务器端口转发
    服务器开放端口,服务器开放端口的步骤
    产品推荐:7月受欢迎AI容器镜像来了,有Qwen系列大模型镜像
    如何使用 WinGet 下载 Microsoft Store 应用
    百度搜索:蓝易云 – 熟悉ubuntu apt-get命令详解
    百度搜索:蓝易云 – 域名解析成功但ping不通解决方案

    发布评论