zoukankan      html  css  js  c++  java
  • Redis 基本认识(笔试、面试题)

    一、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 的文章阅读量

    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 商品

    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

      

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

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

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

    二、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 将从数据库获取的数据写入缓存中,此时缓存数据与数据库数据不一致了。
    上例情况发生概率很低,毕竟写操作的速度慢于读操作,且读操作要先于写操作进入数据库,且慢于写操作操作缓存,同时满足这个情况的概率只能说是走了狗屎运。

  • 相关阅读:
    Python之路(第十七篇)logging模块
    Python之路(第十五篇)sys模块、json模块、pickle模块、shelve模块
    Python之路(第十四篇)os模块
    Python之路(第十三篇)time模块、random模块、string模块、验证码练习
    Python之路(第十二篇)程序解耦、模块介绍导入安装、包
    Python编程笔记(第三篇)【补充】三元运算、文件处理、检测文件编码、递归、斐波那契数列、名称空间、作用域、生成器
    Python之路(第十一篇)装饰器
    Python之路(第十篇)迭代器协议、for循环机制、三元运算、列表解析式、生成器
    Python之路(第九篇)Python文件操作
    Python编程笔记(第二篇)二进制、字符编码、数据类型
  • 原文地址:https://www.cnblogs.com/huoyz/p/14378259.html
Copyright © 2011-2022 走看看