欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Redis 基本认识(笔试、面试题)

程序员文章站 2022-03-28 12:46:00
一、Redis 1、简介 【官方简介地址:】 https://redis.io/topics/introduction 看不懂不要紧,先混个眼熟,慢慢来...。 【初步认识 Redis:】 Redis is an open source (BSD licensed), in-memory data ......

一、redis

1、简介

【官方简介地址:】
    https://redis.io/topics/introduction

  看不懂不要紧,先混个眼熟,慢慢来...。

【初步认识 redis:】
    redis is an open source (bsd licensed), in-memory data structure store, used as a database, cache and message broker.

【翻译:】
    redis 是一个开源的、基于内存的数据存储结构,可以作为数据库、缓存、消息中间件。
    
【重点:】
    基于内存、支持多种数据结构、常用于缓存。

 

2、为什么使用 redis 作为缓存?

(1)为什么要使用缓存?
  对于一个系统来说,若直接操作数据库,每次读写都经过磁盘操作,当并发量过高时,磁盘读写速度极大地影响系统的性能。使用缓存,即在访问磁盘前设置一个缓冲区,若缓冲区没有数据,再去数据库进行操作,这样可以极大地减少磁盘操作,从而提高系统性能。

(2)redis 是基于内存的、一个高性能的 key - value 数据库(非关系型数据库)。
  内存的处理速度比操作磁盘快,可以提高性能。
  缓存分担了部分请求,减少了数据库访问压力,提高了并发量。
  说起 key - value 库,容易想到 java 中的 map,map 实现的是本地缓存(即每台机器各自拥有自己的缓存),容量有限,随着 jvm 存在、消失。而 redis 实现的是分布式缓存(即多台机器可以共享一份缓存数据),其数据可以持久化到硬盘中,可以自定义缓存过期机制。

 

3、redis 的数据结构?使用场景?

(1)常用命令:

【参考地址:】
    http://doc.redisfans.com/
    https://www.cnblogs.com/l-y-h/p/12656614.html

(2)常用数据结构:
  redis 是由 c 语言编写的,其存储是以 key - value 的形式。key 为字符串,value 为 redis 的数据结构。常用数据结构为:string、list、set、hash、sortedset。
  底层实现原理,以后有空再去研究...
  不同数据结构,若采用不同的编码格式,底层会有不同的实现。

(3)常用数据结构使用场景(举例,可能不太恰当,大致理解一下):
  string 使用场景:
  比如:一些博客、文章的阅读量、点赞数等。

  可以根据 文章 id 生成一个键。当某用户阅读、点赞后,在相应的 value 上加 1。
  比如 :
    key 为 文章阅读量:文章id,
    value 为对应的 文章阅读量。
    可以通过 incr、decr 等进行加减阅读量。

【根据文章id 生成一个 key:(每个文章都有不同的 id,从而区分不同的 key)】
    set article:readcount:1001 0      文章 id 为 1001 的文章当前阅读量为 0
    set article:readcount:1002 0      文章 id 为 1002 的文章当前阅读量为 0

【阅读时,数量增 1:】
    incr article:readcount:1001       文章 id 为 1001 的文章阅读量加 1
    
【获取阅读量:】
    get article:readcount:1001        获取文章 id 为 1001 的文章阅读量

Redis 基本认识(笔试、面试题)

 

 

 

hash 使用场景:
  比如:电商网站的购物车。

  可以根据 用户id 生成一个 key,商品 id 为 field,商品数量为 field 对应的 value。
  可以使用 hgetall 获取所有的 field - value,即实现全选。
  可以使用 hincrby 对指定的 field 修改数量。
  可以使用 hlen 获取当前购物车商品的种类。等等操作。

比如:
  key 为 用户 id user:用户 id
  field 为 商品 id wares:商品 id
  value 为 商品数量 商品数量

注:

  其余信息可以通过 ajax 根据 用户 id 、商品 id 进行查询并返回显示。

【根据用户 id、商品 id、商品数量 生成一个 key,】
    hset user:10001 wares:3001 1     给 10001 用户 添加 一个 3001 商品。
    hset user:10001 wares:3002 2     给 10001 用户 添加 两个 3002 商品。
    
【全选操作:】
    hgetall user:10001             获取 10001 用户所有的 商品(field)以及数量(value)
    
【增加商品数量:】
    hincrby user:10001 wares:3002 3     给 10001 用户再增加 3 个 3002 商品

Redis 基本认识(笔试、面试题)

 

 

 

list 使用场景:
  比如:微信订阅号推送的消息。

  不同的公众号推送消息有先有后,最后是按照时间顺序进行排序显示(最近的时间显示在最上面)。
  可以使用 list 存储接收的消息 id。每接受一个 公众号消息 的 id,就 lpush 进 list 中,最后使用 lrange 去获取最新的推送消息。

【接收公众号推送消息的 id:】
    lpush msg:我的订阅号-id 安徽共青团:10001
    lpush msg:我的订阅号-id 唐唐频道:20001
    lpush msg:我的订阅号-id 全是黑科技:34811
    lpush msg:我的订阅号-id 程序人生:2233
    lpush msg:我的订阅号-id 共青团*:32345
    
【展示公众号 id:】
    lrange msg:我的订阅号-id 0 -1

Redis 基本认识(笔试、面试题)

 

 

 

set 使用场景:
  比如:抽奖小程序,获取朋友圈点赞的用户信息,可能关注的人(需要使用并集等操作)等。
  抽奖就是在一堆用户中随机抽取用户。由于 set 不可重复性,可以保证用户唯一。
  使用 sadd 可以添加用户 id 到 set 中。
  使用 smembers 可以查看当前参与抽奖的所有元素。
  使用 srandmember、spop 可以抽取获奖者用户。

【添加用户:】
    sadd user 1001 1002 1003 1004
    
【查看所有用户:】
    smembers user
    
【抽选用户,不删除用户:】
    srandmember user 3
    
【抽选用户,删除用户:】
    spop user 3

Redis 基本认识(笔试、面试题)

 

 

 

sortedset(zset)使用场景:
  比如:微博热搜榜、百度热议榜等。

Redis 基本认识(笔试、面试题)

 

 

 

二、redis 持久化、数据库、单线程

1、redis 数据库

  redis 默认有 16 个库,库编号为 db0 - db15。数据库之间的数据是相互隔离的、互不影响的。
  redis 是 c/s 结构,有一个 redis-cli 和 redis-server。 redis-server 用于启动 redis 服务,默认数据库数量为 16,可以修改。redis-cli 用于连接某个数据库。
  数据库中采用哈希表存储键值对,其中 value 可以为不同类型的数据结构。

2、redis 键过期处理

(1)为什么进行过期处理?
  redis 是基于内存的,内存容量比较有限,如果长期将 key - value 存放在 内存中,会占用大量内存,这样肯定是不行的,所以需要对 key 设置过期时间,当 key 过期后,系统响应并将其删除,从而减少内存的占用。

(2)过期策略:
  定时删除:到某个时间点,就进行删除 过期键 的操作,对 内存 友好,对 cpu 不友好。
  惰性删除:每次获取键时,判断该键是否过期,过期则删除,对 cpu 友好,对 内存 不友好。
  定期删除:每过一段时间,就去删除 过期键。
  redis 中采用 惰性删除 + 定期删除,即意味着 某个键 到了过期时间,也不一定会被立即删除。

(3)内存淘汰机制:
  由于 redis 可能会不及时的删除过期 key,导致 内存里堆积了很多没用的 key,会消耗大量内存。此时,需要通过内存淘汰机制,选择不需要的 key,并将其删除。
  比如:设置消耗内存最大值,当超过内存最大值后,进行数据淘汰,将最近最少使用的 key 数据淘汰(一般应用于热搜排行榜的场景)。

【常见内存淘汰机制:】
    allkeys-lru:      在所有 key 中,移除最近最少使用的 key(常用)
    allkeys-random:   在所有 key 中,随机移除 key。
    volatile-lru:     在设置过期时间的 key 中,移除最近最少使用的 key
    volatile-random:  在设置过期时间的 key 中,随机移除 key。
    volatile-ttl:     在设置过期时间的 key 中,优先移除 即将过期 的 key。

 

3、数据持久化 -- rdb

  redis 是基于内存的,redis 一旦重启,所有数据都会丢失,所以一般会将数据持久化到硬盘中,redis 重启后可以通过硬盘恢复数据。
  redis 采用两种方法进行数据持久化 -- rdb 、aof。
(1)rdb(redis database)
  rdb 基于快照,可以指定时间间隔、将某一时刻的所有数据保存到一个 rdb 文件中,是一个二进制文件,默认为 dump.rdb。redis 启动时,若发现存在 rdb 文件,则会自动载入该文件(载入的过程是一个阻塞的状态)。

(2)通过三种方式可以实现 rdb。
  method1:save 命令触发
    客户端执行 save 命令后,会阻塞当前 redis 服务器(即 redis 不能处理其他命令),直到 rdb 过程结束。若存在旧的 rdb 文件,会进行替换。(此方式若数据量过大,会影响系统性能)

  method2:bgsave 命令触发
    客户端执行 bgsave 命令后,会创建一个子进程,由子进程来创建 rdb 文件,不会阻塞当前 redis 服务器。

  method3:redis.conf 配置文件中配置

【save 格式:】
    save m n          指的是 m 时间间隔内,至少出现了 n 次 key 变化,则进行保存

【举例:】
    save 60 10000       指的是 60 秒内,至少出现了 10000 次 key 变化,则保存

 

(3)save 与 bgsave 比较:
  save 属于 同步操作,会阻塞当前 redis 服务器,但不会消耗额外内存。
  bgsave 属于 异步操作,不会阻塞当前 redis 服务器,但会消耗额外内存(创建子进程)。

(4)rdb 优缺点:
  优点:
    rdb 是全量备份,将数据压缩到二进制文件中,格式紧凑(文件小),适合数据备份以及恢复。
    rdb 可以使用子进程去创建 rdb 文件,主进程不进行 磁盘操作。
  缺点:
    子进程进行持久化时,父进程若修改内存中的数据,子进程不会知晓,此时可能造成数据丢失。

 

4、数据持久化 -- aof

(1)aof(append only file)
  aof 指当 redis 服务器执行写命令时,会将写命令 保存到 aof 文件中(可以理解为日志记录)。

(2)aof 执行流程:
  step1:命令追加到缓冲区
    遇到写命令时,将命令写入 aof_buf 缓冲区。

  step2:确认是否需要将缓冲区内容写入文件。
    通过配置文件 redis.conf 中 appendfsync 去确定是否将缓冲区内容写入文件。

    appendfsync always     # 每次有数据修改发生时都会写入aof文件(磁盘开销大)。
    appendfsync everysec   # 每秒钟同步一次,该策略为aof的默认策略(丢失 1 秒数据)。
    appendfsync no         # 从不同步。高效但是数据不会被持久化(数据丢失)。

 

  step3:文件从缓冲区写入到文件。
    将缓冲区的内容写入到 aof 文件中。

  不停的执行写命令操作后,会使得 aof 文件变得越来越大,可以使用 bgrewriteaof 命令进行 aof 重写(可以合并 写操作命令,减少文件内容冗余),此重写基于当前 数据库数据重写,不需要读取旧的 aof 文件。
  bgrewriteaof 命令会创建子进程,由子进程进行 aof 重写,其会存在一个 aof 重写缓冲区,重写缓冲区用于 记录 创建子进程后 主进程执行的 写操作。当子进程执行完 aof 重写后,向父进程发送请求,将重写缓冲区的数据写入新的 aof 文件中,从而使 当前数据库 与 aof 文件写操作一致。

(3)aof优缺点:
  优点:
    可以更好的保护数据,默认进行 1 秒同步一次的操作,最多丢失 1 秒数据。
  缺点:
    aof 文件过大,恢复数据速度较慢。

(4)aof、rdb 如何选择?
  aof、rdb 可以同时使用,但服务器优先使用 aof 文件进行数据还原。
  aof:丢失数据少(视 appendfsync 而定),文件体积大,恢复数据速度较慢。
  rdb:可能丢失一部分数据,文件体积小,恢复数据速度较快。

 

5、为什么 redis 是单线程?速度为什么快?

(1)为什么 redis 是单线程的?
  redis 基于内存进行操作,cpu 不是 redis 的瓶颈,且单线程 比 多线程容易实现。

(2)速度为什么快?
  基于内存操作,读写速度快。
  单线程操作,避免频繁上下文切换。
  采用了非阻塞 i/o 多路复用机制,保证系统高吞吐量。
注:
  非阻塞 i/o 多路复用机制,用来保证多个连接时的系统吞吐量(此处不展开,有时间再总结)。
  多路 指的是 多个 socket 连接。
  复用 指的是 共用 同一个线程。
  简单的讲,就是使单线程高效的处理多个连接请求。


6、redis 和 memcached 区别?

(1)redis 可以将数据持久化到硬盘中,memcached 只能将数据存储在内存中(断电后消失)。

(2)redis 支持多种数据类型,memcached 支持类型简单。

 

三、缓存雪崩、缓存穿透、缓存与数据库读写一致

1、缓存穿透是什么?如何解决?

(1)缓存穿透是什么?
  缓存穿透指查询一个不存在的数据,且数据不在缓存中,则查询会从数据库查询,而数据库查不到数据,则不会将数据存储在缓存中。以致于每次查询都会绕过缓存,从数据库查数据,使缓存失效。

(2)缓存穿透的可能原因?解决?
原因:
  请求的参数不合理。
  比如数据库的 id 自增,且从 100 开始,但是每次请求都是 100 以下的 id 或者 负数的 id,则每次查询,缓存中没有值,直接去查数据库,而数据库查不到值,就不会将数据保存到缓存中,从而使缓存失效。

解决:
  方式一:对参数进行过滤处理(比如 bloomfilter),不合法的参数不会访问到数据库。
  方式二:当数据库找不到数据时,返回一个空对象到缓存中,并设置一个过期时间,这样就可以从缓存中获取数据了。

2、缓存雪崩是什么?如何解决?

(1)缓存雪崩是什么?
  缓存雪崩指的是由于某种原因,导致缓冲层出现了问题,所有的请求(大量请求)直接访问数据库(可以理解为发生大量数据穿透),从而使数据库宕机。

(2)缓存雪崩的可能原因?解决?
原因一:
  redis 服务挂掉了,即缓存失效,所有请求不经过缓存直达数据库,数据库反应不过来而宕机。

如何解决:
  step1:应该尽量避免 redis 服务挂掉。
    为了实现 redis 高可用,应该使用 主从模式 + 哨兵模式(或者采用 redis 集群),尽量避免 redis 服务挂掉。
  step2:应该尽量避免 数据库 挂掉。
    万一 redis 服务真的挂了,应当进行 熔断、降低、限流等操作,尽量避免数据库被干掉,至少要保证服务还能正常运行。
  step3:数据恢复。
    对 redis 数据进行持久化,重启 redis 服务后,加载磁盘数据进行数据恢复。

原因二:
  redis 对数据设置了过期时间,同一时间这些数据失效,此时恰巧有大量请求同时访问这些数据,会穿过缓存直接访问数据库,造成大量缓存穿透,从而导致数据库宕机。

如何解决:
  缓存的同时,将过期时间设置成随机值,此时能极大避免大量数据 过期时间一致。

3、缓存、数据库读写一致

(1)读操作流程:
  step1:查询缓存中是否存在数据,存在数据则直接返回。
  step2:缓存中不存在数据,则查询数据库中是否存在数据,存在数据,则将数据保存在缓存中,并返回数据。

(2)读写操作同时进行时可能出现数据不一致。
  造成读写不一致的情况有很多。
  比如一件商品,开始时 数据库、缓存里显示的库存数量均为 1000。此时读操作并没有问题。现在卖出一件商品,需要更新数据库,假如更新数据库数据成功,但是更新缓存数据失败 ,即此时数据库显示库存数量为 999,而缓存显示数量为 1000,则下次操作,获取到的商品数量仍为 1000,此时就造成了读写不一致。

(3)如何解决读写不一致?
  方式一:一般给缓存的数据设置过期时间,数据过期则被删除,下次会从数据库查询并更新缓存。
  方式二:保证数据库、缓存更新的原子性(分布式事务)。要么同时成功、要么同时失败。

(4)更新缓存、数据库的两种方式:
  方式一:先更新缓存,再更新数据库。
  方式二:先更新数据库,再更新缓存。
注:
  对于更新缓存,一般直接删除某个数据,简单粗暴。下次读取时从数据库读取并保存到缓存中。

对于方式一(单线程情况):
  若删除缓存失败,可以直接抛出异常,此时数据库与缓存数据均无变化,即数据一致。
  若删除缓存成功,但是更新数据库失败,此时缓存中没有该数据,下次读取时,从数据库中读取并保存到缓存中,从而数据一致。
  若删除缓存、更新数据库均成功,下次读取数据肯定一致。

对于方式一(高并发情况):
  线程 a 进行更新操作,线程 b 进行读操作。
  线程 a 删除缓存,此时线程 b 进行读取,发现缓存不存在,则直接从数据库中读取,并将该值存入缓存。
  线程 a 对数据库数据进行更新,此时缓存中的值 与 数据库的值不一致了。
如何解决上述的数据不一致:
  将命令操作积压到队列中(先进先出),进行串行化,比如先删除缓存,再更新数据库,最后再进行读取。

对于方式二(单线程情况):
  若更新数据库失败,则直接抛出异常,此时数据库与缓存数据均无变化,即数据一致。
  若更新数据库成功,但删除缓存失败,则数据库的数据为新数据,与缓存数据不一致了。
  若更新数据库、删除缓存均成功,则下次读写的数据肯定一致。
如何解决上述的数据不一致:
  不断重复删除 key,直至可以删除。

对于方式二(高并发情况):
  线程 a 进行查询操作,线程 b 进行更新操作。
  线程 a 查询时,恰好缓存失效,直接通过数据库进行查询,此时 线程 b 更新数据库数据,并进行缓存删除,然后 线程 a 将从数据库获取的数据写入缓存中,此时缓存数据与数据库数据不一致了。
上例情况发生概率很低,毕竟写操作的速度慢于读操作,且读操作要先于写操作进入数据库,且慢于写操作操作缓存,同时满足这个情况的概率只能说是走了*运。