# 入门篇

# 1、Redis 入门概述

# Redis 是什么

Redis: RE mote Di ctionary S erver(远程字典服务器),一种基于 Key-Value内存数据库。

Remote Dictionary Server (远程字典服务) 是完全开源的,使用ANSIC 语言编写遵守 BSD 协议,是一个高性能的Key-Value数据库提供了丰富的数据结构,例如 String、Hash、List、Set、SortedSet 等等。数据是存在内存中的,同时 Redis支持事务、持久化、LUA 脚本、发布 / 订阅、缓存淘汰、流技术等多种功能特性提供了主从模式Redis SentinelRedis Cluster 集群架构方案。

# Redis 的功能与优势

Redis 的主流功能与应用如下:

  • 分布式缓存,帮 MySQL 减负

    image-20230802153755454

    MySQL 与 Redis 的对比:

    • MySQL 是关系型数据库,Redis 是key-value数据库(NoSQL 的一种)
    • MySQL 主要存储在磁盘,Redis 数据操作主要在内存
    • Redis 在一些场景中明显优于 MySQL,例如计数器、排行榜等
    • Redis 通常用于一些特定场景,需要与 Mysql 一起配合使用,两者并不是相互替换和竞争关系,而是共用和配合使用
  • 内存存储持久化(RDB+AOF):Redis 支持异步将内存中的数据写到硬盘上,同时不影响继续服务

  • 高可用架构搭配:避免某台 Redis 挂了后,影响系统运行

    • 单机
    • 主从
    • 哨兵
    • 集群
  • 缓存穿透、击穿、雪崩

  • 分布式锁:跨服务器加锁

  • 消息队列平台:Reids提供 list 和 set 操作,这使得 Redis 能作为一个很好的消息队列平台来使用。

    通过 Reids 的队列功能做购买限制。比如到节假日或者推广期间,进行一些活动,对用户购买行为进行限制,限制今天只能购买几次商品或者一段时间内只能购买一次。

  • 排行榜 + 点赞:Redis 提供的zset 数据类型能够快速实现这些复杂的排行榜。

Redis 的总体功能概览图:

image-20230802154814479

Redis 的优势:

  • 读写性能极高
  • 数据类型丰富:不仅支持key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储
  • 支持数据持久化:可将内存中的数据存入磁盘中,重启时再加载到内存使用
  • 支持数据备份,即 master-slave 模式的数据备份

小结:

image-20230802155434366

# Redis 下载

英文官网:https://redis.io/

中文网站:http://www.redis.cn/

下载网站:https://download.redis.io/releases/

中文文档:https://www.redis.com.cn/documentation.html

Redis 源码网站:https://github.com/redis/redis

Redis 在线测试:https://try.redis.io/

Redis 命令参考:http://doc.redisfans.com/

# Redis 怎么玩

  • 多种数据类型基本操作和配置
  • 持久化和复制,RDB/AOF
  • 事务的控制
  • 复制,集群等

# Redis 的迭代历史

image-20230802161140294

Redis重要版本

5.0 版本是直接升级到6.0 版本,对于这个激进的升级,Redis 之父 antirez 表现得很有信心和兴奋,所以第一时间发文来阐述 6.0 的一些重大功能 "Redis 6.0.0 GA is out!"

随后 Redis 再接再厉,直接王炸Redis7.0---2023 年爆款。2022 年 4 月 27 日 Redis 正式发布了 7.0 更新(其实早在 2022 年 1 月 31 日,Redis 已经预发布了 7.0rc-1,经过社区的考验后,确认没重大 Bug 才会正式发布)

Redis 版本的命名规则

  • 版本号第二位如果是奇数,则为非稳定版本。如 2.7、2.9、3.1
  • 版本号第二位如果是偶数,则为稳定版本。如 2.6、2.8、3.0、3.2
  • 当前奇数版本就是下一个稳定版本的开发版本。如 2.9 版本是 3.0 版本的开发版本

# Redis7 的新特性

可以从 redis 的 GitHub 的 releases 中查看当前版本的新特性,Redis7 的部分新特性总览:

image-20230802162007264

  • Redis Functions:Redis 函数,一种新的通过服务端脚本扩展 Redis 的方式,函数与数据本身一起存储。简言之,redis 自己要去抢夺 Lua 脚本的饭碗,但是 Lua 已经稳定且普及,所以 Redis Functions没必要学

    image-20230802162230304

  • Client-eviction:客户端相关优化,能让更多 client 连接上

    限制客户端内存使用,一旦 Redis 连接较多,再加上每个连接的内存占用都比较大的时候,Redis 总连接内存占用可能会达到 maxmemory 的上限,可以增加允许限制所有客户端的总内存使用量配置项,redis.config 中对应的配置项,有两种配置形式:

    • 指定内存大小。例如 maxmemory-clients 1g
    • 基于 maxmemory 的百分比。例如 maxmemory-clients 10%
    image-20230802162439869
  • Multi-part AOF:多 AOF 文件支持,AOF 文件由一个变成了多个,主要分为两种类型:基本文件 (base files)增量文件 (incr files),请注意这些文件名称是复数形式说明每一类文件不仅仅只有一个。在此之外还引入了一个清单文件 (manifest) 用于跟踪文件以及文件的创建和应用顺序(恢复)。性能急剧上升,再也不用担心 AOFRW 异步读写时的运维痛点

    image-20230802163004405
  • config 命令增强:对于Config Set 和 Get 命令,支持在一次调用过程中传递多个配置参数。例如,现在我们可以在执行一次 Config Set 命令中更改多个参数: config set maxmemory 10000001 maxmemory-clients 50% port 6399

  • 访问安全性增强 ACL V2:访问控制,在 redis.conf 配置文件中,protected-mode 默认为 yes,只有当你希望你的客户端在没有授权的情况下可以连接到 Redis server 的时候可以将 protected-mode 设置为 no

    image-20230802163118585
  • listpack 紧凑列表调整:listpack 是用来替代 ziplist 的新数据结构,在 7.0 版本已经没有 ziplist 的配置了(6.0 版本仅部分数据类型作为过渡阶段在使用),listpack 已经替换了 ziplist 类似 hash-max-ziplist-entries 的配置

  • RDB 保存时间调整:将持久化文件 RDB 的保存规则发生了改变,尤其是时间记录频度变化

  • 命令新增和变动:

    • Zset (有序集合) 增加 ZMPOP、BZMPOP、ZINTERCARD 等命令
    • Set (集合) 增加 SINTERCARD 命令
    • LIST (列表) 增加 LMPOP、BLMPOP ,从提供的键名列表中的第一个非空列表键中弹出一个或多个元素
  • 性能资源利用率、安全、等改进:自身底层部分优化改动,Redis 核心在许多方面进行了重构和改进

    • 主动碎片整理 V2:增强版主动碎片整理,配合 Jemalloc 版本更新,更快更智能,延时更低
    • HyperLogLog 改进:在 Redis5.0 中,HyperLogLog 算法得到改进,优化了计数统计时的内存使用效率,7 更加优秀
    • 更好的内存统计报告
    • 如果不为了 API 向后兼容,我们将不再使用 slave 一词......(政治正确)

# 2、Redis 安装与配置

Redis 一般在 Linux 环境上使用,那么就有两种方式:

  • 购买云服务器
  • VMWare 本地虚拟机

需要确保 Linux 是 64 位的,命令 getconf LONG_BIT

# Linux 环境需要 gcc 编译环境

安装 gcc: yum -y install gcc-c++

查看 gcc 版本: gcc -v

# Redis7 安装步骤

至少 6.0.8 以上,本次使用 Redis7.0

具体安装流程看脑图。

# 3、Redis 的 10 种数据类型

前文已声明过 Redis 是基于 Key-Value 的,而 key 类型一般是 String,这里所介绍的 10 种数据类型指的是 value 的数据类型

# 10 种数据类型 (value)

image-20230803183405953
  • 字符串(String):60% 的场景,常用

    • String 是 redis最基本的类型,一个 key 对应一个 value。
    • String 类型是二进制安全的,意思是 redis 的 String可以包含任何数据,比如jpg 图片或者序列化的对象
    • String 类型是 Redis 最基本的数据类型,一个 redis 中字符串 value 最多可以是 512M
  • 列表(List)

    • Redis 列表是简单的字符串列表按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
    • 它的底层实际是个双端链表,最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过 40 亿个元素)
  • 哈希集(Hash)

    • Redis hash 是一个 String 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象
    • Redis 中每个 hash 可以存储 232 - 1 键值对(40 多亿)
  • 集合(Set)

    • Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中的元素不能重复,集合对象的编码可以是 intset 或者 hashtable
    • Redis 中 Set 集合是通过哈希集实现的,所以添加,删除,查找的复杂度都是 O (1)。
    • 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储 40 多亿个成员)
  • 有序集合(ZSet):即上图中的 Sorted Set

    • Redis zset 和 set 一样也是 string 类型元素的集合,且不允许重复的成员
    • 不同的是每个元素都会关联一个 double 类型的分数,redis 正是通过分数来为集合中的成员进行从小到大的排序。
    • zset 的成员是唯一的,但分数 (score) 却可以重复
    • zset 集合是通过哈希集实现的,所以添加,删除,查找的复杂度都是 O (1)。 集合中最大的成员数为 232 - 1
  • 地理空间(GEO):即经纬度

    • Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,包括
      • 添加地理位置的坐标。
      • 获取地理位置的坐标。
      • 计算两个位置之间的距离。
      • 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合
  • 基数统计(HyperLogLog)基数指的是不重复的数字,例如统计网站的访问量

    • HyperLoglog 是一种估计集合基数的数据结构,作为一种概率数据结构,HyperLoglog 为有效的空间利用率提供了完美的精度。
    • HyperLogLog 实现最多使用 12 KB,并提供 0.81% 的标准错误
    • HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定,且很小
    • 在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 264 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
    • 但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素
  • 位图(bitmap):例如每日签到,是否点赞

    image-20230803185955581

    • 0 和 1状态表现二进制位的bit 数组
  • 位域(bitfield)

    • 通过 bitfield 命令可以一次性操作多个比特位域 (指的是连续的多个比特位),它会执行一系列操作并返回一个响应数组,这个数组中的元素对应参数列表中的相应操作的执行结果。
    • 说白了就是通过 bitfield 命令我们可以一次性对多个比特位域进行操作
  • 流(Stream):Redis 自己的消息(队列)中间件,但还是不如别人的好

    • Redis Stream 是 Redis 5.0 版本新增加的数据结构。
    • Redis Stream 主要用于消息队列(MQ,Message Queue),Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。
    • 简单来说发布订阅 (pub/sub) 可以分发消息,但无法记录历史消息
    • 而 Redis Stream 提供了消息的持久化主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失

# 常见数据类型的操作命令手册

英文官网:https://redis.io/commands/

中文官网:http://redis.cn/commands.html

# key 相关的操作命令

image-20230803191631855

  • KEYS * :查看当前库所有的 key

  • EXISTS key :判断某个 key 是否存在

  • TYPE key :查看某个 key 的 value 的数据类型

  • DEL key :删除某个 key 数据

  • UNLINK key :非阻塞删除某个 key,仅仅将 key 从 keyspace 元数据中删除,真正的删除会在后续异步中操作。

  • TTL key :查看某个 key 还有多少秒过期,-1 表示永不过期,-2 表示已过期

  • EXPIRE key 秒钟 :设置某个 key 的过期时间,默认 - 1 表示永不过期。

    Redis 的过期时间设置有四种形式:

    ・EXPIRE 秒 —— 设置指定的过期时间 (秒),表示的是时间间隔。

    ・PEXPIRE 毫秒 —— 设置指定的过期时间,以毫秒为单位,表示的是时间间隔。

    ・EXPIREAT 时间戳 - 秒 —— 设置指定的 Key 过期的 Unix 时间,单位为秒,表示的是时间 / 时刻。

    ・PEXPIREAT 时间戳 - 毫秒 —— 设置指定的 Key 到期的 Unix 时间,以毫秒为单位,表示的是时间 / 时刻。

    expire key seconds [NX|XX|GT|LT]

  • MOVE key dbindex[0-15] :将当前数据库中的某个 key 剪切到指定的数据库 db 中

  • SELECT dbindex :切换到指定的数据库 [0-15],默认为 0

  • DBSIZE :查看当前数据库的 key 数量

  • FLUSHDB :清空当前库

  • FLUSHALL :通杀全部库

# value 数据类型相关的操作命令

  • 命令是不区分大小写的,但是 key 是区分大小写的
  • 帮助命令: HELP @数据类型

# 字符串(String)

单 key 单 value,最常用

# 命令概览

image-20230803204957896

# 设置 / 获取单个键值
  • SET key value :将键 key 设定为指定的 “字符串” value 值。

    • 如果 key 已经保存了一个值,那么这个操作会直接覆盖原来的值,并且忽略原始类型。
    • set 命令执行成功之后,之前设置的过期时间都将失效,除非设置了 KEEPTTL 参数。
    • 返回值: simple-string-reply :如果 SET 命令正常执行那么回返回 OK ,否则如果加了 NX 或者 XX 选项,但是没有设置条件。那么会返回 nil
    • 时间复杂度: O(1)

    完整的命令是: set key value [NX|XX] [GET] [EX seconds|PX milliseconds|EXAT unix-time-seconds|PXAT unix-time-milliseconds|KEEPTTL]

    image-20230804102328872

    如何获得设置指定的 Key 过期的 Unix 时间,单位为秒:

    System.out.println(Long.toString(System.currentTimeMillis()/1000L));
  • GET key :返回 keyvalue

    • 如果 key 不存在,返回特殊值 nil
    • 如果 keyvalue 不是 string,就返回错误,因为 GET 只处理 string 类型的 values
    • 时间复杂度: O(1)
# 设置 / 获取多个键值
  • MSET key value [key value ...] :对应给定的 keys 到他们相应的 values 上。

    • MSET用新的 value 覆盖旧的,就像普通的 SET 命令一样。如果你不想覆盖已经存在的 values,请参看命令 MSETNX
    • MSET原子的,所以所有给定的 keys 是一次性 set 的。客户端不可能看到这种一部分 keys 被更新而另外的没有改变的情况。
    • 返回值: simple-string-reply总是 OK,因为 MSET 不会失败。
    • 时间复杂度: O(N) ,其中 N 是要设置的 key 的数量。
  • MGET key [key ...] :返回所有指定的 keyvalue

    • 对于每个不对应 string 或者不存在的 key,都返回特殊值 nil 。正因为此,这个操作从来不会失败
    • 返回值: array-reply : 指定的 key 对应的 values 的 list
    • 时间复杂度: O(N) ,其中 N 是要查询的 key 的数量。
  • MSETNX key value [key value ...] :对应给定的 keys 到他们相应的 values 上,但是只要有一个 key 已经存在, MSETNX 一个操作都不会执行。

    • 由于这种特性, MSETNX 可以实现要么所有的操作都成功,要么一个都不执行,这样可以用来设置不同的 key,来表示一个唯一的对象的不同字段。
    • MSETNX原子的,所以所有给定的 keys 是一次性 set 的。客户端不可能看到这种一部分 keys 被更新而另外的没有改变的情况。
    • 返回值: integer-reply ,只有以下两种值:
      • 1 如果所有的 key 被 set
      • 0 如果没有 key 被 set (至少其中有一个 key 是存在的)
    • 时间复杂度: O(N) ,其中 N 是要设置的 key 的数量。
# 获取指定区间范围内的值
  • SETRANGE key offset value :覆盖 key 对应的 string 的一部分,从指定的 offset 处开始,覆盖 value 的长度。
    • 如果 offset 比当前 key 对应 string 还要长,那这个 string 后面就补 0以达到 offset。
    • 不存在的 key 被认为是空字符串,所以这个命令可以确保 key 有一个足够大的字符串,能在 offset 处设置 value。
    • 模式:正因为有了 SETRANGE 和类似功能的 GETRANGE 命令,你可以把 Redis 的字符串当成线性数组,随机访问只要 O (1) 复杂度。这在很多真实场景应用里非常快和高效。
    • 返回值: integer-reply修改后的字符串长度
    • 时间复杂度: O(1) ,不计算就地复制新字符串所花费的时间。
      • 通常,此字符串非常小,因此摊销复杂度为 O(1)。
      • 否则复杂度为 O(M),M 是 value 参数的长度。
  • GETRANGE key start end :返回 key 对应的字符串 value 的子串,这个子串是由 startend 位移决定的(两者都在 string 内)。
    • 可以用负的位移来表示从 string 尾部开始数的下标。所以 - 1 就是最后一个字符,-2 就是倒数第二个,以此类推。
    • 这个函数处理超出范围的请求时,都把结果限制在 string 内。
    • 返回值: bulk-reply ,子串
    • 时间复杂度: O(N) ,其中 N 是字符串长度,复杂度由最终返回长度决定。但由于通过一个字符串创建子字符串是很容易的,它可以被认为是 O(1)
# 数值增减

前提:一定要是数字,才能增减!

  • INCR key :对存储在指定 key 的数值执行原子的加 1 操作

    • 如果指定的 key 不存在,那么在执行 incr 操作之前,会先将它的值设定为 0
    • 如果指定的 key 中存储的值不是字符串类型(fix:)或者存储的字符串类型不能表示为一个整数,那么执行这个命令时服务器会返回一个错误 (eq:(error) ERR value is not an integer or out of range)。
    • 返回值: integer-reply ,递增操作后 key 对应的值
  • INCRBY key increment :将 key 对应的数字decrement

    • 如果 key 不存在,操作之前,key 就会被置为 0。
    • 如果 key 的 value 类型错误或者是个不能表示成数字的字符串,就返回错误。
    • 返回值: integer-reply ,增加操作后 key 对应的值
  • DECR key :对 key 对应的数字做减 1 操作

    • 如果 key 不存在,那么在操作之前,这个 key 对应的值会被置为 0。
    • 如果 key 有一个错误类型的 value 或者是一个不能表示成数字的字符串,就返回错误。
    • 返回值:数字,减小后 key 对应的值
  • DECRBY key decrement :将 key 对应的数字decrement

    • 如果 key 不存在,操作之前,key 就会被置为 0。
    • 如果 key 的 value 类型错误或者是个不能表示成数字的字符串,就返回错误。
    • 返回值:返回一个数字:减少之后的 value 值
# 获取字符串长度以及内容追加
  • STRLEN key :返回 key 的 string 类型 value 的长度。
  • APPEND key value :将 value 追加key 对应的字符串值之后,并返回追加后的长度
# 分布式锁
image-20230804110715178
  • SETEX key seconds value :设置 key 对应字符串 value ,并且设置 key 在给定的 seconds 时间之后超时过期。这个命令是原子的,等效于执行下面的命令:

    SET mykey value
    EXPIRE mykey seconds
  • SETNX key value

    • 如果 key 不存在,将值设为 value ,这种情况下等同 SET 命令。
    • key 存在时,什么也不做。 SETNX 是 “SET if Not eXists” 的简写。
    • 可以与 DEL 命令配合使用,对资源加锁
# 先获取,再设置
  • GETSET key value :等同于 set key value get 命令,返回 key 的旧 value,将 key 的值设置为新 value
# 应用场景

image-20230804111834362

  • 抖音中许多人点赞某个视频,点一下加一次

  • 公众号上某篇文章的阅读数,只要点击了 rest 地址,直接可以使用 incr key 命令增加一个数字 1,完成记录数字。

    image-20230804111954515

# 列表(List)

单 key 多 value,有序有重复

# 命令概览

image-20230804112132538

# List 的数据结构

image-20230804112413028

一个双端链表的结构,容量是 232-1 个元素,大概 40 多亿,主要功能有 push / pop 等,一般用在栈、队列、消息队列等场景。

left、right 都可以插入:

  • 如果键不存在,创建新的链表;

  • 如果键已存在,新增内容;

  • 如果值全移除,对应的键也就消失了。

它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

# 插入与遍历
  • LPUSH key value [value ...] :将所有的 value 从 key 列表的左端依次插入
    • 如果 key 不存在,那么在进行 push 操作前会创建一个空列表。
    • 如果 key 对应的值不是一个 list 的话,那么会返回一个错误。
    • 返回值:操作后的 list 长度。
  • RPUSH key value [value ...] :所有的 value 从 key 列表的右端依次插入
  • LRANGE key start stop :遍历 key 列表在下标 [start,stop] 中的元素,注意右区间是闭合的
    • start 和 end 偏移量都是基于 0 的下标,即 list 的第一个元素下标是 0(list 的表头),第二个元素下标是 1,以此类推。
    • 偏移量也可以是负数,表示偏移量是从 list 尾部开始计数。例如, -1 表示列表的最后一个元素,-2 是倒数第二个,以此类推。
    • 当下标超过 list 范围的时候不会产生 error
      • 如果 start 比 list 的尾部下标大的时候,会返回一个空列表。
      • 如果 stop 比 list 的实际尾部大的时候,Redis 会当它是最后一个元素的下标。
# 弹出
  • LPOP key :从 key 列表的左端弹出一个元素,并返回该元素。
  • RPOP key :从 key 列表的右端弹出一个元素,并返回该元素。
# 根据下标获取元素(从左到右)
  • LINDEX key index :返回 key 列表对应索引 index 处的值。
# 获取列表中的元素个数
  • LLEN key
# 删除指定数量个指定 value 的元素
  • LREM key count value :从存于 key 的列表里移除前 count 次出现的值为 value 的元素。 这个 count 参数通过下面几种方式影响这个操作:
    • count > 0: 从头往尾移除值为 value 的元素。
    • count < 0: 从尾往头移除值为 value 的元素。
    • count = 0: 移除所有值为 value 的元素。
# 截取并保存指定下标区间的元素
  • LTRIM key start stop :修剪 (trim) 一个已存在的 list,这样 list 就会只保留指定范围 [start,stop] 的指定元素
# 将元素移至另一个列表
  • RPOPLPUSH source destination 原子性地移除存储在 source 的列表的最后一个元素(列表尾部元素), 并把该元素放入存储在 destination 的列表的第一个元素位置(列表头部)。
    • 如果 source 和 destination 是同样的,那么这个操作等同于移除列表最后一个元素并且把该元素放在列表头部, 所以这个命令也可以当作是一个旋转列表的命令。
    • 返回值:被移除和放入的元素
    • 模式 1—— 安全的队列:RPOPLPUSH 可以实现,消费者端取到消息的同时把该消息放入一个正在处理中的列表。避免了消息丢失的安全问题。
    • 模式 2—— 循环列表
# 设置某下标对应的元素值(从左到右)
  • LSET key index value :设置 index 位置的 list 元素的值为 value
# 在指定元素的前 / 后插入元素
  • LINSERT key BEFORE|AFTER pivot value :把 value 插入存于 key 的列表中在基准值 pivot 的前面或后面。返回插入后的列表长度,或者当 pivot 不存在时返回 - 1。
# 应用场景
  • 微信公众号订阅的消息

# 哈希集(Hash)

单 key,但 value 是一个键值对,即单 key 单键值对:Map<String,Map<Object,Object>>

# 命令概览

image-20230804131019423

# 设置、获取、删除

image-20230804132747399

  • HSET key field value :将 key 指定的哈希集中指定字段 field 的值设置为 value 。返回值为:
    • 1,如果 field 是一个新字段
    • 0,如果 field 已存在
  • HGET key field :获取 key 指定的哈希集中字段 field 所关联的值
  • HMSET key field value [field value ...] :将 key 指定的哈希集中所有指定字段 field 的值设置为对应 value 。将重写所有在哈希集中存在的字段。
  • HMGET key field [field ...] :返回 key 指定的哈希集中所有指定字段 field 的值
  • HGETALL key :返回 key 指定的哈希集中所有的字段和值返回值中,每个字段名的下一个是它的值,所以返回值的长度是哈希集大小的两倍
  • HDEL key field [field ...] :从 key 指定的哈希集中移除各个指定的域返回成功移除的域的数量
# 获取哈希集的字段数量
  • HLEN key :返回 key 指定的哈希集包含的字段的数量
# 判断哈希集中是否存在某个字段
  • HEXISTS key field :返回 key 指定的哈希集中是否存在字段 field
# 获取哈希集中的所有 key/value
  • HKEYS key :返回 key 指定的哈希集中所有字段的名字
  • HVALS key :返回 key 指定的哈希集中所有字段的值
# 增加指定字段的数值
  • HINCRBY key field increment :将 key 指定的哈希集中指定字段 field 的数值增加 increment
  • HINCRBYFLOAT key field increment :将 key 指定的哈希集中指定字段 field 的数值增加 float 类型的  increment
# 只设置哈希集中不存在的字段
  • HSETNX key field value :只在 key 指定的哈希集中不存在指定的字段 field 时,设置其值为 value
    • 如果 key 指定的哈希集不存在,会创建一个新的哈希集并与 key 关联。
    • 如果字段已存在,该操作无效果
# 应用场景

京东购物车的早期设计,目前不再采用,当前中小厂可用:

  • 新增商品 → hset shopcar:uid1024 334488 1
  • 新增商品 → hset shopcar:uid1024 334477 1
  • 增加商品数量 → hincrby shopcar:uid1024 334477 1
  • 商品总数 → hlen shopcar:uid1024
  • 全部选择 → hgetall shopcar:uid1024

image-20230804143259448

# 集合(Set)

单 key 多 value,且无序无重复

# 命令概览

image-20230804143501168

# 添加元素
  • SADD key member [member ...] :向 key 指定的集合中添加一个或多个指定的 member 元素。
    • 指定的一个或者多个元素 member 如果已经在集合 key 中存在则忽略
    • 如果集合 key 不存在,则新建集合 key , 并添加 member 元素到集合 key 中
    • 返回值:成功添加到集合中的元素数量(不包括已经存在于集合中的元素)
# 遍历元素
  • SMEMBERS key :返回 key 指定的集合中所有元素
# 判断集合中是否有某元素
  • SISMEMBER key member :判断成员 member 是否是集合 key 中的成员,是则返回 1,不是或者集合 key 不存在则返回 0
# 删除元素
  • SREM key member [member ...] :在 key 集合中移除指定的元素返回成功移除的元素个数
# 获取集合中的元素数量
  • SCARD key :返回 key 集合的元素数量(即集合的基数)
# 从集合中随机展现指定个数个元素,但元素不删除
  • SRANDMEMBER key [count] 随机返回 key 集合中的 count 个元素
    • 如果 count 是整数且小于元素的个数,返回含有 count 个不同的元素的数组
    • 如果 count 是个整数且大于集合中元素的个数时,仅返回整个集合的所有元素
    • 当 count 是负数,则会返回一个包含 count 的绝对值的个数元素的数组
    • 如果 count 的绝对值大于元素的个数,则返回的结果集里会出现一个元素出现多次的情况
# 从集合中随机弹出指定个数个元素,且元素删除
  • SPOP key [count] 随机弹出 key 集合中的 count 个元素
# 将集合中已存在的某个值移动到另一个集合
  • SMOVE source destination member membersource 集合移动到 destination 集合中。对于其他的客户端,在特定的时间元素将会作为 source 或者 destination 集合的成员出现。
    • 如果 source 集合不存在或者不包含指定的元素,这 smove 命令不执行任何操作并且返回 0。
    • 否则对象将会从 source 集合中移除,并添加到 destination 集合中去,
      • 如果 destination 集合已经存在该元素,则 smove 命令仅将该元素从 source 集合中移除.
    • 如果 source 和 destination 不是集合类型,则返回错误.
# 集合运算

社交软件中一定会大量使用

假设集合 A 的元素为 abc12,集合 B 的元素为 123ax。

# 差集运算

即 A-B,表示属于 A 但不属于 B 的元素构成的集合

  • SDIFF key [key ...] :返回一个集合与给定集合的差集的元素.
# 并集运算

即 A∪B,表示属于 A 或 B 的元素合并后的集合

  • SUNION key [key ...] :返回给定的多个集合的并集中的所有成员.
# 交集运算

即 A∩B,表示属于 A 且属于 B 的共同拥有的元素构成的集合

  • SINTER key [key ...] :返回指定所有的集合的成员的交集.
  • SINTERCARD numkeys key [key ...] [LIMIT limit] :类似于 SINTER 命令,但是不返回结果集,只返回指定 numkeys 个集合的交集结果的基数,是 Redis7 的新指令。LIMIT 用来限制返回值大小,
    • 若返回值小于 limit,则返回该返回值
    • 若返回值大于 limit,则返回 limit
# 应用场景
# 微信抽奖小程序

image-20230804164737598

步骤Redis 命令
1 用户 ID,立即参与按钮sadd key 用户 ID
2 显示已经有多少人参与了,上图 23208 人参加SCARD key
3 抽奖 (从 set 中任意选取 N 个中奖人)SRANDMEMBER key 2 // 随机抽奖 2 个人,元素不删除
SPOP key 3 // 随机抽奖 3 个人,元素会删除
# 微信朋友圈点赞查看同赞朋友

image-20230804165326659

步骤Redis 命令
1 新增点赞sadd pub:msgID 点赞用户 ID1 点赞用户 ID2
2 取消点赞srem pub:msgID 点赞用户 ID
3 展现所有点赞过的用户SMEMBERS pub:msgID
4 点赞用户数统计,就是常见的点赞红色数字scard pub:msgID
5 判断某个朋友是否对楼主点赞过SISMEMBER pub:msgID 用户 ID
# QQ 内推可能认识的人

image-20230804165609346

# 有序集合 Zset(sorted set)

单 key 多 value,且有序无重复,在每个 value 前加一个score分数值。

例如:

  • set 是 k1->v1,v2
  • Zset 是 k1->score1 v1, score2 v2
# 命令概览

image-20230804170237545

# 添加元素
  • ZADD key [NX|XX] [CH] [INCR] score member [score member ...] :将多个分数 / 成员( score / member )对添加到键为 key 有序集合(sorted set)里面,以递增的方式排序,返回新添加成员的数量
    • XX: 仅仅更新存在的成员,不添加新成员。
    • NX: 不更新存在的成员,只添加新成员。
    • CH: 修改返回值为发生变化的成员总数,原始是返回新添加成员的总数 (CH 是 changed 的意思)。更改的元素是新添加的成员,已经存在的成员更新分数。所以在命令中指定的成员有相同的分数将不被计算在内。
    • INCR: 当 ZADD 指定这个选项时,成员的操作就等同 ZINCRBY 命令,对成员的分数进行递增操作。
    • 如果指定添加的成员已经是有序集合里面的成员,则会更新成员的分数(scrore),并更新到正确的排序位置。
    • 时间复杂度:对于每个添加的成员为 O(log(N)) ,其中 N 指的是有序集合中的元素数量。
# 遍历元素
  • ZRANGE key start stop [WITHSCORES] :遍历 key 指定的有序集合中下标在 [ start , stop ] 间的元素返回的元素按分数递增排序

    • 如果添加了 WITHSCORES 选项,会将元素的分数与元素一并返回。
  • ZREVRANGE key start stop [WITHSCORES] :与 ZRANGE 类似,只不过元素是按分数递减排序

  • ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] :遍历 key 指定的有序集合中 score 值在 [ min , max ] 间的元素返回的元素按分数递增排序

    • 具有相同分数的元素按字典序排列

    • LIMIT 参数指定返回结果的起始下标 offset 以及数量 count

      注意,如果 offset 太大,定位 offset 就可能遍历整个有序集合,这会增加 O (N) 的复杂度。

    • minmax 可以是 - inf 和 + inf,这样一来,你就可以在不知道有序集的最低和最高 score 值的情况下,使用 ZRANGEBYSCORE 命令。

    • 默认使用闭区间,也可以通过给参数前增加 ( 符号来使用可选的开区间 (小于或大于)

# 获取元素的分数
  • ZSCORE key member :返回 key 指定的有序集合中,成员 member 的 score 值。
# 获取有序集合的元素数量
  • ZCARD key
# 删除元素
  • ZREM key member [member ...] :从 key 指定的有序集合中删除指定的多个成员 member ,返回的是删除的成员个数,不包括不存在的成员
# 增加某个元素的分数
  • ZINCRBY key increment member :为 key 指定的有序集合中的成员 member 的 score 值加上增量 increment
# 获得指定分数范围内的元素数量
  • ZCOUNT key min max :返回 key 指定的有序集合中 score 值在 [ min , max ] 之间的成员数量。
# 弹出一个或多个元素(Redis7)
  • ZMPOP numkeys key [key ...] <MIN | MAX> [COUNT count] :从 numkeys 个有序集合 key 列表中的第一个非空有序集合中,弹出 count 个元素。
    • 参数 MIN 表示按照 score 值递增的顺序依次弹出
    • 参数 MAX 表示按照 score 值递减的顺序依次弹出
    • 参数 COUNT 表示指定要弹出的元素数量默认设置为 1

image-20230804193510087

# 获取元素的下标值(即排名)
  • ZRANK key member 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大) 顺序排列。排名以 0 为底,也就是说,score 值最小的成员排名为 0。
  • ZREVRANK key member :与 ZRANK 命令类似,只不过是按照 score 值递减排序。
# 应用场景

根据商品的销量对商品进行排序显示。

思路:定义商品销售排行榜 (sorted set 集合),key 为 goods:sellsort,分数为商品销售数量。

步骤Redis 命令
商品编号 1001 的销量是 9,商品编号 1002 的销量是 15zadd goods:sellsort 9 1001 15 1002
有一个客户又买了 2 件商品 1001,商品编号 1001 销量加 2zincrby goods:sellsort 2 1001
求商品销量前 10 名ZREVRANGE goods:sellsort 0 9 withscores

image-20230804193828825

# 位图(bitmap)

单 key 多 value,其中 value 是由0 和 1状态表现二进制位的bit 数组

# 需求

用于状态统计,例如:

  • 用户是否登陆过
  • 电影、广告是否被点击播放过
  • 钉钉打卡上下班,签到统计
# 数据结构

image-20230804195914474

说明:

  • String 类型作为底层数据结构实现的一种统计二值状态的数据类型
  • 位图本质是数组,它是基于 String数据类型的按位的操作。该数组由多个二进制位组成,每个二进制位都对应一个偏移量 (我们称之为一个索引)。
  • Bitmap 支持的最大位数是 232,它可以极大的节约存储空间,使用 512M 内存就可以存储多达 42.9 亿的字节信息 (232 = 4294967296)
# 常用命令

image-20230804200305770

  • SETBIT key offset value :设置 key 指定的 bitmap(字符串)在 offset 处的 bit 值为 value返回 offset 处原来的 bit 值

    image-20230804201300199

  • GETBIT key offset :返回 key 指定的 bitmap(字符串)在 offset 处的 bit 值。

    • 当 offset 超出了字符串长度的时候,这个字符串就被假定为由 0 比特填充的连续空间。

    image-20230804201502414

  • STRLEN key :返回 key 指定的 bitmap (字符串) 的字节数 (1 字节 = 8bit),超过 8bit 后再扩容 1 字节。

    image-20230804201827460

  • BITCOUNT key [start end] :统计 key 指定的 bitmap (字符串) 中 bit 值为 1 的数量。可以指定额外的参数 startend 来限制统计范围的下标。

    image-20230804202346532

  • BITOP operation destkey key [key ...] :对一个或多个 key 指定的 bitmap (字符串) 之间进行位元操作,并将结果保存到 destkey 上,其中操作方式 operation 有以下几种:

    • AND:BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN ,对一个或多个 key 求逻辑并,并将结果保存到 destkey 。
    • OR:BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN,对一个或多个 key 求逻辑或,并将结果保存到 destkey 。
    • XOR:BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN,对一个或多个 key 求逻辑异或,并将结果保存到 destkey 。
    • NOT:BITOP NOT destkey srckey,对给定 key 求逻辑非,并将结果保存到 destkey 。
# 应用场景
  • 统计全年天天登陆占用多少字节

    image-20230804203608847

  • 按照年

    按年去存储一个用户的签到情况,365 天只需要 365 / 8 ≈ 46 Byte,1000W 用户量一年也只需要 44 MB 就足够了。

    假如是亿级的系统,

    每天使用 1 个 1 亿位的 Bitmap 约占 12MB 的内存(10^8/8/1024/1024),10 天的 Bitmap 的内存开销约为 120MB,内存压力不算太高

    此外,在实际使用时,最好对 Bitmap 设置过期时间,让 Redis 自动删除不再需要的签到记录以节省内存开销。

# 基数统计(HyperLogLog)

单 key

# 需求

UV:Unique Visitor,独立访客,一般理解为客户端 IP,通常用于统计网站 / 文章的访问量,需要考虑去重,同时不希望占用太大内存

  • 统计用户搜索网站关键词的数量
  • 统计用户每天搜索不同词条个数
# HyperLogLog 是什么

一言蔽之:HyperLogLog 是一种根据条件去重的基数估计算法

基数:是一种数据集,是去重后的真实数量。

image-20230805111729020

基数统计:统计一个集合中不重复的元素个数。

大白话:去重脱水后的真实数据。

与 set 的区别:二者同样能达到去重的目的,区别是:set 需要保存元素数据本身,而HyperLogLog 只含有基数相关信息,不保存元素数据本身,例如只保存网站的访问量,而不保存各个访问者的信息,因此HyperLogLog 占用的内存更小

优点:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。但是,因为 HyperLogLog只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog不能像集合那样,返回输入的各个元素

在 Redis 里面,每个 HyperLogLog 键只需要花费 12KB 内存,就可以计算接近 264 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

缺点:有 0.81% 的标准误差

# 命令概览

image-20230805111923099

image-20230805112011715

image-20230805111944589

  • PFADD key element [element ...] :向 key 指定的 HyperLogLog 中 **"添加" 若干指定元素 element **。

    这里添加的 element 仅用于计算基数,不会被存储,也无法返回!

  • PFCOUNT key [key ...]

    • 当参数为一个 key 时:返回存储在 HyperLogLog 结构体的该变量的近似基数
    • 当参数为多个 key 时:返回这些 HyperLogLog 并集的近似基数
    • 返回的可见集合基数并不是精确值,而是一个带有 0.81% 标准错误(standard error)的近似值.

    • 这个命令的一个副作用是可能会导致 HyperLogLog 内部被更改。出于缓存的目的,它会用 8 字节来记录最近一次计算得到基数,所以 PFCOUNT 命令在技术上是个写命令。

  • PFMERGE destkey sourcekey [sourcekey ...] :将由 sourcekey 指定的多个 HyperLogLog 合并为一个由 destkey 指定的 HyperLogLog ,合并后的 HyperLogLog 的基数接近于所有输入 HyperLogLog 的可见集合的并集。

# 应用场景

统计天猫网站首页亿级 UV 的 Redis 统计方案。高级篇见!

# 地理空间(GEO)

本质是有序集合 Zset,不同的是score 值替换为经纬度

地球上的地理位置是使用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90, 90]。核心思想主要分为三步:

  • 将三维的地球变为二维的坐标
  • 将二维的坐标转换为一维的点块
  • 将一维的点块转换为二进制,再通过 base32 编码

image-20230805135151199

# 命令概览与实操

如何获取某个地址的经纬度:http://api.map.baidu.com/lbsapi/getpoint/

  • GEOADD key longitude latitude member [longitude latitude member ...] :向 key 指定的 GEO 中添加若干个指定的地理空间位置(经度 longitude 、纬度 latitude 、位置名称 member )。

    • 该命令以采用标准格式的参数 x,y,所以经度必须在纬度之前
    • 时间复杂度:每一个元素添加是 O(log(N)) ,因为底层是有序集合 Zset,N 是有序集合 Zset 的元素数量。

    image-20230805141104831

  • GEOPOS key member [member ...] :从 key 指定的 GEO 中获取若干个指定了地理位置名称 member 的地理位置的经纬度

    • 返回值:一个数组,每项由两个元素组成:经度、纬度。
    • 时间复杂度:每一个元素添加是 O(log(N)) ,N 是有序集合 Zset 的元素数量。

    image-20230805141619345

  • GEOHASH key member [member ...] :从 key 指定的 GEO 中获取若干个指定了地理位置名称 member 的地理位置的 Geohash 表示

    • geohash 算法生成的base32 编码值
    • 返回值:一个数组,每项是一个 geohash
    • 时间复杂度:每一个元素添加是 O(log(N)) ,N 是有序集合 Zset 的元素数量

    image-20230805141940907

  • GEODIST key member1 member2 [unit] :返回 key 指定的 GEO 中两个给定位置( member1member2 )之间的距离

    • 其中参数 unit 可取以下四个值:
      • m 表示单位为米,默认单位
      • km 表示单位为千米
      • mi 表示单位为英里
      • ft 表示单位为英尺
    • 在计算距离时会假设地球为完美的球形,在极限情况下, 这一假设最大会造成 0.5% 的误差

    image-20230805142341845

  • GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] 以给定的经纬度 ( longitudelatitude ) 为中心,返回 key 指定的 GEO 中,与中心的距离不超过给定最大距离 radius 的所有位置元素

    • WITHCOORD : 将位置元素的经度和纬度也一并返回

    • WITHDIST : 在返回位置元素的同时,将位置元素与中心之间的距离也一并返回。距离的单位和用户给定的范围单位保持一致

    • WITHHASH : 以 52 位有符号整数的形式,返回位置元素经过原始 geohash 编码的有序集合分值。这个选项主要用于底层应用或者调试,实际中的作用并不大

    • COUNT限定返回的记录数

    • 命令默认返回未排序的位置元素。通过以下两个参数, 用户可以指定被返回位置元素的排序方式:

      • ASC : 根据中心的位置, 按照从近到远的方式返回位置元素。
      • DESC : 根据中心的位置, 按照从远到近的方式返回位置元素。
    • 时间复杂度:O(N+log(M)),其中 N 是由中心和半径限定的圆形区域的边界框内的元素数量,M 是索引内的项目数量。

    • 返回值:

      • 在没有给定任何 WITH 选项的情况下,命令只会返回一个像 [“New York”,”Milan”,”Paris”] 这样的线性(linear)名称列表

      • 在指定了 WITHCOORDWITHDISTWITHHASH 等选项的情况下,命令返回一个二层嵌套数组,内层的每个子数组就表示一个元素。

        在返回嵌套数组时,子数组的第一个元素总是位置元素的名字。至于额外的信息,则会作为子数组的后续元素,按照以下顺序被返回:

        1. 以浮点数格式返回的中心与位置元素之间的距离,单位与用户指定范围时的单位一致
        2. geohash 整数
        3. 由两个元素组成的坐标,分别为经度和纬度

    image-20230805143142149

  • GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] :与 GEORADIUS 命令类似,只不过这里指定的是中心的位置名称 member ,而不是它的经纬度。

    image-20230805143329626

注意

  • 从 Redis 版本 6.2.0 开始, GEORADIUS 命令被视为已弃用。在迁移或编写新代码时,它可以GEOSEARCH 命令和带有 BYRADIUS 参数的 GEOSEARCHSTORE 命令替换

  • 从 Redis 版本 6.2.0 开始, GEORADIUSBYMEMBER 命令被视为已弃用。在迁移或编写新代码时,它可以由带有 BYRADIUS 和 FROMMEMBER 参数的 GEOSEARCH 命令和 GEOSEARCHSTORE 命令替换

# 应用场景
  • 美团地图位置附近的酒店推送
  • 高德地图附近的核酸检查点

具体见高级篇!

# 流(Stream)

与 Java 中的 Stream 是两码事,几乎没有任何关系!

自成一脉,类型就是 Stream!

# 是什么

Redis5.0 之前的痛点Redis消息队列的 2 种方案:

  • List 实现消息队列

    image-20230805154343768

    image-20230805154402852

    • 点对点的模式
    • 缺点:对于一对多的情况力不从心
    • 常用来做异步队列使用,将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理
  • Pub/Sub(发布 / 订阅)

    image-20230805181258770

    • 缺点 1:消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。
    • 缺点 2:没有 Ack 机制来保证数据的可靠性,假设一个消费者都没有,那消息就直接被丢弃了。

综上,Redis5.0 版本新增了一个更强大的数据结构 Stream。

一言蔽之:Redis Steam 就是 Redis 版本的 MQ 消息中间件 + 阻塞队列

# 能干嘛

Redis Stream 的功能概览如下:

  • 实现消息队列
  • 支持消息的持久化
  • 支持自动生成全局唯一 ID
  • 支持 ack 确认消息的模式
  • 支持消费组模式

让消息队列更加的稳定和可靠。

# 底层结构和原理

image-20230805155540984

一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容。具体角色如下:

序号角色名定义
1Message Content消息内容
2Consumer group消费组,通过 XGROUP CREATE 命令创建,同一个消费组可以有多个消费者
3Last_delivered_id游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
4Consumer消费者,消费组中的消费者
5Pending_ids消费者会有一个状态变量,用于记录被当前消费已读取但未 ack 的消息 Id,如果客户端没有 ack,这个变量里面的消息 ID 会越来越多,一旦某个消息被 ack 它就开始减少。
这个 pending_ids 变量在 Redis 官方被称之为 待处理条目列表 PEL (Pending Entries List),记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符),它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理
# 命令的理论与实操
# 队列相关命令(即生产者角度)
指令名称指令作用
XADD添加消息到队列末尾
XRANGE获取消息列表 (可以指定范围),忽略删除的消息
XREVRANGE反向获取消息列表,ID 从大到小
XTRIM限制 Stream 的长度,如果已经超长会进行截取
XDEL删除消息
XLEN获取 Stream 中的消息长度
XREAD获取消息 (阻塞 / 非阻塞),返回大于指定 ID 的消息
  • XADD key [NOMKSTREAM] [<MAXLEN | MINID> [= | ~] threshold [LIMIT count]] <* | id> field value [field value ...] :向 key 指定的 Stream 队列中添加若干条消息内容( fieldvalue

    • Redis 对 MessageID 有强制要求,必须是时间戳 - 自增 ID这样的方式,且同一时间戳下的后续 ID 不能小于前一个
    • Redis 在增加 Message 条目时会检查当前 MessageID 与上一条目的 MessageID,自动纠正错误的情况,一定要保证后面的 MessageID 比前面大,一个流中信息条目的 ID 必须是单调增的,这是流的基础
    • * 号表示服务器自动生成 MessageID(类似 mysql 里面主键 auto_increment)
    • 返回值:添加的 Message 条目的 MessageID

    image-20230805162458455

  • XRANGE key start end [COUNT count] :返回 key 指定的 Stream 队列中与 ** 给定 ID 范围 [ start , end ]** 匹配的消息条目。

    • start 表示最小 ID,- 代表最小值
    • end 表示最大 ID,+ 代表最大值
    • count 表示能获取的最大消息数

    image-20230805164434264

  • XREVRANGE key end start [COUNT count] :与 XRANGE 命令相反,以相反的顺序返回消息条目。需要先指定最大 ID end ,再指定最小 ID start

    image-20230805165347529

  • XDEL key ID [ID ...] :从 key 指定的 Stream 队列中逻辑删除指定 ID 的消息条目。

    • 当你从 Stream 中删除一个条目的时候,条目并没有真正被驱逐,只是被标记为删除

    image-20230805165654765

  • XLEN key :返回 key 指定的 Stream 队列中的消息条目数。

    image-20230805165759795

  • XTRIM key <MAXLEN | MINID> [= | ~] threshold [LIMIT count] 通过删除较旧的消息条目(ID 较低的)来修剪 key 指定的 Stream 队列。可以使用以下策略之一来修剪流:

    • MAXLEN :只要 Stream 队列的长度超过指定的阈值 threshold (值为正整数),就会逐出 ID 较低的旧消息条目

      image-20230805170543749

    • MINID :驱逐 ID 低于阈值 threshold (值为 MessageID)的消息条目。

      image-20230805170619213

  • XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] :从一个或者多个 key 指定的 Stream 队列中读取消息条目,仅返回 ID 大于调用者报告的最后接收 ID 的消息条目。参数 count 表示最多读取的消息数目。参数 [BLOCK milliseconds] 表示是否以阻塞的方式读取消息,默认不阻塞

    • 非阻塞使用:即不提供 BLOCK 参数,此时命令是同步的,会返回 Stream 队列中的一系列消息条目

      image-20230805171740158

      • **$** 代表特殊 ID,表示当前 Stream 已经存储的最大的 ID作为最后一个 ID,当前 Stream 中不存在大于当前最大 ID 的消息,因此此时返回 nil

      • 0-0 代表从最小的 ID 开始获取 Stream 中的消息,当不指定 count,将会返回 Stream 中的所有消息,注意也可以使用 0(00/000 也都是可以的……)

    • 阻塞使用:提供 BLOCK 参数,如果 milliseconds 设置为 0,表示永远阻塞

      image-20230805172251635

小结:Stream 的基础方法,使用 xadd 存入消息和 xread 循环阻塞读取消息的方式可以实现简易版的消息队列,交互流程如下:

image-20230805172400243

对比 List 结构实现 Redis 消息队列:

image-20230805172428816

# 消费组相关命令(即消费者角度)
指令名称指令作用
XGROUP CREATE创建消费组
XGROUP SETID设置消费组最后递送消息的 ID
XGROUP DESTROY完全销毁消费组
XGROUP DELCONSUMER移除给定的消费者
XREADGROUP GROUP读取消费者组中的消息
XACK将消息标记为 ack,即 “已处理”
XPENDING打印待处理消息的详细信息
XCLAIM转移消息的归属权(长期未被处理 / 无法处理的消息,转交给其他消费者组进行处理)
XINFO打印 Stream\Consumer\Group 的详细信息
XINFO GROUPS打印消费者组的详细信息
XINFO STREAM打印 Stream 的详细信息
  • XGROUP [CREATE key groupname id-or-$] [SETID key id-or-$] [DESTROY key groupname] [DELCONSUMER key groupname consumername] :用于管理 key 指定的 Stream 队列上所关联的消费组,可以:

    • CREATE :在 key 指定的 Stream 队列上创建一个新消费组

      • 设置消费组名为 groupname
      • 指定从消息 id 开始从头到尾读取(消费)
      • 或者 $ 表示从尾部开始反向读取(消费)

      image-20230805174041986

    • SETID :针对 key 指定的 Stream 队列,设置消费组最后递送的消息 id ,同理也能取 $

    • DESTROY :从 key 指定的 Stream 队列上销毁一个名为 groupname 的消费组

    • DELCONSUMER :针对 key 指定的 Stream 队列,从组名为 groupname 的消费组中移除名为 consumername 的消费者

  • XREADGROUP GROUP groupname consumername [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] :是 XREAD 命令的特殊版本,支持消费者组。针对 key 指定的 Stream 队列,可以让消费组 groupname 的不同消费者 consumername 来读取 Stream 的不同部分

    • 同一个消费组中的消费者共享同一个游标,因此:

      • 同一个消费组中的消费者不能消费同一条消息

        image-20230805175640453

      • 不同消费组中的消费者可以消费同一条消息

        image-20230805180522655

    • 参数 COUNT 限制当前消费者能够读取的消息数量,默认为 +∞

    • 参数 BLOCK 表示是否阻塞读取消息

    • ID 表示从哪条消息 id 开始读取,其中 > 表示从第一条尚未被消费的消息开始读取

    消费组的目的:

    ​ 让组内的多个消费者共同分担读取消息。所以,我们通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的。(负载均衡

    image-20230805180648195

重点问题:ACK 机制

image-20230805180956093

问题:

​ 基于 Stream 实现的消息队列,如何保证消费者在发生故障或宕机再次重启后,仍然可以读取未处理完的消息?

方案:

  • Streams 会自动使用内部队列(也称为待处理条目列表 PEL (Pending Entries List))留存每个消费组里每个消费者读取的消息保底措施,直到消费者使用 XACK 命令通知 Streams “消息已经处理完成”。
  • 消费确认机制增加了消息的可靠性,一般在业务处理完成之后,需要执行 XACK 命令确认消息已经被消费完成。
  • XPENDING key groupname [start end count] [consumername] :查询 key 指定的 Stream 队列上,组名为 groupname消费组已读取但未确认的情况

    image-20230805182227981

    返回值是概要信息

    1. 该消费者组的待处理消息的数量

    2. 待处理消息的最小 ID

    3. 待处理消息的最大 ID

    4. 对于消费者组中每一个至少有一条待处理消息的消费者,

      1. 他的名称
      2. 他的待处理消息数量
    • 若指定消费者名 consumername ,也可以查看某个消费者的已读取但未确认的情况

      image-20230805182430081

      返回值是详细信息

      1. 消息的 ID
      2. 获取并仍然要确认消息的消费者名称,我们称之为消息的当前所有者
      3. 自上次将此消息传递给该消费者以来,经过的毫秒数
      4. 该消息被传递的次数
  • XACK key groupname ID [ID ...] :从 key 指定的 Stream 队列中的消费者组 groupname 的待处理条目列表(简称 PEL)中删除若干条指定了 ID 的消息,即确认消息。返回成功确认的消息数。

    image-20230805184337097

  • XINFO [CONSUMERS key groupname] key key [HELP] :打印关于 Stream 和关联的消费组的不同的信息。

    image-20230805184502786

# 四个特殊符号
符号作用
- +最小和最大可能出现的 Id
$表示只消费新的消息,当前流中最大的 id,可用于将要到来的信息
>用于 XREADGROUP 命令,表示迄今还没有发送给组中使用者的信息,会更新消费者组的最后 ID
*用于 XADD 命令中,让系统自动生成 Id
# 使用建议

Redis Stream 不能 100% 替代 Kafka、RabbitMQ 来使用,生产案例少,慎用!

# 位域(Bitfield)

了解即可

定义:将一个 Redis 字符串看作是一个由二进制位组成的数组,并能对变长位宽和任意没有字节对齐的指定整型位域进行寻址和修改。

作用:

  • 位域修改
  • 溢出控制

基本语法:

image-20230805185443067

# 4、Redis 持久化(persistence)

Redis 持久化:Redis 是如何将数据从内存写入磁盘的?

Redis 为什么需要持久化?因为 Redis 运行过程中数据是缓存在内存中的,一旦发生意外导致宕机,数据将会消失,Redis 就会形同虚设。

Redis 持久化的三种实现方式:

  • RDB(Redis DataBase)
  • AOF(Append Only File)
  • RDB + AOF

# 持化双雄

image-20230806003810720 image-20230806003858469

# RDB( R edis D ata B ase)

# 简介

RDB 持久化:指定的时间间隔执行数据集的时间点快照,将内存中的数据集以全量快照的形式写入磁盘保存的文件是dump.rdb,恢复时将磁盘中的快照文件读回内存中。

image-20230806005245328

# 触发 RDB 快照的时间间隔

对于自动触发快照的时间间隔,在配置文件 redis.conf 中的 SNAPSHOTTING 下配置 save 参数,来触发 RDB 持久化条件。比如 “save m n”: 表示每隔 m 秒检测一次数据集,如果检测出超过 n 次变化时,自动触发 RDB 持久化条件,执行快照。

注意,这里说的是每隔 m 秒检测一次,对变化的计数是累加的,只要在某次检测中发现变化数累加值达到 n 次,就会触发 RDB 持久化。而不是要求 n 次变化都集中发生在某个 m 秒内!

  • Redis6.0.16 及之前

    • save 900 1:每隔 900s (15min) 检测一次,如果有超过 1 个 key 发生了变化,就写一份新的 RDB 文件
    • save 300 10:每隔 300s (5min) 检测一次,如果有超过 10 个 key 发生了变化,就写一份新的 RDB 文件
    • save 60 10000:每隔 60s (1min) 检测一次,如果有超过 10000 个 key 发生了变化,就写一份新的 RDB 文件

    image-20230806010837837

  • Redis6.0.16 以后至 Redis7 至今

    • 每隔 3600s(1hour)检测一次,如果有超过 1 处变化,就写一份新的 RDB 文件
    • 每隔 300s(5min)检测一次,如果有超过 100 处变化,就写一份新的 RDB 文件
    • 每隔 60s(1min)检测一次,如果有超过 10000 处变化,就写一份新的 RDB 文件

    image-20230806010819707

# RDB 快照的触发方式
# 自动触发
修改配置信息
  • 修改时间间隔与变化数:通过修改配置文件 redis.conf 中的 SNAPSHOTTING 下的 save 参数

  • 修改 dump.rdb 文件的保存路径 **(Redis 每次启动都会读取磁盘中该目录下的 dump.rdb 文件(文件名需要与配置文件中保持一致)来初始化内存中的 Redis 数据库)**:通过修改配置文件 redis.conf 中的 SNAPSHOTTING 下的 dir 参数

    通过 Redis 命令 CONFIG GET dir 可以查看 dir 参数的取值:

    image-20230806113359010

  • 修改 dump.rdb 文件的名称:通过修改配置文件 redis.conf 中的 SNAPSHOTTING 下的 dbfilename 参数

触发备份的2个案例
  • 每隔 5 秒检测一次,检测到 2 处变化,执行备份:

    image-20230806113923793

  • 每隔 5 秒检测一次,先设置 k3,只检测到 1 处变化,无备份动作。再过一段时间(可以超过 5 秒!)设置 k4,检测出第 2 处变化,执行备份:

    image-20230806114111595

如何恢复数据
  1. 根据配置文件 redis.conf ,将备份文件(dump.rdb)移至保存路径下

    这里备份文件的名称、保存路径一定要与配置文件 redis.conf 中的设置保持一致!

  2. 让 Redis 读取指定的配置文件 redis.conf ,并启动 Redis 服务

    image-20230806114837995

执行 flushdb / flushall 命令会产生一个空的 dump.rdb 文件,执行 shutdown产生一个退出时的 dump.rdb 文件,且会覆盖同路径下的同名备份文件等到下次 Redis 服务启动时,读取的就是这个空的 / 上次 shutdown 时的 dump.rdb 文件

image-20230806115618721

因此,不可以把备份文件 dump.rdb 和生产 redis 服务器放在同一台机器,必须分开存储,分机隔离,以防生产机物理损坏后备份文件也挂了

image-20230806115554369

# 手动触发

在默认情况下(即自动触发),Redis 将数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 Redis 的配置文件 redis.conf 进行设置,让它在 “每 N 秒检测一次,当数据集有 M 个改动时” 这一条件被满足时,自动保存一次快照。

也可以通过调用 SAVE 或者 BGSAVE 命令,手动让 Redis 保存数据库的快照

快照保存的工作方式:

  • Redis 调用forks. 同时拥有父进程和子进程。

    在 Linux 程序中,fork () 会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,尽量避免膨胀。

  • 子进程将数据集写入到一个临时 RDB 文件中。

  • 当子进程完成对新 RDB 文件的写入时,Redis 用新 RDB 文件替换原来的 RDB 文件,并删除旧的 RDB 文件。

这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。

image-20230806122757973
SAVE命令

** 线上严禁使用!** 因为在主程序中执行 SAVE 命令时,会阻塞当前 redis 服务器,Redis 不能处理其他命令,缓存功能就缺失了,直到持久化工作完成。

image-20230806123508579

image-20230806123541045

BGSAVE命令(默认)

Redis 会在后台异步进行快照操作,不阻塞当前 Redis 服务器,还可以同时响应客户端请求。

image-20230806145215893

image-20230806145228038

LASTSAVE命令:获取最近一次快照的时间

image-20230806145339310

image-20230806145349994

# RDB 的优缺点

RDB 持久化的优点:

  • 适合大规模的数据恢复
  • 按照业务,定时备份
  • 对数据完整性和一致性要求不高
  • dump.rdb 文件在内存中的加载速度要比 AOF 快得多

RDB 持久化的缺点:

  • 一定间隔时间做一次备份,所以如果 Redis 意外 down 掉的话,就会丢失从当前至最近一次快照期间的数据,快照之间的数据会丢失
  • 内存数据的全量同步,如果数据量太大会导致I/O 严重影响服务器性能
  • RDB 依赖于主进程的 fork,在更大的数据集中,这可能会导致服务请求的瞬间延迟
  • fork 的时候内存中的数据被克降了一份,大致 2 倍的数据膨胀性,需要考虑

快照之间的数据丢失案例:

  1. 正常录入数据

    image-20230806151059638

  2. kill -9 故意模拟意外宕机

    image-20230806151113184

  3. Redis 重启,查看数据发现丢失

    image-20230806151151946

# 如何检查、恢复 dump.rdb 文件

当 dump.rdb 文件破损时,需要恢复它,可以使用 redis-check-rdb 命令进行修复。

image-20230806151515656

# 触发 RDB 快照的情况
  • 配置文件 redis.conf 中默认的快照配置

  • 手动 save / bgsave 命令

  • 执行 flushall / flushdb 命令会产生空的 dump.rdb 文件

  • 执行 shutdown 命令,且没有设置开启 AOF 持久化

  • 主从复制时,主节点自动触发

# 如何禁用 RDB 快照

两种方式:

  • 命令: res-cli config set save ""

  • 修改配置文件:

    image-20230806152126685

# RDB 快照的配置优化项

即配置文件 redis.conf 中的 SNAPSHOTTING 模块

  • save <seconds> <changes>:触发快照的时间间隔、变化数

  • dbfilename:rdb 文件的名称

  • dir:rdb 文件的保存路径

  • stop-writes-on-bgsave-error:当子进程执行快照保存出现错误时,是否让主进程停止接收新的写请求,默认为 yes。

    如果不在乎数据不一致或者有其他手段发现和控制这种不一致,也可以设置为 no。此时在快照写入失败时,也能确保 Redis 继续接受新的写请求

  • rdbcompression:对于存储到磁盘中的快照,可以设置是否采用 LZF 算法进行压缩存储,默认为 yes。

  • rdbchecksum:是否采用 CRC64 算法对快照文件进行数据校验,默认为 yes。

  • rdb-del-sync-files:看不懂,默认情况下 no,禁用。

# RDB 小结
image-20230806153628002

# AOF( A ppend O nly F ile)

# 简介

AOF 持久化:以日志文件的形式来记录 Redis 执行过的每个写操作指令,只许追加记录,不可改写记录。Redis 启动之初会读取该日志文件重新构建数据,换言之,Redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。

默认情况下,Redis 是没有开启 AOF 的,开启 AOF 功能需要在配置文件 redis.conf 中设置配置: appendonly yes

动机:对于 RDB 持久化的快照,如果 Redis 因为某些原因而造成故障停机,那么服务器将丢失最近写入、且仍未保存到快照中的那些数据。 因此,Redis 增加了一种完全耐久的持久化方式:AOF 持久化。

AOF 持久化所保存的文件: appendonly.aof 文件。

# AOF 持久化的工作流程

image-20230806195559645

  1. Client 作为命令的来源,会有多个源头以及源源不断的请求写命令

  2. 在这些命令到达 Redis Server 以后并不是直接写入 AOF 文件,会将其这些命令先放入 AOF 缓存中进行保存。

    这里的 AOF 缓冲区实际上是内存中的一片区域,存在的目的是当这些命令达到一定量以后再写入磁盘,避免频繁的磁盘 IO 操作。

  3. AOF 缓冲会根据 AOF 缓冲区同步文件的三种写回策略将命令写入磁盘上的 AOF 文件

  4. 随着写入 AOF 内容的增加为避免文件膨胀,会根据规则进行命令的合并 (又称 AOF 重写),从而起到 AOF 文件压缩的目的。

  5. 当 Redis Server 服务器重启的时候Redis 会从 AOF 文件载入数据

# AOF 缓冲区的三种写回策略

AOF 缓冲区需要将它保存的写命令写入磁盘上的 AOF 文件,可以修改配置文件上的 参数appendfsync ,有三种策略:

  • always同步写回,每个写命令执行完立刻同步地将日志写入磁盘上的 AOF 文件
  • everysec每秒写回,每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔 1 秒把缓冲区中的内容写入磁盘
  • no操作系统控制的写回,每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘

image-20230806200107294

# 案例演示和说明
# 配置文件说明(6 vs 7)

image-20230806200804696

如何开启AOF

将配置文件 redis.conf 中 APPEND ONLY MODE 模块下的 参数appendonly 设置为 yes,即打开 AOF 持久化支持。

使用默认的写回策略:everysec

将配置文件 redis.conf 中 APPEND ONLY MODE 模块下的 参数appendfsync 设置为 everysec

AOF文件的保存路径

Redis6:AOF 保存文件的位置和 RDB 保存文件的位置一样,都是通过配置文件 redis.conf 的 参数dir 配置

dir/dump.rdb

Redis7:在 参数dir 的基础上,再通过配置文件 redis.conf 中 APPEND ONLY MODE 模块下的 参数appenddirname ,二者拼接成为 AOF 文件的保存路径

dir/appenddirname/appendonly.aof

AOF文件的名称

Redis6:有且仅有 appendonly.aof 一个 AOF 文件

Redis7:采用了 multi part AOF 机制,将原来的单个 AOF 文件拆分成多个 AOF 文件文件名前缀都是 appendonly.aof ,分为三种类型

  • BASE AOF:基础 AOF,它一般由子进程 **通过重写产生**,该文件最多只有一个。

  • INCR AOF:增量 AOF,它一般会在 **AOFRW 开始执行时被创建**,该文件可能存在多个。

    记录写命令的主力军!

  • HISTORY AOF:历史 AOF,它由 BASE AOF 和 INCR AOF 变化而来。每次AOFRW 成功完成时,本次 AOFRW 之前对应的 BASE AOF 和 INCR AOF 都将变为 HISTORY AOF,之后会被 Redis 自动删除

为了管理这些 AOF 文件,引入了一个manifest (清单)文件来跟踪、管理这些 AOF。同时,为了便于 AOF 备份和拷贝,我们将所有的 AOF 文件和 manifest 文件放入一个单独的文件目录中,目录名由参数 appenddirname 配置 (Redis 7.0 新增配置项) 决定。

image-20230806210712843

# 正常恢复 AOF 文件

首先开启 AOF,然后执行写操作,生成 AOF 文件到指定的目录中

image-20230806220336112

Redis 重启并重新加载,结果符合预期,具体过程见脑图。

flushdb 命令也会被增量 AOF 记录,因此 Redis 重启后也会加载并执行清空库操作。

# 异常恢复 AOF 文件

何为异常:在高并发情况下,可能上一秒刚写入一半,突然 Redis 挂了,导致 AOF 文件有缺陷、错误,那么如何恢复 AOF 文件呢?

首先故意乱写正常的增量 AOF 文件,模拟网络闪断文件写 error

image-20230806222044975

然后尝试重启 Redis 加载 AOF 文件,发现怎样都启动不了

image-20230806222108149

执行异常修复命令 redis-check-aof --fix 来修复增量 AOF 文件

只能修复增量 AOF 文件!

image-20230806222145406

重启 Redis,成功加载 AOF 文件

image-20230806222228890

# AOF 的优缺点

AOF 有以下优点:

  • 更好地保护数据不丢失

    使用 AOF Redis 更加持久∶您可以有不同的 fsync 策略: 根本不 fsync、每秒 fsync、每次查询时 fsync。使用每秒 fsync 的默认策略,写入性能仍然很棒。fsync 是使用后台线程执行的,当没有 fsync 正在进行时,主线程将努力执行写入,因此您只能丢失一秒钟的写入

  • 易修复

    AOF 日志是一个仅附加日志,因此不会出现寻道问题,也不会在断电时出现损坏问题。即使由于某种原因(磁盘已满或其他原因)日志以写一半的命令结尾, redis-check-aof 工具也能够轻松修复它。

  • 得益于 AOF 的重写机制,能够自我压缩

    当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。重写是完全安全的,因为当 Redis 继续附加到旧文件时,会使用创建当前数据集所需的最少操作集生成一个全新的文件,一旦第二个文件准备就绪,Redis 就会切换两者并开始附加到新的那一个。

  • 性能高

  • 文件内容易理解

    AOF 以易于理解和解析的格式依次包含所有操作的日志。您甚至可以轻松导出 AOF 文件。

  • 可做紧急恢复

    即使您不小心使用该 FLUSHALL 命令刷新了所有内容,只要在此期间没有执行日志重写,您仍然可以通过停止服务器、删除最新命令并重新启动 Redis 来保存您的数据集。

AOF 有以下缺点:

  • 对于相同的数据集而言,aof 文件要远大于 rdb 文件恢复速度慢于 rdb
  • aof运行效率要慢于 rdb,每秒同步策略效率较好,不同步效率和 rdb 相同
# AOF 重写机制
# 简介

AOF 重写机制:启动 AOF 文件的内容压缩,合并其中的命令,只保留可以恢复数据的最小指令集。

重写完成后

  • 重写结果被保存到一个新的 BASE AOF 文件中,文件名上的标号加 1。
  • 同时,新建一个空的 INCR AOF 文件,文件名上的标号加 1,旧的被删除

AOF 重写机制有两种触发方式

  • 自动触发:当 INCR AOF 文件同时满足以下两个条件时,Redis 就会自动启动重写机制,只保留可以恢复数据的最小指令集

    INCR AOF 文件负责记录从 AOF 缓冲区写回的写命令

    • 当 INCR AOF 文件的大小超过上一次重写结果(即 BASE AOF 文件)大小 1 倍(可以通过配置 auto-aof-rewrite-percentage 修改)
    • 当 INCR AOF 文件的大小超过 64MB(可以通过配置 auto-aof-rewrite-min-size 修改)
  • 手动触发:可以手动使用命令 bgrewriteaof 来重写。

# 案例演示和说明

具体过程见脑图,这里只演示 AOF 重写后的效果:

image-20230807004504357

自动重写

image-20230807004613263

手动重写

结论:

  • AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。
  • AOF 文件重写触发机制:通过 redis.conf 配置文件中的 auto-aof-rewrite-percentage : 默认值为 100,以及 auto-aof-rewrite·min-size : 64mb 配置,也就是说默认 Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍文件大于 64M 时触发。
# 重写原理
  1. 在重写开始前,redis 会创建一个 “重写子进程”,这个子进程会读取现有的 AOF 文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。

  2. 与此同时,主进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的 AOF 文件中,这样做是保证原有的 AOF 文件的可用性,避免在重写过程中出现意外。

  3. 当 “重写子进程” 完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新 AOF 文件中

  4. 当追加结束后,redis 就会用新 AOF 文件来代替旧 AOF 文件,之后再有新的写指令,就都会追加到新的 AOF 文件中

  5. 重写 aof 文件的操作,并没有读取旧的 aof 文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的 aof 文件,这点和快照有点类似

# AOF 的配置优化项

模块 APPEND ONLY MODE:

image-20230807005317244

# AOF 小结

image-20230807005534144

# RDB-AOF 混合持久化

# 简介

Redis 默认仅使用 RDB 持久化,禁用 AOF 持久化。但是,当我们手动启用 AOF 持久化后,AOF 的优先级高于 RDB!对应的数据恢复顺序和加载流程如下图:

image-20230807010014261

# 到底采用哪种持久化方式?

二者各自的特点如下:

  • RDB 持久化:(定时一锅端)能够在指定的时间间隔能对你的数据进行快照存储
  • AOF 持久化:(实时记录写命令)记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,命令以 redis 协议追加保存每次写的操作到文件末尾。

同时开启时的情况:

  • 当 redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集更完整
  • RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。但是作者建议不要只使用 AOF,因为 RDB 更适合用于备份数据库 (AOF 在不断变化不好备份),留着 rdb 以防万一

综上,推荐方式:RDB+AOF 混合方式,既能快速加载又能避免丢失过多的数据。配置方式:

  1. 默认开启混合方式,对应配置文件中的 aof-use-rdb-preamble ,默认为 yes
  2. 开启 AOF 持久化,对应配置文件中的 appendonly 设置为 yes,默认为 no

此时,RDB 镜像做全量持久化,AOF 做增量持久化:

  • 先使用 RDB 进行快照存储
  • 然后使用 AOF 持久化记录所有的写操作
  • 当重写策略满足或手动触发重写的时候,将最新的数据存储为新的 RDB 记录。
  • 这样的话,重启服务的时候会从 RDB 和 AOF 两部分恢复数据,既保证了数据完整性,又提高了恢复数据的性能。简单来说:混合持久化方式产生的文件一部分是 RDB 格式,一部分是 AOF 格式。----》AOF 包括了 RDB 头部 + AOF 混写

image-20230807011820642

# 纯缓存模式

Redis 作为基于 key-value 的内存数据库,Redis 最主要的功能是用作缓存,而 Redis 持久化会消耗 Redis 的性能,因此可以同时关闭 RDB+AOF

禁用 RDB

此时仍然可以手动使用命令 SAVEBGSAVE 生成 rdb 文件

  • 命令: res-cli config set save ""

  • 修改配置文件:

    image-20230807012414863

禁用 AOF

此时仍然可以手动使用命令 BGREWRITEAOF 生成 aof 文件

  • 命令: res-cli config set appendonly no
  • 修改配置文件:将 redis.conf 中 APPEND ONLY MODE 模块下的 参数appendonly 设置为 no

# 5、Redis 事务(Transactions)

数据库事务:由一系列数据库操作组成的一个完整的逻辑过程,不可拆分。

例如银行转帐,从原账户扣除金额,以及向目标账户添加金额,这两个数据库操作的总和,构成一个完整的逻辑过程,不可拆分。

具有 ACID 特性:

  • 原子性( a tomicity)
    • 一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
    • 事务在执行过程中发生错误,会被恢复(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
  • 一致性( c onsistency)
    • 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
  • 隔离性( i solation)
    • 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致
    • 事务隔离分为不同级别,包括:
      • 读未提交(Read uncommitted)
      • 读已提交(read committed)
      • 可重复读(repeatable read)
      • 序列化(Serializable)
  • 持久性( d urability)
    • 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

# Redis 事务是什么?

Redis 事务允许在一个队列中,一次性、按顺序、排他地,执行多个命令,本质是一组命令的集合。一个事务中的所有命令都会序列化,按顺序地串行化执行,而不会被其它命令插入,不许加塞

Redis 事务以命令 MULTIEXECDISCARDWATCH 为中心,提供两个重要保证:

  • 事务中的所有命令都是按顺序序列化、执行的。由另一个客户端发送的请求将永远不会在 Redis 事务的执行过程中提供服务。这保证了命令作为单个独立操作执行。
  • EXEC 命令触发事务中所有命令的执行,因此,如果客户端在调用 EXEC 命令之前在事务上下文中失去了与服务器的连接,则不会执行任何操作,相反,如果调用了 EXEC 命令,则会执行所有操作。当使用 AOF 时,Redis 确保使用单个 write(2)系统调用将事务写入磁盘。但是,如果 Redis 服务器崩溃或被系统管理员以某种艰难的方式终止,则可能只注册了部分操作。Redis 将在重新启动时检测到这种情况,并将退出并返回错误。使用 redis check aof 工具,可以修复将删除部分事务的 AOF 文件,以便服务器可以重新启动。

从版本 2.2 开始,Redis 允许以乐观锁的形式为上述两种操作提供额外的保证,其方式与 check-and-set (CAS)操作非常相似。稍后将对此进行记录。

# Redis 事务 vs 数据库事务

  1. 单独的隔离操作:Redis 的事务仅仅是保证事务里的操作会被连续独占的执行,redis 命令执行是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求

  2. 没有隔离级别的概念:因为事务提交前任何指令都不会被实际执行,也就不存在 “事务内的查询要看到事务里的更新,在事务外查询不能看到” 这种问题了

    因此不存在 “三大读问题”:不可重复读、脏读、幻读

  3. 不保证原子性:Redis 的事务 **不保证原子性**,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有回滚能力

  4. 排它性:Redis 会保证一个事务内的命令依次执行,而不会被其它命令插入

# 案例说明

# 常用命令

命令描述返回值
MULTI标记一个事务块的开始。随后的一系列指令将在执行 EXEC 时作为一个原子执行。OK
EXEC执行事务块中所有在排队等待的指令,并将链接状态恢复到正常。
当使用 WATCH 时,只有当被监视的键没有被修改,且允许检查设定机制时, EXEC 会被执行。
每个元素与原子事务中的指令一一对应。
使用 WATCH 时,如果被终止, EXEC 则返回一个空的应答集合。
WATCH key [key ...]监视若干个 key,如果在事务执行前这些 key 发生改动,那么事务将被打断。在事务中有条件的执行(乐观锁)。OK
UNWATCH释放所有被 WATCH 命令监视的 key
如果执行 EXEC 或者 DISCARD , 则不需要手动执行该命令。
OK
DISCARD取消事务,放弃执行事务块中的所有指令
同时,释放所有被 WATCH 命令监视的 key
OK

# Redis 事务中的错误

在 Redis 事务处理过程中,可能会遇到两种命令错误

  • 命令可能无法排队,因此在调用 EXEC 之前可能会出现错误。例如,该命令可能在语法上错误(参数数量错误、命令名称错误…),或者可能存在一些关键条件,如内存不足(如果使用 maxmemory 指令将服务器配置为具有内存限制)。
  • 调用 EXEC 后,命令可能会失败。例如,因为我们对具有错误值的键执行了操作(如对字符串值调用列表操作)。

从 Redis 2.6.5 开始,服务器将在命令累积过程中检测到错误。然后,它将拒绝执行在 EXEC 期间返回错误的事务,从而丢弃该事务

对于 Redis 事务中遇到的错误,有两种处理方式,具体见 case3 和 case4。

# case1:正常执行

MULTI + 一系列 Redis 命令 + EXEC

image-20230807133026046

# case2:放弃事务

MULTI + 一系列 Redis 命令 + DISCARD

image-20230807115127428

# case3:全体连坐

情况(编译时异常):EXEC 命令执行前,由于语法错误或者内存不足等原因,导致事务块中某条命令无法加入队列

解决方式:EXECABORT,取消执行事务块中队列里的所有命令。

image-20230807120332653

# case4:冤头债主

情况(运行时异常):EXEC 命令执行后,事务块中某条命令执行失败。例如,因为我们对具有错误值的键执行了操作(如对字符串值调用列表操作)。

解决方式:即使事务内一个命令失败,队列中的所有其他命令都会被执行

补充:Redis 不提供事务回滚的功能,在事务执行出错后,开发者必须自行恢复数据库状态

image-20230807132520648

# case5:watch 监控

悲观锁(Pessimistic Lock):顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次访问数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。

乐观锁(Optimistic Lock): 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新数据时会判断一下在此期间别人有没有去更新这个数据

乐观锁策略:只有当 **提交版本 大于 记录当前版本**,才能执行数据更新。

CAS(Check-And-Set)

# watch

watch 命令是一种乐观锁的实现,Redis 在修改时会检测数据是否被更改,如果更改了,则执行失败

image-20230807131835629

第一个窗口蓝色框第 5 步执行结果返回为 nil,也就是相当于是失败。

WATCH 命令用于为 Redis 事务提供一种 CAS(check-and-set)行为
WATCH 的 key 用来检测 key 的变化。如果在执行 EXEC 命令之前至少修改了一个被监视的 key,则整个事务将中止, EXEC 将返回一个 Null 回复以通知事务失败。

# unwatch

image-20230807132443762

# 小结
  • EXEC 命令执行后,会释放所有被 WATCH 命令监视的 key
  • 当客户端连接丢失的时候 (比如退出链接),会释放所有被 WATCH 命令监视的 key

# 总结

Redis 事务分为三部分:

  • 开启:以 MULTI 开始一个事务
  • 入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
  • 执行:由 EXEC 命令触发执行事务

# 6、Redis 管道(pipeline)

Redis 管道与 Redis 事务之间的关系,类似于雷锋与雷峰塔的关系,Java 与 JavaScript 的关系,看上去相似,但实际没有任何关系!

# 面试题

如何优化命令频繁往返造成的性能瓶颈?

问题由来:

Redis 是一种基于客户端 - 服务端模型以及请求 / 响应协议的 TCP 服务。一个请求会遵循以下步骤:

  1. 客户端向服务端发送命令(分四步:发送命令→命令排队→命令执行→返回结果),并监听 Socket 返回,通常以阻塞模式等待服务端响应

  2. 服务端处理命令,并将结果返回给客户端

上述两步的总耗时称为:Round Trip Time(即 RTT, 数据包往返于两端的时间)

如果同时需要执行大量的命令,那么就要等待上一条命令应答后再执行,这中间不仅仅多了 RTT(Round Time Trip),而且还频繁调用系统 IO,发送网络请求,同时需要 redis 调用多次 read () 和 write () 系统方法,系统方法会将数据从用户态转移到内核态,这样就会对进程上下文有比较大的影响了,性能不太好o(╥﹏╥)o

image-20230807140057973

# 简介

通过批处理 Redis 命令优化往返时间 RTT

Redis 管道 (pipeline):为了优化 RTT 往返时间,可以一次性打包发送多条命令给服务端,而无需等待对每个命令的响应。服务端依次处理完完毕后,通过一条响应一次性将结果返回,通过减少客户端与 redis 的通信次数来实现降低往返延时时间。pipeline 的实现原理是队列,先进先出特性就保证数据的顺序性。

一种批处理命令的变种优化措施,类似 Redis 原生的批命令(例如 mget 和 mset)。

# 案例

  1. 将欲执行的命令全部写到一个 txt 文件中
  2. 将 txt 文件的内容传递给 Redis 的 pipe 参数

image-20230807140935362

# 总结

# 管道 vs 原生批量命令

原生批量命令管道
原子性非原子性
一次只能执行一种命令支持批量执行不同命令
服务端实现服务端与客户端共同完成

# 管道 vs 事务

事务管道
不保证原子性非原子性
逐条发送命令一次性发送多条命令
会阻塞其他命令的执行非阻塞

# 使用管道的注意事项

  • pipeline 缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令

    与 Redis 事务发生命令的运行时异常类似,冤头债主,不会连坐

  • 使用 pipeline 组装的命令个数不能太多(例如 10k),不然数据量过大客户端阻塞的时间可能过久,同时服务端此时也被迫回复一个队列答复,占用很多内存

# 7、Redis 发布订阅(pub/sub)

这是 Redis 的第一代消息中间件,第二代是 Stream,然而一般使用的都是更加成熟的第三方消息中间件

了解即可,实际工作中用的很少,一般都是将 Redis 用作分布式缓存

# 简介

Redis 发布订阅(pub/sub)是一种消息通信模式:发送者 (PUBLISH) 发送消息,订阅者 (SUBSCRIBE) 接收消息,可以实现进程间的消息传递。

一言蔽之:Redis 可以通过发布订阅实现消息的引导和分流,实现消息中间件 MQ 的功能。但是不推荐使用该功能,专业的事情交给专业的中间件处理,redis 就做好分布式缓存功能。

image-20230807173639960

Redis客户端可以订阅任意数量的频道,类似微信关注多个公众号

image-20230807173711847

当有新消息通过PUBLISH命令发送给频道时

小结:发布 / 订阅其实是一个轻量的队列,只不过数据不会被持久化,一般用来处理实时性较高的异步消息

image-20230807173943443

# 常用命令

image-20230807174014734

# 缺点

  • 发布的消息在 Redis 系统中不能持久化,因此,必须先执行订阅,再等待消息发布。如果先发布了消息,那么该消息由于没有订阅者,消息将被直接丢弃

  • 消息只管发送对于发布者而言消息是即发即失的,不管接收,也没有 ACK 机制,无法保证消息的消费成功。

  • 以上的缺点导致 Redis 的 Pub/Sub 模式就像个小玩具,在生产环境中几乎无用武之地。

    为此 Redis5.0 版本新增了 Stream 数据结构,不但支持多播,还支持数据持久化,相比 Pub/Sub 更加的强大,但是也不推荐使用。

# 8、Redis 主从复制(replica)

承上启下的一章,前文都是在单机场景下,从此开始介绍 Redis 多台机器的场景,即通过主从复制支持多可用性和故障切换

# 简介

Redis 数据库的主从复制,其中 master 数据库以写为主,slave 数据库以读为主

当 master 数据库上的数据变化时,会自动将新的数据以异步的方式同步到其他 slave 数据库上。

Redis 主从复制(replica)的功能如下:

  • 读写分离
  • 容灾恢复
  • 数据备份
  • 水平扩容支撑高并发

配置方法:配从不配主

  • master 如果配置了 requirepass 参数,需要密码登陆

  • 那么 slave 就要配置 masterauth 来设置校验密码,否则 master 会拒绝 slave 的访问请求

    image-20230807183910365

# 基本命令

  • INFO replication :以一种易于理解和阅读的格式,返回关于当前 Redis 服务器的直接主 / 从复制信息

  • REPLICAOF masterIp masterPort :在线修改当前 Redis 服务器的主 / 从复制设置(自动配置)

    一般写入进 redis.conf 配置文件内

  • SLAVEOF masterIp masterPort :将当前 Redis 服务器转变为指定服务器的从属服务器(手动配置)

    • 每次与 master 断开之后,都需要重新连接,除非你配置进 redis.conf 文件
    • 在运行期间修改 slave 节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系转而和新的主数据库同步,改换门庭
  • SLAVEOF NO ONE :将使得这个从属服务器关闭复制功能,并从从属服务器转回主服务器,自立为王,同时原来同步所得的数据集不会被丢弃。

# 案例演示

# 架构说明

一主二从,一个 master,两个 slave,示意图如下:

image-20230807185141276

拷贝多份配置文件,分别命名为:

  • redis6379.conf
  • redis6380.conf
  • redis6381.conf

# 口诀

面试重点

前提:三边网络互相 ping 通,同时注意防火墙配置。

三大命令:

  • 主从复制: REPLICAOF masterIp masterPort ,配从不配主
  • 改换门庭: SLAVEOF masterIp masterPort
  • 自立为王: SLAVEOF NO ONE

# 修改配置文件的操作细节

以 redis6379.conf 为例,步骤如下:

  1. 要求 Redis 后台运行,不要弹出命令行窗口: daemonize yes

  2. 取消 IP 的绑定,否则影响远程 IP 连接,注释掉 bind 127.0.0.1

  3. 关闭保护模式,否则影响远程访问 / 连接: protected-mode no

  4. 指定端口: port 6379

  5. 指定当前工作目录, dir /myredis

  6. 设置 pid(进程 id)文件的路径和名字: pidfile /var/run/redis_6379.pid

  7. 设置 log 文件的路径和名字: logfile "/myredis/6379.log"

  8. 设置 Redis 服务器的密码requirepass 111111

    master、slave 均配置

  9. 设置 rdb 文件的名称: dbfilename dump6379.rdb

  10. 若开启 AOF,还需设置 aof 文件的名字:appendfilename 。这里不开启了。

  11. slaveslave 设置所访问的 mastermaster 的 IP 和端口: replicaof masterIp 6379 ,并设置通行密码 masterauth "111111"

    slave 需要配置

# 常用的 3 招

# 一主二从

1 个 master,2 个 slave

image-20230807201752116

# 方案 1:配置文件固定写死
  1. 配从(6380 和 6381)不配主

    image-20230807193239828

  2. 依次启动 master 和两台 slave

    image-20230807193418057

  3. 查看主从关系

    1. 通过日志文件:通过 vim 6379.log 查看 master 日志,通过 vim 6380/6381.log 查看 slave 日志

      image-20230807193701621

      master日志

      image-20230807193733743

      slave日志
    2. 通过命令: info relication

      image-20230807193843707

      master的主从复制信息

      image-20230807193915854

      slave的主从复制信息
# 主从复制问题演示
  • 问题 1:slave 不能执行写命令!

    image-20230807194936720

  • 问题 2:slave 切入点问题。当某台 slave shutdown 并重启后,slave 对 master 首次进行全量复制,然后进行增量复制

    image-20230807194924560

  • 问题 3:master shutdown 后,slave 原地待命,数据仍可以正常使用,slave 等待 master 重启归来

  • 问题 4:shutdown 后的 master 重启归来,主从关系还在!slave 还能顺利复制!

# 方案 2:命令操作手动指定
  1. slave 停机并去掉配置项,清空主从关系。此时 3 机都是 master,互不从属。

  2. 在预设的 2 个 slave 上执行命令 SLAVEOF masterIp masterHost 指定 master

这种情况下,若 slave shutdown 并重启,主从关系就不存在了(因为没有设置配置文件)!

image-20230807200155294

# 配置 vs 命令

配置(即方案 1)持久稳定,命令(即方案 2)临时生效

# 薪火相传

image-20230807202217260

要点:

  • slave(6380)也可以作为 master(6379),接收其他 slave(6381)的连接和同步请求,可以有效减轻主 master 的写压力
  • 改变 master 的命令: SLAVEOF newMasterIp newMasterPort
  • slave(6380)仍然无法执行写命令!
  • slave(6381)中途变更转向,master 从 6379 变为 6380,会清除之前 master(6379)的数据,重新建立拷贝新的 master(6380)的数据
# 自立为王

slave 转成 master

命令 SLAVEOF NO ONE停止与其他数据库的同步,清空数据,转成主数据库

# 主从复制的原理、工作流程

面试重点

  1. slave 首次连接,请求完全同步:slave首次连接master 后会发送一个 sync 命令,请求完全同步(全量复制)

    执行一次完全同步(全量复制),slave 自身原有数据会被覆盖清除

  2. master 保存快照、缓存写命令,响应给 slave 进行初始化

    • master 节点收到 sync 命令后会开始在后台保存快照(即 RDB 持久化,主从复制时会触发 RDB),同时缓存所有接收到的写命令,master 节点执行 RDB 持久化完后,master 将 rdb 快照文件和所有缓存的写命令发送到所有 slave,以完成一次完全同步
    • 而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化
  3. 心跳持续,保持通信:master 向 slave 发出 PING 包的周期,默认是 10 秒。

    image-20230807205137957

  4. 进入平稳,增量复制:master 继续将新的所有收集到的写命令自动依次传给 slave,完成同步

  5. slave 下线,重连续传:假设某台 slave 宕机并重启了,master 会检查 backlog 里面的 offset ,master 和 slave 都会保存一个复制的 offset 和一个 masterId, offset 是保存在 backlog 中的。master 只会把已经复制的 offset 后面的数据复制给 slave,类似断点续传

# 主从复制的缺点

面试重点

  • 复制延时,信号衰减

    由于所有的写操作都是先在 Master 上操作,然后同步更新到 Slave 上,所以从 Master 同步到 Slave 机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave 机器数量的增加也会使这个问题更加严重。

    image-20230807210259342

  • **master 挂了咋办?** 默认情况下,不会从 slave 中重选一个 master,岂不是群龙无首?系统会陷入半瘫痪状态(只能读取,不能写入)那客户端的写命令如何执行啊?

    期待有一种高可用的备份、恢复机制,能够从剩下的 slave 中选出一个 master!(无人值守安装:哨兵!

# 9、Redis 哨兵(sentinel)

Redis 为了支持高可用,有 2 套机制:

  • 主从复制(replica)+ 哨兵(sentinel)
  • 集群(cluster)

# 简介

哨兵(sentinel)巡查监控后台 master 是否故障,如果故障了根据 **投票数** 自动将某一个 slave 转换为新 master,继续对外服务。

image-20230808215652689

哨兵的作用:

  • 主从监控:哨兵能监控主从 Redis 库是否正常运行

  • 故障转移:如果 master 异常,哨兵会将根据投票数将某个 slave 转为新的 master,即主从切换

  • 消息通知:哨兵可将故障转移的结果发送给客户端

  • 配置中心:客户端通过连接哨兵来获得当前 Redis 服务的 master 地址

# 案例演示

# 架构说明

3 个哨兵:自动监控和维护集群,不存放数据

哨兵必须要配置集群,且数量最好是奇数,方便投票。

3 个 Redis 库(1 主 2 从):用于数据读取和存放

image-20230808002037403

# 哨兵配置文件 ( sentinel.conf )

默认在 /opt/redis-7.0.0 目录下

重点参数说明:

  • bind :服务监听地址,用于客户端连接,默认为本机地址

  • daemonize :是否以后台 daemon(后台进程)方式运行,设为 yes

  • protected-mode :是否开启安全保护模式,设为 no,否则影响远程访问 / 连接

  • port :端口,默认是 26379

  • logfile :日志文件路径

  • pidfile :pid 文件路径

  • dir :工作目录

  • sentinel monitor <master-name> <master-ip> <master-port> <quorum> :设置哨兵要监控的 master

    • quorum确认客观下线的最少哨兵数量,同意故障迁移的法定投票数

      sentinel 通过定时向 master 发出 PING 包来确认 master 是否挂掉。

      但网络是不可靠的,有时某个 sentinel 可能因为网络拥堵没收到 master 的响应,从而误以为 master 已挂掉。因此需要多个 sentinel 都一致任务 master 已挂,才可进行主从切换、故障转移,保证了公平性和高可用。

  • sentinel auth-pass <master-name> <password> :设置连接 master 服务器的密码

  • sentinel down-after-milliseconds <master-name> <milliseconds> :指定如果 master 在多少毫秒之后没有应答 sentinel,sentinel 则主观上认为 master 下线(主观下线

  • sentinel parallel-syncs <master-name> <nums> :表示允许并行同步的 slave 个数,当 master 挂了后,哨兵会选出新的 master,此时,剩余的 slave 会向新的 master 发起同步数据

  • sentinel failover-timeout <master-name> <milliseconds> :故障转移的超时时间,进行故障转移时,如果超过设置的毫秒,表示故障转移失败

  • sentinel notification-script <master-name> <script-path> :配置当某一事件发生时所需要执行的脚本

  • sentinel client-reconfig-script <master-name> <script-path> :客户端重新配置 master 参数脚本

# 本次案例中 sentinel.conf 的通用配置

image-20230808105627695

由于机器硬件关系,我们的 3 个哨兵都同时配置进 192.168.111.169 同一台机器,即3 个哨兵和 master 在一台机器上

配置这 3 个哨兵的配置文件:

image-20230808104641977

image-20230808104655030

image-20230808104707250

master 配置文件说明:

image-20230808104812771

# 先测试正常的主从复制(一主二从)

image-20230807201752116

  1. 169 机器上新建 redis6379.conf 配置文件,由于 6379 后续可能会变成从机,需要设置访问新主机的密码, 请设置 masterauth 项访问密码为 111111,不然后续可能报错 master_link_status:down
  2. 172 机器上新建 redis6380.conf 配置文件,设置好 replicaof \<masterip> \<masterport> ,以及 masterauth 项访问密码为 111111
  3. 173 机器上新建 redis6381.conf 配置文件,设置好 replicaof \<masterip> \<masterport> ,以及 masterauth 项访问密码为 111111
  4. 启动 3 台机器实例:
    1. redis-cli -a 111111 -p 6379
    2. redis-cli -a 111111 -p 6380
    3. redis-cli -a 111111 -p 6381
  5. 测试

# 哨兵来了!

sentinel 之间通过 master 来获取:

  • slave 信息
  • 其他 sentinel 信息

从而实现通信。

  1. 在 master(6379)这台机器上启动 3 个 sentinel(26379/26380/26381),完成监控

    1. redis-sentinel sentinel26379.conf --sentinel
    2. redis-sentinel sentinel26380.conf --sentinel
    3. redis-sentinel sentinel26381.conf --sentinel

    image-20230808110202022

    image-20230808110255818

  2. 查看哨兵的日志文件 sentinel26379.log ,可以看到当前 sentinel 的信息所监控 master 以及 slave 的信息其他 sentinel 的信息

    image-20230808111127304

  3. 再测试一次主从复制,木有问题

# master 挂了!

通过命令 SHUTDOWN 手动关闭 6379 服务器,模拟 master 挂掉。

思考以下问题:

  • 问题 1:两台 slave 上的数据还 OK!

  • 问题 2:** 会从这两台 slave 上选出新的 master!** 具体信息可查看 sentinel 的 log 文件。

    在此过程中,哨兵配置文件 sentinel.conf 中会自动生成内容信息

  • 问题 3:down 机的旧 master 重启归来,也只能拜认新 master,作它的 slave!

在 master6379 宕机后,会出现两种错误:

  • Error:Server closed the connection

  • Error:Broken pipe

    broken pipe:pipe 是管道的意思,管道里面是数据流,通常是从文件或网络套接字读取的数据。当该管道从另一端突然关闭时,会发生数据突然中断,即是 broken,对于 socket 来说,可能是网络被拔出或另一端的进程崩溃。

    如何解决:当该异常产生的时候,对于服务端来说,并没有多少影响。因为可能是某个客户端突然中止了进程导致了该错误。

    总结:这个异常是客户端读取超时关闭了连接,这时候服务器端再向客户端已经断开的连接写数据时就发生了 broken pipe 异常!

    image-20230808113740061

针对本次案例,分析谁是 master:

  1. 6381 被选为新 master,上位成功
  2. 以前的 6379 从 master 降级变成了 slave
  3. 6380 还是 slave,只不过换了个新老大 6381 (6379 变 6381),6380 还是 slave

# 对比新老 master 的配置文件

旧 master redis6379.conf 中会自动生成以下内容,让 6379 去做 6381 的 slave:

image-20230808115231877

新 master redis6381.conf 中:

  • 自动删掉 replicaof 参数的配置
  • 自动生成以下内容:

结论:

  • conf 文件的内容会被 sentinel 动态更改
  • Master-Slave 切换后,master_redis.conf、slave_redis.conf 和 sentinel.conf 的内容都会发生改变,即master_redis.conf 中会多一行 slaveof 的配置sentinel.conf 的监控目标会随之调换

# 其他备注

  • 生产都是不同机房不同服务器,很少出现哨兵全挂掉的情况
  • 可以同时监控多个 master,一行一个

# 哨兵运行流程、选举原理

面试重点

当一个主从配置中的 master 失效之后,sentinel 可以从 slave 中选举出一个新的 master,用于接替原 master 的工作。

主从配置中的其他 redis 服务器自动指向新的 master 同步数据

一般建议 sentinel 采取奇数台,一是防止某一台 sentinel 无法连接到 master 导致误切换,二是利于投票选举。

故障切换的流程:

  1. 3 个 sentinel 监控一 master 二 slave,正常运行中

    image-20230808161905591

  2. SDown 主观下线(Subjectively Down):指的是单个 Sentinel 实例对 master 服务器做出的下线判断(有可能是接收不到订阅,之间的网络不通等等原因)。如果 master 服务器在 [ sentinel down-after-milliseconds ] 给定的毫秒数之内没有回应 PING 命令或者返回一个错误消息,那么这个 Sentinel 会主观的 (单方面的) 认为这个 master 不可以用了。

    sentinel 配置文件中的 sentinel down-after-milliseconds <masterName> <timeout> 设置了判断主观下线的时间长度,表示 master 被当前 sentinel 实例认定为失效的间隔时间。

    image-20230808162539926

  3. ODown 客观下线(Objectively Down):需要一定数量的 sentinel,多个哨兵达成一致意见才能认为一个 master 客观上已经宕掉。

    image-20230808162827271

    • master-name 是对某个 master+slave 组合的一个区分标识 (一套 sentinel 可以监听多组 master+slave 这样的组合)
    • quorum 这个参数是进行客观下线的一个依据,即法定人数 / 法定票数。意思是至少有 quorum 个 sentinel 认为这个 master 有故障才会对这个 master 进行下线以及故障转移。因为有的时候,某个 sentinel 节点可能因为自身网络原因导致无法连接 master,而此时 master 并没有出现故障,所以这就需要多个 sentinel 都一致认为该 master 有问题,才可以进行下一步操作,这就保证了公平性和高可用。
  4. 从哨兵中选出兵王:当 master 被判断 ODown 以后,各个 sentinel 节点会进行协商,先通过Raft 算法选举出一个兵王,由它进行 failover (故障迁移)

    监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是 Raft 算法,其基本思路是 **先到先得**:即在一轮选举中,哨兵 A 向 B 发送成为领导者的申请,如果 B 没有同意过其他哨兵,则会同意 A 成为领导者。

    image-20230808164314705

    从三个 sentinel 实例的 log 文件中可以看见兵王的诞生过程以及兵王执行故障迁移的过程:

    image-20230808163922904

    sentinel26379.log

    image-20230808163958440

    sentinel26380.log

    image-20230808164037410

    sentinel26381.log
  5. 兵王开始故障切换,选举新的 master

    1. 新主登基:**新 master 选举算法** 如下:

      1. 优先级高:所有 slave 中,根据 redis.conf 配置文件中的优先级 slave-priority 或者 replica-priority ,选择优先级最高的 slave 作为新 master。

        数字越小优先级越高

        image-20230808170206385

      2. 复制偏移大:所有 slave 中,根据复制偏移位置 offset ,该值最大的 slave 作为新 master。

      3. Run ID 小:所有 slave 中,选择 Run ID 最小的 slave 作为新 master,是按照字典顺序,ASCII 码。

      image-20230808165640759

    2. 群臣俯首:一朝天子一朝臣,换个码头重新拜

      1. Sentinel leader 会对选举出的 slave 执行 SLAVEOF NO ONE 命令,将其提拔为新 master
      2. Sentinel leader 向其余 slave 发送 SLAVEOF 命令,使它们成为新 master 的 slave
    3. 旧主拜服:老 master 回来也认怂

      1. 老 master 成为新 master 的 slave
      2. Sentinel leader 会让老 master 降级为 slave,并恢复正常工作

    总结:上述 failover(故障迁移)均由 sentinel 独自完成,无需人工干预,因此称之为无人值守安装

# 哨兵使用建议

  1. 哨兵的数量应为多个且奇数。哨兵本身应该集群,保证高可用。

  2. 各个哨兵的配置应一致

  3. 如果哨兵部署在 Docker 等容器里面,尤其要注意端口的正确映射

  4. 主从复制 + 哨兵 机制并不能确保数据零丢失。因为从 master 挂掉到选举出新 master 的这段时间内,无法执行写命令!

    引出集群

# 10、Redis 集群(cluster)

我尼玛又白雪,集群才是 yyds!

# 简介

由于数据量过大单个 Master 复制集难以承担,因此需要对多个复制集进行集群,形成水平扩展每个复制集只负责存储整个数据集的一部分,这就是 Redis 的集群。

image-20230808220609079

总之,Redis 集群是一个提供在多个 Redis 节点间共享数据的程序集。其功能总结如下:

  • 支持多个 Master,每个 Master 又可以挂载多个 Slave。
    • 读写分离
    • 支持数据的高可用
    • 支持海量数据的读写存储操作
  • 自带 failover(故障转移)机制,内置了高可用的支持,无需再去使用哨兵功能
  • 客户端只需连接集群中的任意一个可用 Master 节点即可,不需要连接集群中的所有 Master 节点。
  • 槽位 slot 负责分配到各个物理服务节点,由对应的集群来负责维护 Redis 节点、插槽、数据之间的关系

# 集群算法、分片、槽位 slot

# 官网介绍

Redis 集群的 key 空间被划分为 16384 个插槽 slot,有效地设置了 16384 个 master 节点的集群大小上限(然而,master 节点的最大数量建议为 1000)。

插槽,也称哈希槽

集群中的每个 master 节点处理 16384 个哈希槽的子集。当没有正在进行的集群重新配置时(即哈希槽从一个节点移动到另一个节点),集群是稳定的。当集群稳定时,单个哈希槽将由单个节点提供服务(但是,服务节点可以有一个或多个副本,在网络分裂或故障的情况下,这些副本将替换它,并且可以用于扩展读取过时数据的读取操作)。

用于将 key 映射到哈希槽的基本算法如下(请阅读下一段以了解此规则的哈希标记异常):

HASH_SLOT = CRC16(key) mod 16384

# 插槽

Redis 集群没有使用一致性哈希算法,而是引入了 hash槽 的概念。
Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后,再对 16384 取模来决定放置哪个槽。集群的每个 Redis 节点负责一部分 hash 槽

举个例子,比如当前集群有 3 个节点,那么:

image-20230809002715513

# 数据分片

数据分片 :Redis 集群中会将存储的数据分散到多台 redis 机器上。每个 Redis 实例都被认为是整个数据的一个分片。

如何找到给定 key 的分片?

  1. 对 key 进行CRC16(key)算法处理,并通过对总分片数量取模
  2. 然后,使用确定性哈希函数,这意味着 **给定的 key 将始终映射到同一个分片**,我们可以推断将来读取特定 key 的位置。

# 分片 + 插槽的优点

  • 方便 Redis 节点的扩容和缩容

    • 添加 Redis 节点:比如我想新添加个节点 D,我需要从节点 A,B,C 中移动部分槽到 D 上。
    • 删除 Redis 节点:如果我想移除节点 A,需要将 A 中的槽移到 B 和 C 节点上,然后将没有任何槽的 A 节点从集群中移除即可。
    • 由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除节点,或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态
  • 方便数据的分派和查找

# 槽位映射的 3 种方案

# 哈希取余分区

小厂

image-20230809004525207

假设有 N 台机器构成一个集群,用户每次对 key 的读写操作都是根据公式:

hash(key) % N

计算出哈希值,用来决定数据映射到哪一个节点上。

优点

  • 简单有效。只需要预估好数据规模,规划好节点,就能保证一段时间的数据支撑。
  • 负载均衡。使用 Hash 算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息)。

缺点

  • Redis 节点的扩容 / 缩容麻烦。如果需要弹性扩容或故障停机,导致节点有变动,映射关系需要重新进行计算。原来的取模公式就会发生变化: Hash(key)/3 会变成 Hash(key) /? 。此时地址经过取余运算的结果将发生很大变化,根据公式获取的服务器也会变得不可控
  • 某个 Redis 机器宕机了,由于台数数量变化,会导致 hash 取余全部数据重新洗牌。
# 一致性哈希算法分区

一致性:意味着取余的分母是固定的。

设计目标:为了解决分布式缓存数据变动和映射问题,某个机器宕机了,分母数量改变了,自然取余数不 OK 了。目的是当 Redis 服务器个数发生变动时,尽量减少客户端到服务器的映射关系的影响

3 大步骤

  1. 构建一致性哈希环

    一致性哈希算法必然有个 hash 函数用于产生 hash 值,这个算法的所有可能哈希值会构成一个全量集,这个集合可以成为一个 **hash 空间 [0,232-1],这个是一个线性空间,但是在算法中,我们通过适当的逻辑控制将它首尾相连 (0 = 232)**, 这样让它形成了一个逻辑上的环形空间

    它也是按照使用取模的方法,前面介绍的是对 Redis 节点的数量进行取模。而 **一致性 Hash 算法是对 232 取模**。

    简单来说,一致性 Hash 算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数的值空间为 [0,232-1](即哈希值是一个 32 位无符号整形),整个哈希环如下图:整个空间按顺时针方向组织,圆环的正上方的点代表 0,0 点右侧的第一个点代表 1,以此类推,2、3、4、…… 直到 232-1,也就是说0 点左侧的第一个点代表 232-1, 0 和 232-1 在零点中方向重合,我们把这个 **由 232 个点组成** 的圆环称为 Hash环

    image-20230809010605612

  2. Redis 服务器节点 IP 映射

    将集群中各个 Redis 节点的 IP 映射到环上的某一个位置。

    将各个 Redis 服务器的 IP 或主机名作为关键字使用 Hash 进行哈希,这样每台机器就能确定其在哈希环上的位置。假如 4 个 Redis 节点 NodeA、NodeB、NodeC、NodeD,经过IP 地址的哈希函数计算 hash (ip),使用 IP 地址哈希后在环空间的位置如下:

    image-20230809114247849

  3. 落 key 规则

    当我们需要存储一个键值对时,首先计算 key 的 hash 值,hash (key),确定此数据在环上的位置,从此位置沿环 **顺时针**“行走”,第一台遇到的 Redis 服务器就是其应该定位到的服务器,并将该键值对存储在该节点上。

    如我们有 Object A、Object B、Object C、Object D 四个数据对象,经过哈希计算后,在环空间上的位置如下:根据一致性 Hash 算法,Object A 会被定为到 Node A 上,Object B 被定为到 Node B 上,Object C 被定为到 Node C 上,Object D 被定为到 Node D 上。

    image-20230809114625589

优点

  • 容错性

    假设 Node C 宕机,可以看到此时对象 A、B、D 不会受到影响。一般的,在一致性 Hash 算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响。简单说,就是 C 挂了,受到影响的只是 B、C 之间的数据,且这些数据会转移到 D 进行存储

    image-20230809115305990

  • 扩展性

    随着数据量的增加,需要增加一台节点 NodeX,位置在 A 和 B 之间,那受到影响的也就是 A 到 X 之间的数据,重新把 A 到 X 的数据录入到 X 上即可,不会导致 hash 取余全部数据重新洗牌

    image-20230809120153493

缺点数据倾斜问题

Redis 服务节点太少时,容易因为节点分布不均匀而造成数据倾斜(被缓存的数据对象大部分集中缓存在某一台服务器上)问题。

例如系统中只有两台服务器:

image-20230809120432504

小结

  • 设计目标:在 Redis 节点的数目发生改变时,尽可能地减少数据的迁移
  • 设计思路:将所有的 Redis 节点排列在首尾相接的 Hash 环上,每个 key 在计算 Hash 后会顺时针找到临近的 Redis 节点存放。而当有 Redis 节点加入或退出时仅影响该节点在 Hash 环上顺时针相邻的后续节点
  • 优点:加入和删除节点只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响。
  • 缺点:数据的分布和节点的位置有关,因为这些节点不是均匀的分布在哈希环上的,所以数据在进行存储时达不到均匀分布的效果。
# 哈希槽分区 (√)

大厂

为什么出现:因为一致性哈希算法具有数据倾斜的问题。

哈希槽是什么:哈希槽实质是一个数组哈希槽空间为 [0,214-1]

214=16384

能干嘛

解决数据分配不均匀的问题,在数据和节点之间又加入了一层,把这层称为 哈希槽(slot) ,用于管理数据和节点之间的关系,现在就相当于节点上放的是槽,槽里放的是数据。

image-20230809130342790

槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动

哈希解决的是映射问题,使用 key 的哈希值来计算所在的槽,便于数据分配

哈希槽的个数

一个集群只能有 16384 个哈希槽,编号 0-16383(0-214-1)。这些槽会分配给集群中的所有 master 节点,分配策略没有要求。

集群会记录 Redis 节点和槽的对应关系,解决了节点和槽的关系后,接下来就需要对 key 求哈希值,然后对 16384 取模,余数是几 key 就落入对应的槽里。 HASH_SLOT = CRC16(key) mod 16384以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。

哈希槽计算

Redis 集群中内置了 16384 个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。当需要在 Redis 集群中放置一个 key-value 时,redis 先对 key 使用 crc16 算法算出一个结果然后用结果对 16384 求余数 [ CRC16(key) % 16384 ],这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,也就是映射到某个节点上。

如下代码,key 之 A 、B 在 Node2, key 之 C 落在 Node3 上:

image-20230809131108267

# 经典面试题:为什么 Redis 集群的最大哈希槽数目是 16384 个?

CRC16 算法产生的哈希值有 16bit,即 216=65536 个值,为什么 Redis 集群的算法只采用 214=16384 个哈希槽?在进行 mode 运算时,为什么是 HASH_SLOT = CRC16(key) mod 16384 而不是 HASH_SLOT = CRC16(key) mod 65536

作者的回复

image-20230809132510614

消息头 clusterMsg 的结构

image-20230809132604570

标准回答

  • 如果槽位为 65536,发送心跳信息的消息头达 8k,发送的心跳包过于庞大

    在消息头中最占空间的是 myslots[CLUSTER_SLOTS/8] :

    • 当槽位为 65536 时,这块的大小是: 65536÷8÷1024=8kb
    • 当槽位为 16384 时,这块的大小是: 16384÷8÷1024=2kb

    因为每秒钟 redis 节点需要发送一定数量的 ping 消息作为心跳包,如果槽位为 65536,这个ping 消息的消息头太大了,浪费带宽

  • 对于基本不可能超过 1000 个 master 节点数量的 redis 集群而言,16384 个槽位就已经够用了

    集群的节点越多,心跳包的消息体内携带的数据越多。如果节点过 1000 个,也会导致网络拥堵。因此 redis 作者不建议 redis cluster 节点数量超过 1000 个。那么,对于节点数在 1000 以内的 redis cluster 集群,16384 个槽位够用了。没有必要拓展到 65536 个。

  • 槽位越小,节点少的情况下,压缩比高,容易传输

    Redis 的 master 节点的配置信息中它所负责的哈希槽是通过一张 bitmap 的形式来保存的,在传输过程中会对 bitmap 进行压缩,但是如果 bitmap 的填充率 slots / N 很高的话 (N 表示节点数),bitmap 的压缩率就很低。如果节点数很少,而哈希槽数量很多的话,bitmap 的压缩率就很低。

# Redis 集群不保证强一致性

Redis 集群不保证强一致性,这意味着在特定的条件下,Redis 集群可能会丢掉一些被系统收到的写入请求命令

Redis 集群使用节点之间的异步复制,最后一次故障切换隐式合并功能。这意味着最后一次选择的主数据会完全替换所有其他副本。在分区期间,总是有一个可能丢失写入的时间窗口。然而,在连接到大多数主数据的客户端的情况下,这些窗口非常不同,以及与少数 master 有联系的客户。

# 案例演示

# 3 主 3 从 redis 集群配置

在 3 台虚拟机上新建 6 个独立的 Redis 实例服务,每台机器上一主一从,设计图如下:

IMG_8445(20230809-135635)

配置这 6 个 Redis 实例的conf 文件,下面以 6381 为例:

image-20230809135942477

启动6 台 Redis 实例,以 6381 为例: redis-server /myredis/cluster/redisCluster6381.conf

构建 6 个 Redis 实例的集群关系,命令如下:

redis-cli -a 111111
--cluster create --cluster-replicas 1
192.168.111.175:6381 192.168.111.175:6382
192.168.111.172:6383 192.168.111.172:6384
192.168.111.174:6385 192.168.111.174:6386

--cluster-replicas 1 表示为每个 master 创建一个 slave 节点,主从的实际分配是随机的!

image-20230809141622996

image-20230809141121945

image-20230809141157047

启动 6381,查看 6381 的主从复制信息 info replication

image-20230809141652111

查看 6381 的集群信息 cluster info

image-20230809141857437

再查看集群的节点信息 cluster nodes

image-20230809141717499

slave 后跟着 master 信息,而 master 后没有 slave 信息。

目前的主从关系:

6381 的 slave 是 6384,6383 的 slave 是 6386,6385 的 slave 是 6382。

# 3 主 3 从 redis 集群读写

redis-cli -a 111111 -p 6381 启动 master 6381,并新增 2 个 key:

image-20230809143452632

在设置 k1 时遇到报错,提示 k1 对应的哈希槽是 12706,应该存到 master 6385 上。因此,要注意槽位的范围区间,需要将 key 路由到正确的槽位上

解决方法:** 启动 Redis 实例时添加 -c 参数,表示以集群模式运行,防止路由失效。** 即 redis-cli -a 111111 -p 6381 -c

image-20230809144237468

此时 key 会自动重定向到对应 Redis 实例的哈希槽上。

查看某个 key 对应的槽位置CLUSTER KEYSLOT key

# 主从容错切换迁移 (failover)

image-20230809144752893

  1. 假如 master 6381 宕机,其对应的 slave 6384 会上位成为新的 master

    image-20230809145659230

  2. 当 6381 重启恢复,自动成为 master 6384 的 slave

    image-20230809145905827

# 手动切换主从 / 调整节点从属关系

上面一换后 6381、6384 主从对调了,和原始设计图不一样了,该如何恢复原来的主从关系?

重启 6381,执行命令 CLUSTER FAILOVER ,自动调整 6381 的主从关系

image-20230809150634974

# 主从扩容

三主三从 -> 四主四从

  1. 新 master 加入集群
  2. 重新分配槽号(reshard)
  3. 为新 master 分配 slave

image-20230809151024511

思考问题:

  • 如何将新机加入原有集群中?
  • 新机的槽位如何分配?重新洗牌!
  1. 配置 2 台新机的 redis.conf 文件,以 6387 为例:

    image-20230809151526914

  2. 启动 2 台新机,此时它们都是 master:

    image-20230809151554375

  3. 将新增的 6387(空槽号)作为master 节点加入原有集群,执行命令:

    redis-cli -a 111111
    --cluster add-node
    192.168.111.174:6387
    192.168.111.175:6381

    • 6387 就是将要作为master 新增节点

    • 6381 就是原来集群节点里面的领路人,相当于 6387 拜拜 6381 的码头从而找到组织加入集群

    image-20230809152412183

  4. 检查集群情况,执行命令:

    redis-cli -a 111111
    --cluster check
    192.168.111.175:6381

    image-20230809152820379

    此时新加入的 6387 节点还没分配哈希槽

  5. 重新分配槽号,执行命令:

    redis-cli -a 密码
    --cluster reshard
    192.168.111.175:6381

    image-20230809153203484

    image-20230809153429470

  6. 再次检查集群情况,执行命令:

    redis-cli -a 111111
    --cluster check
    192.168.111.175:6381

    image-20230809153519681

    此时槽号重新分派完成!但是为什么 6387 是 3 个新的区间,以前的还是连续?

    重新分配的成本太高,所以之前的 Redis 节点各自匀出来一部分给新节点。从 6381/6383/6385 三个旧节点分别匀出 1364 个坑位给新节点 6387。

  7. 为 master 6387分配 slave 6388,执行命令:

    redis-cli -a 密码
    --cluster add-node
    ip: 新 slave 端口 ip: 新 master 端口
    --cluster-slave --cluster-master-id 新 master 节点 ID

    redis-cli -a 111111
    --cluster add-node
    192.168.111.174:6388 192.168.111.174:6387
    --cluster-slave --cluster-master-id 4feb6a7ee0ed2b39ff86474cf4189ab2a554a40f

    image-20230809154138459

  8. 第三次检查集群情况:

    image-20230809154202088

至此,完成 Redis 集群中的主从扩容:

image-20230809154357068

# 主从缩容

四主四从 -> 三主三从

目的:让 6387、6388 下线

image-20230809160221198

image-20230809155155205

  1. 检查集群情况,获取 slave 6388 的节点 ID:

    redis-cli -a 密码
    --cluster check
    192.168.111.174:6388

    image-20230809155313502

  2. 从集群中将 slave 6388 删除

    redis-cli -a 111111
    --cluster del-node
    192.168.111.174:6388
    218e7b8b4f81be54ff173e4776b4f4faaf7c13da

    image-20230809155525228

  3. 检查集群情况,发现 slave 6388 被成功删除:

    image-20230809155602447

  4. 将 master 6387 的槽号清空,本例中将其重新分派给 master 6381:

    redis-cli -a 111111
    --cluster reshard
    192.168.111.175:6381

    image-20230809155839782

    image-20230809155856778

  5. 检查集群情况:

    image-20230809155927823

    发现:

    • master 6387 的 4096 个槽位都指给了 master 6381(变成了 8192 个槽位)
    • master 6387 变成了 master 6381 的 slave
  6. 删除 6387:

    image-20230809160629562

  7. 检查集群情况:

    image-20230809160704133

    此时 6387/6388 已从集群中移除

  8. 此时,若再想在 6387 上写数据,会报错:

    image-20230809160827236

# 通识占位符

痛点:不在同一个 slot 槽位下的批操作命令(多键操作)支持不好。

image-20230809161249034

可以通过 通识占位符{} 来定义同一个 slot 槽位的概念,使 key 中{} 内相同内容的键值对放到一个 slot 槽位去,对照下图类似 k1、k2、k3 都映射为 x,自然槽位一样:

image-20230809161554065

# CRC16 算法分析

Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽。集群的每个节点负责一部分 hash 槽。

下面浅析 CRC16 算法的源码,源码文件是 cluster.c

image-20230809161758328

# 集群中的常用配置与命令

  • 配置参数 cluster-require-full-coverage集群是否完整时才能对外提供服务,默认为 yes。

    image-20230809162230541

    现在集群架构是 3 主 3 从,由 3 个 master 平分 16384 个 slot,每个 master 的小集群负责 1/3 的 slot,对应一部分数据。

    通常情况,如果这 3 个小集群中,任何一个(1 主 1 从)挂了,你这个集群对外可提供的数据只有 2/3 了,整个集群是不完整的,redis 默认在这种情况下,是不会对外提供服务的。

  • 命令 CLUSTER COUNTKEYSINSLOT slotindex查看第 slotindex 号槽位上的 key 数量,空则返回 0。

    image-20230809162506376

  • 命令 CLUSTER KEYSLOT key查看 key 应该存放的槽位号

    image-20230809162815558

# 11、SpringBoot 集成 Redis

前面都是通过命令与 Redis 交互,实际生产中更多是通过Java 程序来操作 Redis。

# 整体概述

对比 Jedis、lettuce、RedisTemplate

Java 连接 MySQL 的驱动中间件是 JDBC,那么 Java 连接 Redis 所需要的驱动中间件有哪些呢?

  • Jedis:一代目,老牌,线程池不安全
  • lettuce:二代目,对 Jedis 的优化
  • RedisTemplate:三代目,对 lettuce 进行封装

# 本地 Java 连接 Redis 的常见问题

以下问题可能会导致 Java 程序无法远程连接 Redis:

  • redis.conf 中的 bind 配置请注释掉
  • redis.conf 中的保护模式设置为 no
  • Linux 系统的防火墙设置
  • redis 服务器的 IP 地址和密码是否正确
  • 忘记写访问 redis 的服务端口号和 auth 密码
  • 无脑粘贴脑图笔记......o (...T) o

# 集成 Jedis

Jedis Client 是 Redis 官网推荐的一个面向 java 客户端,库文件实现了对各类 API 进行封装调用。

集成 Jedis 的步骤如下:

约定>配置>编码

  1. 创建 Module

  2. 修改 POM

  3. 写 YML

  4. 主启动

    package com.atguigu.redis7;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    /**
     * @auther zzyy
     * @create 2022-11-17 16:36
     */
    @SpringBootApplication
    public class Redis7Study7777
    {
        public static void main(String[] args)
        {
            SpringApplication.run(Redis7Study7777.class,args);
        }
    }
  5. 业务类

    1. 通过指定 ip 和 port 获得 connection 对象
    2. 指定访问服务器的密码
    3. 得到 Jedis 客户端后,即可访问 redis
    @Slf4j
    public class JedisDemo
    {
        public static void main(String[] args)
        {
            Jedis jedis = new Jedis("192.168.111.185",6379);
            jedis.auth("111111");
            log.info("redis conn status:{}","连接成功");
            log.info("redis ping retvalue:{}",jedis.ping());
            jedis.set("k1","jedis");
            log.info("k1 value:{}",jedis.get("k1"));
        }
    }

# 集成 lettuce

我来人间一趟,本想光芒万丈,奈何 springboot 太强,刚出生就被团灭!

Lettuce 是一个 Redis 的 Java 驱动包,翻译为生菜。

lettuce 与 Jedis 的区别:

  • Jedis 连接 Redis 时,每个线程都要创建 Jedis 实例,开销大

  • Jedis 是线程不安全的,一个线程通过 Jedis 实例更改 Redis 服务器中的数据之后,会影响另一个线程

  • Lettuce 底层使用的是 Netty,当有多个线程都需要连接 Redis 服务器的时候,可以保证只创建一个 Lettuce 连接,使所有的线程共享这一个 Lettuce 连接,这样可以减少创建关闭一个 Lettuce 连接时候的开销。

  • 这种方式也是线程安全的,不会出现一个线程通过 Lettuce 更改 Redis 服务器中的数据之后而影响另一个线程的情况。

  • 因此,在 SpringBoot2.0 之后默认都是使用的 Lettuce

案例:

  1. 修改 POM

  2. 写业务类

    package com.atguigu.redis7.test;
    import io.lettuce.core.RedisClient;
    import io.lettuce.core.RedisFuture;
    import io.lettuce.core.RedisURI;
    import io.lettuce.core.SortArgs;
    import io.lettuce.core.api.StatefulRedisConnection;
    import io.lettuce.core.api.async.RedisAsyncCommands;
    import io.lettuce.core.api.sync.RedisCommands;
    import lombok.extern.slf4j.Slf4j;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.concurrent.ExecutionException;
    /**
     * @auther zzyy
     * @create 2022-11-17 17:05
     */
    @Slf4j
    public class LettuceDemo
    {
        public static void main(String[] args)
        {
            // 使用构建器(链式编程) RedisURI.builder
            RedisURI uri = RedisURI.builder()
                    .redis("192.168.111.181")
                    .withPort(6379)
                    .withAuthentication("default","111111")
                    .build();
            // 创建连接客户端
            RedisClient client = RedisClient.create(uri);
            StatefulRedisConnection conn = client.connect();
            // 操作命令 api
            RedisCommands<String,String> commands = conn.sync();
            //keys
            List<String> list = commands.keys("*");
            for(String s : list) {
                log.info("key:{}",s);
            }
            //String
        commands.set("k1","1111");
        String s1 = commands.get("k1");
        System.out.println("String s ==="+s1);
            //list
        commands.lpush("myList2", "v1","v2","v3");
        List<String> list2 = commands.lrange("myList2", 0, -1);
        for(String s : list2) {
         System.out.println("list ssss==="+s);
        }
        //set
        commands.sadd("mySet2", "v1","v2","v3");
        Set<String> set = commands.smembers("mySet2");
        for(String s : set) {
         System.out.println("set ssss==="+s);
        }
        //hash
        Map<String,String> map = new HashMap<>();
            map.put("k1","138xxxxxxxx");
            map.put("k2","atguigu");
            map.put("k3","zzyybs@126.com");// 课后有问题请给我发邮件
        commands.hmset("myHash2", map);
        Map<String,String> retMap = commands.hgetall("myHash2");
        for(String k : retMap.keySet()) {
         System.out.println("hash  k="+k+" , v=="+retMap.get(k));
        }
        //zset
        commands.zadd("myZset2", 100.0,"s1",110.0,"s2",90.0,"s3");
        List<String> list3 = commands.zrange("myZset2",0,10);
        for(String s : list3) {
         System.out.println("zset ssss==="+s);
        }
        //sort
        SortArgs sortArgs = new SortArgs();
        sortArgs.alpha();
        sortArgs.desc();
        List<String> list4 = commands.sort("myList2",sortArgs);
        for(String s : list4) {
         System.out.println("sort ssss==="+s);
        }
            // 关闭
            conn.close();
            client.shutdown();
        }
    }

# 集成 RedisTemplate(推荐)

# 连接单机

# boot 整合 Redis 基础演示
  1. 创建 Module

  2. 修改 POM: spring-boot-starter-data-redis 包(依赖于 lettuce 包)和 commons-pool2

  3. 写 YML

    ==============redis单机==================
    spring.redis.database=0
    # 修改为自己真实 IP
    spring.redis.host=192.168.111.185
    spring.redis.port=6379
    spring.redis.password=111111
    # lettuce 连接池
    spring.redis.lettuce.pool.max-active=8
    spring.redis.lettuce.pool.max-wait=-1ms
    spring.redis.lettuce.pool.max-idle=8
    spring.redis.lettuce.pool.min-idle=0
  4. 主启动

    @SpringBootApplication
    public class Redis7Study7777
    {
        public static void main(String[] args)
        {
            SpringApplication.run(Redis7Study7777.class,args);
        }
    }
  5. 业务类

    1. 配置类

      1. RedisConfig
      2. SwaggerConfig
    2. service

      @Service
      @Slf4j
      public class OrderService
      {
          public static final String ORDER_KEY = "order:";
          @Resource
          private RedisTemplate redisTemplate;
          public void addOrder()
          {
              int keyId = ThreadLocalRandom.current().nextInt(1000)+1;
              String orderNo = UUID.randomUUID().toString();
              redisTemplate.opsForValue().set(ORDER_KEY+keyId,"京东订单"+ orderNo);
              log.info("=====>编号"+keyId+"的订单流水生成:{}",orderNo);
          }
          public String getOrderById(Integer id)
          {
              return (String)redisTemplate.opsForValue().get(ORDER_KEY + id);
          }
      }
    3. controller

      @Api(tags = "订单接口")
      @RestController
      @Slf4j
      public class OrderController
      {
          @Resource
          private OrderService orderService;
          @ApiOperation("新增订单")
          @RequestMapping(value = "/order/add",method = RequestMethod.POST)
          public void addOrder()
          {
              orderService.addOrder();
          }
          @ApiOperation("按orderId查订单信息")
          @RequestMapping(value = "/order/{id}", method = RequestMethod.GET)
          public String findUserById(@PathVariable Integer id)
          {
              return orderService.getOrderById(id);
          }
      }
  6. 测试

    1. swagger

    2. 序列化问题

      image-20230809211944574

      image-20230809212145565

      image-20230809212339643

      解决方案 1:将 RedisTemplate 对象替换为 StringRedisTemplate 对象。此时除了 Redis 命令行中仍然显示中文乱码外,在 swagger、服务器中的返回值都没有乱码的问题了。

      解决方案 2-1启动 Redis 时添加参数 --raw 解决 Redis 服务器端显示乱码

      解决方案 2-2:看下源码 RedisTemplate # afterPropertiesSet() 发现在默认情况下,RedisTemplate 使用的数据列化方式是 JdkSerializationRedisSerializer ,也就是导致乱码的罪魁祸首!解决方法就是 **编写 RedisConfig 配置类,指定序列化器**!

      @Configuration
      public class RedisConfig
      {
          /**
           * redis 序列化的工具配置类,下面这个请一定开启配置
           * 127.0.0.1:6379> keys *
           * 1) "ord:102"  序列化过
           * 2) "\xac\xed\x00\x05t\x00\aord:102"   野生,没有序列化过
           * this.redisTemplate.opsForValue (); // 提供了操作 string 类型的所有方法
           * this.redisTemplate.opsForList (); // 提供了操作 list 类型的所有方法
           * this.redisTemplate.opsForSet (); // 提供了操作 set 的所有方法
           * this.redisTemplate.opsForHash (); // 提供了操作 hash 表的所有方法
           * this.redisTemplate.opsForZSet (); // 提供了操作 zset 的所有方法
           * @param lettuceConnectionFactory
           * @return
           */
          @Bean
          public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
          {
              RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
              redisTemplate.setConnectionFactory(lettuceConnectionFactory);
              // 设置 key 序列化方式 string
              redisTemplate.setKeySerializer(new StringRedisSerializer());
              // 设置 value 的序列化方式 json,使用 GenericJackson2JsonRedisSerializer 替换默认序列化
              redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
              redisTemplate.setHashKeySerializer(new StringRedisSerializer());
              redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
              redisTemplate.afterPropertiesSet();
              return redisTemplate;
          }
      }
# 调用其他命令 api(家庭作业)

# 连接集群

# 步骤演示
  1. 启动 Redis 集群的 6 台实例(三主三从)

  2. 改写 YML

    # =============redis集群=============
    spring.redis.password=111111
    # 获取失败 最大重定向次数
    spring.redis.cluster.max-redirects=3
    spring.redis.lettuce.pool.max-active=8
    spring.redis.lettuce.pool.max-wait=-1ms
    spring.redis.lettuce.pool.max-idle=8
    spring.redis.lettuce.pool.min-idle=0
    spring.redis.cluster.nodes=192.168.111.175:6381,192.168.111.175:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.174:6385,192.168.111.174:6386
  3. 通过微服务访问 redis 集群:一切 ok

# 故障转移时的经典问题
  1. 人为模拟 master 6381 机器意外宕机,手动 shutdown

  2. 对 redis 集群命令方式,手动验证各种读写命令,slave 6384 成功上位

  3. Redis 侧的集群能自动感知并完成主从切换,对应的 slave 6384 会被选举为新的 master

  4. 微服务客户端再次读写访问,发现连接不上 master 6381!SpringBoot 客户端没有动态感知到 Redis 集群的最新集群信息。当 master 宕机主从切换成功,redis 手动 OK,但是有 **2 个经典故障**:

    image-20230809214444224

    报错:命令超时 1 分钟!

    image-20230809214514675

    报错:无法连接 6381!

    根本原因SpringBoot 2.X 版本,Redis 默认的连接池采用 Lettuce。当 Redis 集群节点发生变化后,Letture 默认是不会刷新节点拓扑

    解决方案刷新节点集群拓扑动态感应修改 YML 中的两个配置项即可

    image-20230809214957793

  5. 修改 YML

    # ============redis集群============
    spring.redis.password=111111
    # 获取失败 最大重定向次数
    spring.redis.cluster.max-redirects=3
    spring.redis.lettuce.pool.max-active=8
    spring.redis.lettuce.pool.max-wait=-1ms
    spring.redis.lettuce.pool.max-idle=8
    spring.redis.lettuce.pool.min-idle=0
    # 支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
    spring.redis.lettuce.cluster.refresh.adaptive=true
    # 定时刷新
    spring.redis.lettuce.cluster.refresh.period=2000
    spring.redis.cluster.nodes=192.168.111.175:6381,192.168.111.175:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.174:6385,192.168.111.174:6386

# 高级篇

前置技术要求:微服务(boot、cloud)+ docker + Nginx + JUC + Jmeter

# 1、Redis 的单线程与多线程 (入门篇)

# 2、BigKey

# 3、缓存双写一致性之更新策略探讨

# 4、Redis 与 MySQL 数据双写一致性工程落地案例

# 5、案例落地实战 bitmap/HyperLogLog/GEO

# 6、布隆过滤器 BloomFilter

# 7、缓存预热 + 缓存雪崩 + 缓存击穿 + 缓存穿透

# 8、手写 Redis 分布式锁

# 9、Redlock 算法和底层源码分析

# 10、Redis 经典五大类型源码及底层实现

# 11、Redis 为什么快?高性能设计之 epoll 和 IO 多路复用深度解析

# 12、终章の总结