图解Redis 记录

小林coding + 尚硅谷
小林coding学习链接: https://www.xiaolincoding.com/

一、Redis基础

  1. NoSQL数据库简介
  • 1.1 技术发展
    web1.0 单点数据库 (数据访问有限) web2…0 CPU压力和内存压力 (数据量剧增)
    解决CPU和内存压力:文件服务器或者数据库服务器:大量的IO问题; session复制:节点浪费多; 缓存数据库:完全在内存,速度比较快
    解决IO压力: 分库分表 和 读写分离

  • 1.2 NoSQL数据库
    对数据高并发的读写,海量数据的读写,对数据高可扩展性的
    不遵循SQL标准。 不支持ACID。 远超于SQL的性能。

Memcache Redis MongoDB

  • 1.3 行式存储数据库

行式存储数据库(大数据时代)

行存储: 事务支持好
列存储:列分析性支持速度快,事务逻辑支持速度慢
Hbase
cassandra

  • 1.4 图关系数据库
  1. Redis概述
  • 2.1 概述

图解Redis 记录_第1张图片

  • 2.2 应用场景

高频次,热门访问的数据,降低数据库IO
分布式架构,做session共享

图解Redis 记录_第2张图片

  1. 常用的5中类型 及 新的数据类型

string list hash set zset | geo bitmap hyperloglog stream

  1. Redis配置文件介绍
  • 4.1 units include 相关

  • 4.2 网络配置相关
    bind port tcp-backlog(连接队列) timeout(多长时间会断开) tcp-keepalive(心跳包检测)

  • 4.3 通用
    damean 后台线程 pidfile loglevel(debug、verbose、notice、warning,默认为notice) logfile

  • 4.5 安全

  • 4.6 limits限制

  1. Redis的发布和订阅
    图解Redis 记录_第3张图片

  2. Redis 事务 锁 机制

  • 6.1 Redis事务的定义

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
Redis事务的主要作用就是串联多个命令防止别的命令插队。

  • 6.2 Multi、Exec、discard
    从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。
    组队的过程中可以通过discard来放弃组队。

  • 6.3 事务的错误处理
    组队中某个命令出现了报告错误,执行时整个的所有队列都会被取消。
    如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。

  • 6.4 为什么要做成事务
    有很多人有你的账户,同时去参加双十一抢购

  • 6.5 事务冲突的问题

悲观锁:
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

乐观锁:
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

WATCH :
在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。

unwatch:
取消 WATCH 命令对所有 key 的监视。
如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

  • 6.6 Redis事务三特性
  • 单独的隔离操作
    事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • 没有隔离级别的概念
    队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
  • 不保证原子性
    事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
  1. 持久化RDB
  • 7.1 是什么
    在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里

  • 7.2 备份是如何执行的
    Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

  • 7.3 fork
    Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
    在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”
    一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

  • 7.4 RDB持久化流程
    图解Redis 记录_第4张图片

rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下 dir “/myredis/”

  • 7.5 触发RDB快照的时机
    save 600 1
    命令save save时只管保存,其它不管,全部阻塞。手动保存。不建议。
    bgsave Redis会在后台异步进行快照操作, 快照同时还可以响应客户端请求。
    flushall 执行flushall命令,也会产生dump.rdb文件,但里面是空的,无意义

  • 7.6 优势
     适合大规模的数据恢复
     对数据完整性和一致性要求不高更适合使用
     节省磁盘空间
     恢复速度快

  • 7.7 劣势
     Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
     虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
     在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。

图解Redis 记录_第5张图片

  1. 持久化AOF
  • 8.1 是什么
    以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

  • 8.2 AOF持久化流程
    (1)客户端的请求写命令会被append追加到AOF缓冲区内;
    (2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
    (3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
    (4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

图解Redis 记录_第6张图片

  • 8.3 操作相关
      1. AOF默认不开启
        可以在redis.conf中配置文件名称,默认为 appendonly.aof AOF文件的保存路径,同RDB的路径一致。
      1. AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)
      1. AOF启动/修复/恢复
        AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,
        需要恢复时再拷贝到Redis工作目录下,启动系统即加载。

      正常恢复
       修改默认的appendonly no,改为yes
       将有数据的aof文件复制一份保存到对应目录(查看目录:config get dir)
       恢复:重启redis然后重新加载

      异常恢复
       修改默认的appendonly no,改为yes
       如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof–fix appendonly.aof进行恢复
       备份被写坏的AOF文件
       恢复:重启redis,然后重新加载

      1. AOF同步频率设置
        appendfsync always
        始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好
        appendfsync everysec
        每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。
        appendfsync no
        redis不主动进行同步,把同步时机交给操作系统。
        1. Rewrite压缩
          1 是什么:
          AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制, 当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集.可以使用命令bgrewriteaof

      2 重写原理,如何实现重写
      AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename),redis4.0版本后的重写,是指上就是把rdb 的快照,以二级制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
      no-appendfsync-on-rewrite:
      如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
      如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)

      • 触发机制,何时重写
        Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
        重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
        auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
        auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
        例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB
        系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,
        如果Redis的AOF当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

3、重写流程
(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有,则等待该命令结束后再继续执行。
(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。
(3)子进程遍历redis内存中数据到临时文件,客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(4)1).子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。2).主进程把aof_rewrite_buf中的数据写入到新的AOF文件。
(5)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。

  1. 缓存应用问题

    主要看16章的内容 缓存应用

二、Redis面试高频题目

Redis持久化

AOF 日志是如何实现的?

为什么先执行命令,再把数据写入日志呢?

AOF 写回策略有几种?

Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

AOF 日志过大,会触发什么机制?

AOF 重写机制
在使用重写机制后,就会读取 name 最新的 value(键值对) ,然后用一条 「set name xiaolincoding」命令记录到新的 AOF 文件,,之前的第一个命令就没有必要记录了,因为它属于「历史」命令,没有作用了。这样一来,一个键值对在重写日志中只用一条命令就行了。

重写 AOF 日志的过程是怎样的?

重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的
但是重写过程中,主进程依然可以正常处理命令,
Redis 设置了一个 AOF 重写缓冲区
在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。

RDB 快照是如何实现的呢?

RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。

RDB 做快照时会阻塞线程吗?

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」

执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;

RDB 在执行快照的时候,数据能修改吗?

写时复制技术(Copy-On-Write, COW)

执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。
如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。

为什么会有混合持久化?

混合使用 AOF 日志和内存快照
混合持久化工作在 AOF 日志重写过程,当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。

Redis集群

Redis 如何实现服务高可用?

主从复制

主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。

主从服务器之间的命令复制是异步进行的。
无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。

哨兵模式

Redis 增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。

切片集群模式

一个切片集群共有 16384 个哈希槽
根据键值对的 key,按照 CRC16 算法 (opens new window)计算一个 16 bit 的值。
再用 16bit 值对 16384 取模,得到 0~16383 范围内的模数,每个模数代表一个相应编号的哈希槽。

接下来的问题就是,这些哈希槽怎么被映射到具体的 Redis 节点上的呢?有两种方案:
平均分配: 在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个。
手动分配: 可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。

集群脑裂导致数据丢失怎么办?

什么是脑裂?

主节点网络问题与从节点失联,哨兵选举了新的主节点,客户端还向旧的主节点发送数据,两个主节点

然后,网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。

总结一句话就是:由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。

解决方案

当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。

Redis 过期删除与内存淘汰

Redis 使用的过期删除策略是什么?

Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中,也就是说「过期字典」保存了数据库中所有 key 的过期时间。

什么是惰性删除策略?

惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。

优缺点 CPU 内存

什么是定期删除策略?

定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。

定期删除是一个循环的流程。那 Redis 为了保证定期删除不会出现循环过度,导致线程卡死现象,为此增加了定期删除循环流程的时间上限,默认不会超过 25ms。

** Redis 选择「惰性删除+定期删除」这两种策略配和使用,**

Redis缓存设计

如何避免缓存雪崩、缓存击穿、缓存穿透?

图解Redis 记录_第7张图片
对于缓存雪崩问题,我们可以采用两种方案解决。
将缓存失效时间随机打散:
设置缓存不过期:

对缓存击穿可以采取前面说到两种方案:
互斥锁方案(Redis 中使用 setNX 方法设置一个状态位,表示这是一种锁定状态)
不给热点数据设置过期时间,由后台异步更新缓存,

应对缓存穿透的方案,常见的方案有三种。
非法请求的限制:
设置空值或者默认值:
使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在:

如何设计一个缓存策略,可以动态缓存热点数据呢?

通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作

说说常见的缓存更新策略?

Cache Aside(旁路缓存)策略;
Read/Write Through(读穿 / 写穿)策略;
Write Back(写回)策略;

Cache Aside(旁路缓存)策略

写策略的步骤:
先更新数据库中的数据,再删除缓存中的数据。

读策略的步骤:
如果读取的数据命中了缓存,则直接返回数据;
如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

用「读 + 写」请求的并发的场景来分析。

先更新数据库,再删除缓存也是会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高。
写策略的步骤的顺序顺序不能倒过来,即不能先删除缓存再更新数据库,原因是在「读+写」并发的时候,会出现缓存和数据库的数据不一致性的问题。
因为缓存的写入通常要远远快于数据库的写入,

Cache Aside 策略适合读多写少的场景,不适合写多的场景,

Read/Write Through(读穿 / 写穿)策略

缓存组件做代理

Write Back(写回)策略

写缓存脏页,然后系统进行写盘

如何保证缓存和数据库数据的一致性?

Redis实战

Redis的大 key 如何处理?

分步骤删除
异步删除 交给后台线程 unlink

Redis 管道有什么用?

多个事务,管道里面,客户端实现

Redis 事务支持回滚吗?

如何用 Redis 实现分布式锁的?

实现

基于 Redis 实现分布式锁有什么优缺点?

Redis 如何解决集群情况下分布式锁的可靠性?

为了保证集群环境下分布式锁的可靠性,Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)。它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。

是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。

三、数据类型

【Redis数据类型和应用场景】

介绍 - 内部实现 - 常用指令 - 应用场景

五个: string list hash set zset
四个:bitmap htperloglog geo stream

总结:
图解Redis 记录_第8张图片

图解Redis 记录_第9张图片

1. String

  • 介绍
    String 是最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。

  • 实现
    String 类型的底层的数据结构实现主要是 int 和 SDS(简单动态字符串)。

图解Redis 记录_第10张图片

int编码
图解Redis 记录_第11张图片

<32字节 embstr编码
在这里插入图片描述
大于32字节 raw编码
图解Redis 记录_第12张图片

  • 指令

  • 应用场景
    -1. 缓存对象
    直接缓存整个对象的 JSON

    1. 常规计数
      因此 String 数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
    1. 分布式锁

图解Redis 记录_第13张图片

    1. 共享session信息
      通常我们在开发后台管理系统时,会使用 Session 来保存用户的会话(登录)状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。
      我们需要借助 Redis 对这些 Session 信息进行统一的存储和管理,这样无论请求发送到那台服务器,服务器都会去同一个 Redis 获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。

2. List

  • 介绍
    List 列表是简单的字符串列表,按照插入顺序排序,可以从头部或尾部向 List 列表添加元素。
    列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。

  • 实现
    List 类型的底层数据结构是由双向链表或压缩列表实现的:
    但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。

  • 可以作为消息队列
    消息队列在存取消息时,必须要满足三个需求,分别是:
    消息保序、处理重复的消息和保证消息可靠性。

  1. 消息有序性的保证
    劣势:
    即使没有新消息写入List,消费者也要不停地调用 RPOP 命令,这就会导致消费者程序的 CPU 一直消耗在执行 RPOP 命令上,带来不必要的性能损失。
    改进:
    Redis提供了 BRPOP 命令。BRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。和消费者程序自己不停地调用RPOP命令相比,这种方式能节省CPU开销。

  2. 处理重复的消息
    消费者要实现重复消息的判断,需要 2 个方面的要求:

每个消息都有一个全局的 ID。
消费者要记录已经处理过的消息的 ID。当收到一条消息后,消费者程序就可以对比收到的消息 ID 和记录的已处理过的消息 ID,来判断当前收到的消息有没有经过处理。如果已经处理过,那么,消费者程序就不再进行处理了。

我们执行以下命令,就把一条全局 ID 为 111000102、库存量为 99 的消息插入了消息队列:
LPUSH mq “111000102:stock:99”

3、如何保证消息可靠性?

List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。

小结:
消息保序:使用 LPUSH + RPOP;
阻塞读取:使用 BRPOP;
重复消息处理:生产者自行实现全局唯一 ID;
消息的可靠性:使用 BRPOPLPUSH

List 作为消息队列有什么缺陷?

List 不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。

要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 List 类型并不支持消费组的实现

这就要说起 Redis 从 5.0 版本开始提供的 Stream 数据类型了,Stream 同样能够满足消息队列的三大需求,而且它还支持「消费组」形式的消息读取。

3. Hash

  • 介绍
    Hash 是一个键值对(key - value)集合,其中 value 的形式如: value=[{field1,value1},…{fieldN,valueN}]。Hash 特别适合用于存储对象。

string和hash区别
图解Redis 记录_第14张图片

  • 内部实现
    Hash 类型的底层数据结构是由压缩列表或哈希表实现的:
  • 如果哈希类型元素个数小于 512 个,所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了

  • 应用场景
  • 缓存对象
# 存储一个哈希表uid:1的键值
> HSET uid:1 name Tom age 15
2
# 存储一个哈希表uid:2的键值
> HSET uid:2 name Jerry age 13
2
# 获取哈希表用户id为1中所有的键值
> HGETALL uid:1
1) "name"
2) "Tom"
3) "age"
4) "15"
  • 购物车
添加商品:HSET cart:{用户id} {商品id} 1
添加数量:HINCRBY cart:{用户id} {商品id} 1
商品总数:HLEN cart:{用户id}
删除商品:HDEL cart:{用户id} {商品id}
获取购物车所有商品:HGETALL cart:{用户id}

4. Set

  • 介绍
    Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。
    一个集合最多可以存储 2^32-1 个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等
    所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。

Set 类型和 List 类型的区别如下:
List 可以存储重复元素,Set 只能存储非重复元素;
List 是按照元素的先后顺序存储元素的,而 Set 则是无序方式存储元素的。

  • 内部实现
    Set 类型的底层数据结构是由哈希表或整数集合实现的:
    如果集合中的元素都是整数且元素个数小于 512 个,
    Redis 会使用整数集合作为底层数据结构,
    否则使用哈希表作为 底层数据结构。

  • 常用命令

  • 应用场景
    Set 类型比较适合用来数据去重和保障数据的唯一性,还可以用来统计多个集合的交集、错集和并集等,当我们存储的数据是无序并且需要去重的情况下,比较适合使用集合类型进行存储。

有一个潜在的风险。Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。

在主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,我们可以选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计。

  • 点赞

  • 共同关注
    uid:1 和 uid:2 共同关注的公众号: SINTER uid:1 uid:2
    给 uid:2 推荐 uid:1 关注的公众号: SDIFF uid:1 uid:2
    验证某个公众号是否同时被关注:SISMEMBER uid:1 5 SISMEMBER uid:2 5

  • 抽奖活动
    如果允许重复中奖,可以使用 SRANDMEMBER 命令,如果不允许重复中奖,可以使用 SPOP 命令。

5. Zset

  • 介绍:
    Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 score(分值),对于有序集合 ZSet 来说,每个存储元素相当于有两个值组成的,一个是有序结合的元素值,一个是排序值。
    有序集合保留了集合不能有重复成员的特性(分值可以重复),但不同的是,有序集合中的元素可以排序。

  • 内部实现:
    Zset 类型的底层数据结构是由压缩列表或跳表实现的:
    如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
    如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;
    在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

  • 常用命令
    正序获取有序集合key从start下标到stop下标的元素
    ZRANGE key start stop [WITHSCORES]

倒序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES]

  • 应用场景
  • 排行榜
arcticle:5 文章获得了150个赞
> ZADD user:xiaolin:ranking 150 arcticle:5
(integer) 1

> ZINCRBY user:xiaolin:ranking 1 arcticle:4
"51"

 WITHSCORES 表示把 score 也显示出来
> ZREVRANGE user:xiaolin:ranking 0 2 WITHSCORES
1) "arcticle:1"
2) "200"
3) "arcticle:5"
4) "150"
5) "arcticle:3"
6) "100"
  • 电话、姓名排序
    返回指定成员区间内的成员,按字典正序排列, 分数必须相同。
    ZRANGEBYLEX key min max [LIMIT offset count]
    返回指定成员区间内的成员,按字典倒序排列, 分数必须相同
    ZREVRANGEBYLEX key max min [LIMIT offset count]

不要在分数不一致的 SortSet 集合中去使用 ZRANGEBYLEX和 ZREVRANGEBYLEX 指令,因为获取的结果会不准确。

6. BitMap

由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。

Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。
String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

签到统计、判断用户登陆态、连续签到用户总数(日期为id,统计一段事件)

7. Hyperloglog

是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。 HyperLogLog 提供不精确的去重计数。

HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的内存空间总是固定的、并且是很小的。
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,和元素越多就越耗费内存的 Set 和 Hash 类型相比,HyperLogLog 就非常节省空间。

PFADD key element [element …]
PFCOUNT key [key …]
PFMERGE destkey sourcekey [sourcekey …]

百万级网页 UV 计数: 非常适合统计百万级以上的网页 UV 的场景

8. GEO

Redis GEO 是 Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。

直接使用了 Sorted Set 集合类型,
GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。

滴滴叫车

9. Stream

Redis Stream 是 Redis 专门为消息队列设计的数据类型。
在 Redis 5.0 Stream 没出来之前,消息队列的实现方式都有着各自的缺陷,例如:
发布订阅模式,不能持久化也就无法可靠的保存消息,并且对于离线重连的客户端不能读取历史消息的缺陷;
List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID。

基于以上问题,Redis 5.0 便推出了 Stream 类型也是此版本最重要的功能,用于完美地实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ack 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。
图解Redis 记录_第15张图片

【Redis数据结构】

图解Redis 记录_第16张图片

键值对数据库如何实现

Redis 的键值对中的 key 就是字符串对象,而 value 可以是字符串对象,也可以是集合数据类型的对象,

图解Redis 记录_第17张图片

  • redisDb 结构,表示 Redis 数据库的结构,结构体里存放了指向了 dict 结构的指针;
  • dict 结构,结构体里存放了 2 个哈希表,正常情况下都是用「哈希表1」,「哈希表2」只有在 rehash 的时候才用,具体什么是 rehash,我在本文的哈希表数据结构会讲;
  • ditctht 结构,表示哈希表的结构,结构里存放了哈希表数组,数组中的每个元素都是指向一个哈希表节点结构(dictEntry)的指针;
  • dictEntry 结构,表示哈希表节点的结构,结构里存放了 **void * key 和 void * value 指针, key 指向的是 String 对象,而 value 则可以指向 String 对象,也可以指向集合类型的对象,比如 List 对象、Hash 对象、Set 对象和 Zset 对象。

void * key 和 void * value 指针指向的是 Redis 对象,Redis 中的每个对象都由 redisObject 结构表示

对象结构里包含的成员变量:
type,标识该对象是什么类型的对象(String 对象、 List 对象、Hash 对象、Set 对象和 Zset 对象);
encoding,标识该对象使用了哪种底层的数据结构;
ptr,指向底层数据结构的指针。

SDS

C的字符串不足:

  • 获取字符串长度的时间复杂度为 O(N);
  • 字符串的结尾是以 “\0” 字符标识,字符串里面不能包含有 “\0” 字符,因此不能保存二进制数据;
  • 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止;
  1. Redis的SDS:

图解Redis 记录_第18张图片
2. 优点:
1. O(1)复杂度获取字符串长度
2. 二进制安全
3. 不会发生缓冲区溢出 可以通过alloc-len判断缓冲区是否会溢出,不够用可以自动扩容
4. 节省内存空间
设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。 uint16 uint32 更节省空间
之所以 SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。
除了设计不同类型的结构体,Redis 在编程上还使用了专门的编译优化来节省内存空间,即在 struct 声明了 attribute ((packed)) ,它的作用是:告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐。

链表

  1. 结构设计
    Redis 的 List 对象的底层实现之一就是链表。
    图解Redis 记录_第19张图片

图解Redis 记录_第20张图片

  1. 优点 缺点
    优点:
    获取头节点 尾节点都是O1复杂度

listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值;

缺点:
链表每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存。
还有一点,保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大。

压缩列表

压缩列表是 Redis 为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组。

  1. 压缩列表结构设计
    图解Redis 记录_第21张图片

zlbytes,记录整个压缩列表占用对内存字节数;
zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
zllen,记录压缩列表包含的节点数量;
zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。

压缩列表节点包含三部分内容:
prevlen,记录了「前一个节点」的长度;
encoding,记录了当前节点实际数据的类型以及长度;
data,记录了当前节点的实际数据;

  • 使用不同空间大小的 prevlen 和 encoding 这两个元素里保存的信息,这种根据数据大小和类型进行不同的空间大小分配的设计思想,正是 Redis 为了节省内存而采用的。

在压缩列表中,如果我们要查找定位第一个元素和最后一个元素,可以通过表头三个字段的长度直接定位,复杂度是 O(1)。而查找其他元素时,就没有这么高效了,只能逐个查找,此时的复杂度就是 O(N) 了,因此压缩列表不适合保存过多的元素。

  1. 连锁更新

压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。

  1. 压缩列表的缺陷
    核心缺陷:
    连锁更新一旦发生,就会导致压缩列表占用的内存空间要多次重新分配,这就会直接影响到压缩列表的访问性能。
    压缩列表只会用于保存的节点数量不多的场景,

哈希表

Redis 的 Hash 对象的底层实现之一是压缩列表(最新 Redis 代码已将压缩列表替换成 listpack)。Hash 对象的另外一个底层实现就是哈希表。
在哈希表大小固定的情况下,随着数据不断增多,那么哈希冲突的可能性也会越高。
Redis 采用了「链式哈希」来解决哈希冲突,

  1. 哈希表结构设计
    图解Redis 记录_第22张图片

图解Redis 记录_第23张图片

  1. 哈希冲突
    当有两个以上数量的 kay 被分配到了哈希表中同一个哈希桶上时,此时称这些 key 发生了冲突。

  2. 链式哈希
    每个哈希表节点都有一个 next 指针,用于指向下一个哈希表节点,因此多个哈希表节点可以用 next 指针构成一个单项链表,被分配到同一个哈希桶上的多个节点可以用这个单项链表连接起来,这样就解决了哈希冲突。

  3. rehash

图解Redis 记录_第24张图片
在正常服务请求阶段,插入的数据,都会写入到「哈希表 1」,此时的「哈希表 2 」 并没有被分配空间。
随着数据逐步增多,触发了 rehash 操作,这个过程分为三步:

  • 给「哈希表 2」 分配空间,一般会比「哈希表 1」 大 2 倍;
  • 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
  • 迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。
    缺点:
    如果「哈希表 1 」的数据量非常大,那么在迁移至「哈希表 2 」的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求。
  1. 渐进式 rehash
    将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。
  • 给「哈希表 2」 分配空间;
  • 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上;
  • 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。
  1. rehash 触发条件

负载因子:
在这里插入图片描述

  • 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
  • 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。

整数集合

整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不大时,就会使用整数集这个数据结构作为底层实现。

  1. 整数集合结构设计
    图解Redis 记录_第25张图片
    可以看到,保存元素的容器是一个 contents 数组,虽然 contents 被声明为 int8_t 类型的数组,但是实际上 contents 数组并不保存任何 int8_t 类型的元素,contents 数组的真正类型取决于 intset 结构体里的 encoding 属性的值。
    不同类型的 contents 数组,意味着数组的大小也会不同。

  2. 整数集合的升级操作
    整数集合会有一个升级规则,就是当我们将一个新元素加入到整数集合里面,如果新元素的类型(int32_t)比整数集合现有所有元素的类型(int16_t)都要长时,整数集合需要先进行升级,也就是按新元素的类型(int32_t)扩展 contents 数组的空间大小,然后才能将新元素加入到整数集合里,当然升级的过程中,也要维持整数集合的有序性。

升级的好处:整数集合升级的好处是节省内存资源。
不支持降级操作,一旦对数组进行了升级,就会一直保持升级后的状态。

跳表

Redis 只有 Zset 对象的底层实现用到了跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。
zset 结构体里有两个数据结构:一个是跳表,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。

typedef struct zset {
    dict *dict;
    zskiplist *zsl;
} zset;

由「哈希表+跳表」组成 zset 结构体, zset 结构体中的哈希表只是用于以常数复杂度获取元素权重,大部分操作都是跳表实现的。

  1. 跳表结构设计

跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表,

图解Redis 记录_第26张图片
图解Redis 记录_第27张图片

图解Redis 记录_第28张图片

跳表结构里包含了:
跳表的头尾节点,便于在O(1)时间复杂度内访问跳表的头节点和尾节点;
跳表的长度,便于在O(1)时间复杂度获取跳表节点的数量;
跳表的最大层数,便于在O(1)时间复杂度获取跳表中层高最大的那个节点的层数量

  1. 查询过程
    如果当前节点的权重「小于」要查找的权重时,跳表就会访问该层上的下一个节点。
    如果当前节点的权重「等于」要查找的权重时,并且当前节点的 SDS 类型数据「小于」要查找的数据时,跳表就会访问该层上的下一个节点。

  2. 节点层数设置过程
    跳表的相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。

那怎样才能维持相邻两层的节点数量的比例为 2 : 1 呢?
Redis 则采用一种巧妙的方法是,跳表在创建节点的时候,随机生成每个节点的层数,并没有严格维持相邻两层的节点数量比例为 2 : 1 的情况。

具体的做法是,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。

quiklist

quicklist 就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表。

压缩列表会有「连锁更新」的风险,因此采用:
quiklist通过控制每个链表节点中的压缩列表的大小或者元素个数,来规避连锁更新的问题。因为压缩列表元素越少或越小,连锁更新带来的影响就越小,从而提供了更好的访问性能。

图解Redis 记录_第29张图片

listpack

quicklist 虽然通过控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响,但是并没有完全解决连锁更新的问题。
Redis 在 5.0 新设计一个数据结构叫 listpack,目的是替代压缩列表,它最大特点是 listpack 中每个节点不再包含前一个节点的长度了,压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患。

listpack 结构:图解Redis 记录_第30张图片
listpack 头包含两个属性,分别记录了 listpack 总字节数和元素数量,然后 listpack 末尾也有个结尾标识。
图中的 listpack entry 就是 listpack 的节点了。
主要包含三个方面内容:
encoding,定义该元素的编码类型,会对不同长度的整数和字符串进行编码;
data,实际存放的数据;
len,encoding+data的总长度;

listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。

四、持久化

4.1 AOF持久化

提纲

图解Redis 记录_第31张图片

1. AOF日志

Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,然后重启 Redis 的时候,先去读取这个文件里的命令,
注意只会记录写操作命令,读操作命令是不会被记录的
在 Redis 中 AOF 持久化功能默认是不开启的,需要我们修改 redis.conf

图解Redis 记录_第32张图片
「*3」表示当前命令有三个部分,每部分都是以「$+数字」开头,后面紧跟着具体的命令、键或值。
「$3 set」表示这部分有 3 个字节,也就是「set」命令这个字符串的长度。

Redis 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,好处:
第一个好处,避免额外的检查开销。
第二个好处,不会阻塞当前写操作命令的执行,

风险:
风险1,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。
风险2,由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前写操作命令的执行,但是可能会给「下一个」命令带来阻塞风险。因为将命令写入到日志的这个操作也是在主进程完成的(执行命令也是在主进程),也就是说这两个操作是同步的。
两个风险都有一个共性,都跟「 AOF 日志写回硬盘的时机」有关

2. 三种写回策略

图解Redis 记录_第33张图片

Redis 写入 AOF 日志的过程:

  1. Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;
  2. 然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘;
  3. 具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。

Redis 提供了 3 种写回硬盘的策略,控制的就是上面说的第三步的过程:
图解Redis 记录_第34张图片

底层通过调用 fsync() 函数,决定哪个写,功能是刷缓存

3. AOF重写机制

AOF 重写机制是在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。

重写机制的妙处在于,尽管某个键值对被多条写命令反复修改,最终也只需要根据这个「键值对」当前的最新状态,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这样就减少了 AOF 文件中的命令数量。

4. AOF后台重写

过程其实是很耗时的,所以重写的操作不能放在主进程里。,Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的

图解Redis 记录_第35张图片
在 bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作:
执行客户端发来的命令;
将执行后的写命令追加到 「AOF 缓冲区」;
将执行后的写命令追加到 「AOF 重写缓冲区」;

当子进程完成 AOF 重写工作(扫描数据库中所有数据,逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志)后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。

主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:
将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

注意要点:
有两个阶段会导致阻塞父进程:

  • 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
  • 创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;

主进程修改了已经存在 key-value,就会发生写时复制,注意这里只会复制主进程修改的物理内存数据,没修改物理内存还是与子进程共享的。

4.2 RDB快照

1. 快照使用

Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。

在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少。

    1. save和bgsave
      执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
      执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;

使用方法
Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令:
save 900 1 900 秒之内,对数据库进行了至少 1 次修改;

2. 执行快照时,数据是否可以被修改

执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的。
关键的技术就在于写时复制技术(Copy-On-Write, COW)。

如果主线程(父进程)要修改共享数据里的某一块数据时,就会发生写时复制,于是这块数据的物理内存就会被复制一份,然后主线程在这个数据副本进行修改操作。
与此同时,bgsave 子进程可以继续把原来的数据写入到 RDB 文件。

极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍。

快照:是否一致
Redis 在使用 bgsave 快照过程中,如果主线程修改了内存数据,不管是否是共享的内存数据,RDB 快照都无法写入主线程刚修改的数据,因为此时主线程(父进程)的内存数据和子进程的内存数据已经分离了,子进程写入到 RDB 文件的内存数据只能是原本的内存数据。

3. RDB与AOF混合持久化

将 RDB 和 AOF 合体使用,也叫混合持久化。
aof-use-rdb-preamble yes

混合持久化工作在 AOF 日志重写过程。

当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。
加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。

五、功能篇

  1. 过期删除策略

Redis 使用的过期删除策略是「惰性删除+定期删除」,删除的对象是已过期的 key。

在这里插入图片描述
2. 内存淘汰策略

内存淘汰策略是解决内存过大的问题,当 Redis 的运行内存超过最大运行内存时,就会触发内存淘汰策略,Redis 4.0 之后共实现了 8 种内存淘汰策略,我也对这 8 种的策略进行分类,如下
图解Redis 记录_第36张图片

六、高可用

多台服务器要保存同一份数据,这些服务器之间的数据如何保持一致性呢?
数据的读写操作是否每台服务器都可以处理?

Redis 提供了主从复制模式,来避免上述的问题。

读写分离模式

主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令。

所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,保证主从服务器的数据是一致

主从复制机制比较麻烦;

6.1 主从复制

1. 第一次同步-全量复制

replicaof <服务器 A 的 IP 地址> <服务器 A 的 Redis 端口号>

第一阶段是建立链接、协商同步;
第二阶段是主服务器同步数据给从服务器;
第三阶段是主服务器发送新写操作命令给从服务器。

图解Redis 记录_第37张图片

第一阶段:建立链接、协商同步
psync 命令包含两个参数,分别是主服务器的 runID 和复制进度 offset。

  • runID,每个 Redis 服务器在启动时都会自动生产一个随机的 ID 来唯一标识自己。当从服务器和主服务器第一次同步时,因为不知道主服务器的 run ID,所以将其设置为 “?”。
  • offset,表示复制的进度,第一次同步时,其值为 -1。

主服务器收到 psync 命令后,会用 FULLRESYNC 作为响应命令返回给对方。告知全量复制的方式
并且这个响应命令会带上两个参数:主服务器的 runID 和主服务器目前的复制进度 offset。从服务器收到响应后,会记录这两个值。

第二阶段:主服务器同步数据给从服务器
主服务器会执行 bgsave 命令来生成 RDB 文件,然后把文件发送给从服务器。
主服务器生成 RDB 这个过程是不会阻塞主线程的,因为 bgsave 命令是产生了一个子进程来做生成 RDB 文件的工作,是异步工作的,这样 Redis 依然可以正常处理命令。

这期间的写操作命令并没有记录到刚刚生成的 RDB 文件中,这时主从服务器间的数据就不一致了。那么为了保证主从服务器的数据一致性,

  • 主服务器在下面这三个时间间隙中将收到的写操作命令,写入到 replication buffer 缓冲区里。
  • 主服务器生成 RDB 文件期间;
  • 主服务器发送 RDB 文件给从服务器期间;
  • 「从服务器」加载 RDB 文件期间;

第三阶段:主服务器发送新写操作命令给从服务器
在主服务器生成的 RDB 文件发送完,从服务器加载完 RDB 文件后,然后将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,然后「从服务器」重新执行这些操作,至此主从服务器的数据就一致了。
至此,主从服务器的第一次同步的工作就完成了

2. 命令传播

主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接。

后续主服务器可以通过这个连接继续将写操作命令传播给从服务器,然后从服务器执行该命令,使得与主服务器的数据库状态相同。

而且这个连接是长连接的,目的是避免频繁的 TCP 连接和断开带来的性能开销。

3. 分摊服务器压力

主从服务器在第一次数据同步的过程中,主服务器会做两件耗时的操作:生成 RDB 文件和传输 RDB 文件。
如果从服务器过多,会发生下面情况:

  • 由于是通过 bgsave 命令来生成 RDB 文件的,那么主服务器就会忙于使用 fork() 创建子进程,如果主服务器的内存数据非大,在执行 fork() 函数时是会阻塞主线程的,从而使得 Redis 无法正常处理请求;
  • 传输 RDB 文件会占用主服务器的网络带宽,会对主服务器响应命令请求产生影响。

解决办法:找一个从服务器充当其他从服务器的主服务器
主服务器生成 RDB 和传输 RDB 的压力可以分摊到充当经理角色的从服务器。

4. 增量复制

网络断开又恢复后,从主从服务器会采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。

主要有三个步骤:
从服务器在恢复网络后,会发送 psync 命令给主服务器,此时的 psync 命令里的 offset 参数不是 -1;
主服务器收到该命令后,然后用 CONTINUE 响应命令告诉从服务器接下来采用增量复制的方式同步数据;
然后主服务将主从服务器断线期间,所执行的写命令发送给从服务器,然后从服务器执行这些命令。

  • 主服务器怎么知道要将哪些增量数据发送给从服务器呢?
    repl_backlog_buffer,是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据;
    replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。

5. 总结

主从复制共有三种模式:全量复制、基于长连接的命令传播、增量复制。

主从服务器第一次同步的时候,就是采用全量复制,此时主服务器会两个耗时的地方,分别是生成 RDB 文件和传输 RDB 文件。为了避免过多的从服务器和主服务器进行全量复制,可以把一部分从服务器升级为「经理角色」,让它也有自己的从服务器,通过这样可以分摊主服务器的压力。

第一次同步完成后,主从服务器都会维护着一个长连接,主服务器在接收到写操作命令后,就会通过这个连接将写命令传播给从服务器,来保证主从服务器的数据一致性。

如果遇到网络断开,增量复制就可以上场了,不过这个还跟 repl_backlog_size 这个大小有关系。

如果它配置的过小,主从服务器网络恢复时,可能发生「从服务器」想读的数据已经被覆盖了,那么这时就会导致主服务器采用全量复制的方式。所以为了避免这种情况的频繁发生,要调大这个参数的值,以降低主从服务器断开后全量同步的概率。

6. 面试题

  1. redis主从节点时长连接还是短链接?
    长连接

  2. 怎么判断 redis 某个节点是否正常工作?

redis 判断接点是否正常工作,基本都是通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接。

redis 主从节点发送的心态间隔是不一样的,而且作用也有一点区别:

redis 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态,可通过参数repl-ping-slave-period控制发送频率。
redis 从节点每隔 1 秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量,目的是为了:

  • 实时监测主从节点网络状态;
  • 上报自身复制偏移量, 检查复制数据是否丢失, 如果从节点数据丢失, 再从主节点的复制缓冲区中拉取丢失数据。
  1. 主从复制架构中,过期key如何处理?

主节点处理了一个key或者通过淘汰算法淘汰了一个key,这个时间主节点模拟一条del命令发送给从节点,从节点收到该命令后,就进行删除key的操作。

  1. redis 是同步复制还是异步复制? 异步复制

redis 主节点每次收到写命令之后,先写到内部的缓冲区,然后异步发送给从节点。

  1. 主从复制中两个 Buffer(replication buffer 、repl backlog buffer)有什么区别?

replication buffer 、repl backlog buffer 区别如下:
replication buffer 是在全量复制阶段会出现,主库会给每个新连接的从库,分配一个 replication buffer;
repl backlog buffer 是在增量复制阶段出现,一个主库只分配一个repl backlog buffer;

这两个 Buffer 都有大小限制的,当缓冲区满了之后。repl backlog buffer,因为是环形结构,会直接覆盖起始位置数据
replication buffer则会导致连接断开,删除缓存,从库重新连接,重新开始全量复制。

  1. redis 主从切换如何减少数据丢失?
  • 6.1 异步复制同步丢失

对于 redis 主节点与从节点之间的数据复制,异步复制的,当客户端发送写请求给主节点的时候,客户端会返回 ok,接着主节点将写请求异步同步给各个从节点,但是如果此时主节点还没来得及同步给从节点时发生了断电,那么主节点内存中的数据会丢失。

可以有 2 种解决方案:

第一种:客户端将数据暂时写入本地缓存和磁盘中,在一段时间后将本地缓存或者磁盘的数据发送给主节点,来保证数据不丢失;
第二种:客户端将数据写入到消息队列中,发送一个延时消费消息,比如10分钟后再消费消息队列中的数据,然后再写到主节点。

  • 6.2 集群产生脑裂数据丢失

在 redis 中,集群脑裂产生数据丢失的现象是怎样的呢?

总结一句话就是:由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。

网络故障,客户端向主机写数据,从节点不能及时的更新
如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),此时这些数据被旧主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。

这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在从节点中选举出一个 leeder 作为主节点,这时集群就有两个主节点了 —— 脑裂出现了。

这时候网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。

  1. redis 主从如何做到故障自动切换?

哨兵在发现主节点出现故障时,由哨兵自动完成故障发现和故障转移,并通知给应用方,从而实现高可用性。

6.2 哨兵

图解Redis 记录_第38张图片

6.3 哨兵机制使用原因

哨兵(Sentinel)机制,它的作用是实现主从节点故障转移。它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。

6.3 哨兵机制是如何工作

哨兵其实是一个运行在特殊模式下的 Redis 进程,所以它也是一个节点。它相当于是“观察者节点”,观察的对象是主从节点。
哨兵节点主要负责三件事情:监控、选主、通知:

6.3 监控 如何判断主节点真的故障

哨兵会每隔 1 秒给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵,这样就可以判断它们是否在正常运行。

  • 有主观下线、客观下线两类:
    如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令,哨兵就会将它们标记为「主观下线」。

针对「主节点」设计「主观下线」和「客观下线」两个状态,是因为有可能「主节点」其实并没有故障,可能只是因为主节点的系统压力比较大或者网络发送了拥塞,导致主节点没有在规定时间内响应哨兵的 PING 命令。

为了减少误判的情况,哨兵在部署的时候不会只部署一个节点,而是用多个节点部署成哨兵集群(最少需要三台机器来部署哨兵集群),通过多个哨兵节点一起判断,就可以就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况。同时,多个哨兵的网络同时不稳定的概率较小,由它们一起做决策,误判率也能降低。

  • 具体是怎么判定主节点为「客观下线」的呢?
  1. 当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。
  2. 当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。
  3. 哨兵判断完主节点客观下线后,哨兵就要开始在多个「从节点」中,选出一个从节点来做新主节点。

6.3 选主 由哪个哨兵进行主从故障转移

还需要在哨兵集群中选出一个 leeder,让 leeder 来执行主从切换。

  1. 那谁来作为候选者呢?
    哪个哨兵节点判断主节点为「客观下线」,这个哨兵节点就是候选者,所谓的候选者就是想当 Leader 的哨兵。

  2. 候选者如何选举成为 Leader?
    候选者会向其他哨兵发送命令,表明希望成为 Leader 来执行主从切换,并让所有其他哨兵对它进行投票。

在投票过程中,任何一个「候选者」,要满足两个条件即可:
第一,拿到半数以上的赞成票;
第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。

  1. 为什么哨兵节点至少要有 3 个?

如果哨兵集群中只有 2 个哨兵节点,此时如果一个哨兵想要成功成为 Leader,必须获得 2 票,而不是 1 票。

所以,如果哨兵集群中有个哨兵挂掉了,那么就只剩一个哨兵了,如果这个哨兵想要成为 Leader,这时票数就没办法达到 2 票,就无法成功成为 Leader,这时是无法进行主从节点切换的。

因此,通常我们至少会配置 3 个哨兵节点。这时,如果哨兵集群中有个哨兵挂掉了,那么还剩下两个个哨兵,如果这个哨兵想要成为 Leader,这时还是有机会达到 2 票的,所以还是可以选举成功的,不会导致无法进行主从节点切换。

6.3 主从故障转移的过程

第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑选出一个从节点,并将其转换为主节点。
第二步:让已下线主节点属下的所有「从节点」修改复制目标,修改为复制「新主节点」;
第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端;
第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点

6.3 哨兵集群是如何组成的?

配置哨兵的信息时,设置主节点名字、主节点的 IP 地址和端口号以及 quorum 值。

sentinel monitor

哨兵节点之间是通过 Redis 的发布者/订阅者机制来相互发现的。

在主从集群中,主节点上有一个名为__sentinel__:hello的频道,不同哨兵就是通过它来相互发现,实现互相通信的。

6.4 总结

图解Redis 记录_第39张图片

七、缓存篇

7.1 缓存雪崩、击穿、穿透

其中,缓存雪崩和缓存击穿主要原因是数据不在缓存中,而导致大量请求访问了数据库,数据库压力骤增,容易引发一系列连锁反应,导致系统奔溃。不过,一旦数据被重新加载回缓存,应用又可以从缓存快速读取数据,不再继续访问数据库,数据库的压力也会瞬间降下来。因此,缓存雪崩和缓存击穿应对的方案比较类似。

而缓存穿透主要原因是数据既不在缓存也不在数据库中。因此,缓存穿透与缓存雪崩、击穿应对的方案不太一样。

图解Redis 记录_第40张图片

  1. 缓存雪崩
    两个原因: 大量数据同时过期; Redis 故障宕机;
  • 对于大量数据同时过期:
    均匀设置过期时间;
    互斥锁;
    双 key 策略;
    后台更新缓存;

  • Redis故障宕机:
    服务熔断或请求限流机制;
    构建 Redis 缓存高可靠集群

互斥锁
当业务线程在处理用户请求时,如果发现访问的数据不在 Redis 里,就加个互斥锁,保证同一时间内只有一个请求来构建缓存(从数据库读取数据,再将数据更新到 Redis 里),当缓存构建完成后,再释放锁。未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。

实现互斥锁的时候,最好设置超时时间,不然第一个请求拿到了锁,然后这个请求发生了某种意外而一直阻塞,一直不释放锁,这时其他请求也一直拿不到锁,整个系统就会出现无响应的现象。

双key策略
一个是主 key,会设置过期时间,一个是备 key,不会设置过期,它们只是 key 不一样,但是 value 值是一样的,相当于给缓存数据做了个副本。
当业务线程访问不到「主 key 」的缓存数据时,就直接返回「备 key 」的缓存数据,然后在更新缓存的时候,同时更新「主 key 」和「备 key 」的数据。

后台更新缓存
业务线程不再负责更新缓存,缓存也不设置有效期,而是让缓存“永久有效”,并将更新缓存的工作交由后台线程定时更新。

事实上,缓存数据不设置有效期,并不是意味着数据一直能在内存里,因为当系统内存紧张的时候,有些缓存数据会被“淘汰”,而在缓存被“淘汰”到下一次后台定时更新缓存的这段时间内,业务线程读取缓存失败就返回空值,业务的视角就以为是数据丢失了。

  1. 缓存击穿
    概念:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。

应对缓存击穿可以采取前面说到两种方案:

  • 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
  1. 缓存穿透
    概念: 既不在缓存中,也不在数据库中
    发生的情况:
    业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
    黑客恶意攻击,故意大量访问某些读取不存在数据的业务;

应对缓存穿透的方案,常见的方案有三种:

  • 第一种方案,非法请求的限制;
    API 判断求请求参数是否合理,如果判断出是恶意请求就直接返回错误

  • 第二种方案,缓存空值或者默认值;
    可以针对查询的数据,在缓存中设置一个空值或者默认值,这样后续请求就可以从缓存中读取到空值或者默认值,返回给应用,而不会继续查询数据库。

  • 第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在;
    我们可以在写入数据库数据时,使用布隆过滤器做个标记,然后在用户请求到来时,业务线程确认缓存失效后,可以通过查询布隆过滤器快速判断数据是否存在,如果不存在,就不用通过查询数据库来判断数据是否存在。

  • 布隆过滤器原理:
    图解Redis 记录_第41张图片

7.2 三种常用的缓存读写策略

Cache Aside Pattern(旁路缓存模式)
写:先更新 DB 然后直接删除 cache 。
读:从 cache 中读取数据,读取到就直接返回

  • cache中读取不到的话,就从 DB 中读取数据返回
  • 再把数据放到 cache 中。

不能先删除cache后更新DB:(数据不一致问题)
请求1先把cache中的A数据删除 -> 请求2从DB中读取数据->请求1再把DB中的A数据更新

旁路缓存也存在不一致问题,但是内存写入快,发生不一致的概率低;

缺陷:
1 首次请求数据不在cache: 预热,把热点数据放入cache
2 写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率

缺陷2解决办法:
强一致性场景:更新DB的时候同样更新cache,加上分布式锁或者锁
弱一致性:更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,可以保证即使数据不一致的话影响也比较小

Read/Write Through Pattern(读写穿透)
写:
先查 cache,cache 中不存在,直接更新 DB。
cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(同步更新 cache 和 DB)

读:
从 cache 中读取数据,读取到就直接返回 。
读取不到的话,先从 DB 加载,写入到 cache 后返回响应。

Write Behind Pattern(异步缓存写入)
和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。

不同:
Read/Write Through 是同步更新 cache 和 DB,
Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。

比如消息队列中消息的异步写入磁盘、MySQL 的 InnoDB Buffer Pool 机制都用到了这种策略。
Write Behind Pattern 下 DB 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。

你可能感兴趣的:(CS基础,Redis,数据库,计算机基础,学习记录)