Redis 7.0 核心技术、实战应用、面试题

Redis 7.0 核心技术与实战应用

🌈 Redis 入门概述

01、Redis 是什么

Redis:REmote Dictionary Server(远程字典服务器)

官网介绍:https://redis.io/docs/about

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

02、Redis 使用场景

🅰️ 1、主流功能与应用

1)分布式缓存,挡在 MySQL 数据库之前的带刀护卫

在这里插入图片描述

与传统数据库(MySQL)关系:

  • Redis 是 key-value 数据库(NoSQL 一种),MySQL 是关系数据库;
  • Redis 数据操作主要在内存,而 MySQL 主要存储在磁盘;
  • Redis 在某一些场景使用中要明显优于 MySQL,比如计数器、排行榜等方面;
  • Redis 通常用于一些特定场景,需要与 MySQL 一起配合使用;
  • 两者并不是相互替换和竞争关系,而是共用和配合使用。

2)内存存储和持久化(RDB + AOF

Redis 支持 异步 将内存中的数据写到硬盘上,同时不影响继续服务。

3)高可用架构搭配

支持 单机主从哨兵集群 架构模式。

4)缓存穿透、击穿、雪崩

5)分布式锁

6)队列

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

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

7)排行版+点赞

在互联网应用中,有各种各样的排行榜,如电商网站的月度销量排行榜、社交APP的礼物排行榜、小程序的投票排行榜等等。Redis 提供的 zset 数据类型能够快速实现这些复杂的排行榜。

比如小说网站对小说进行排名,根据排名,将排名靠前的小说推荐给用户。

🅰️ 2、总体功能概述

在这里插入图片描述

🅰️ 3、优势

1)性能极高 :Redis 能读的速度是 110000次/秒,写的速度是 81000次/秒。

2)数据类型丰富 :支持简单的 key-value 类型数据,还提供 listsetzsethash 等数据结构的存储。

3)数据的持久化 :可以将内存中的数据保持在磁盘中,重启的时候可以再次加载数据进行使用。

4)数据的备份 :即 master-slave 模式的数据备份。

🅰️ 4、小结

在这里插入图片描述

03、Redis 下载使用

🅰️ 1、地址

文档:https://redis.io

源码:https://github.com/redis/redis

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

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

中文资料:

  • http://www.redis.cn
  • https://redis.com.cn/documentation.html

下载地址:https://redis.io/download

🅰️ 2、使用

官网:https://redis.io/docs/about

  • Transactions
  • Pub/Sub
  • Lua scripting
  • Keys with a limited time-to-live
  • LRU eviction of keys
  • Automatic failover

04、Redis 迭代演化

🅰️ 1、版本迭代推演介绍

1)里程碑式的重要版本

在这里插入图片描述

Redis 3.0 中的 cluster 解决了 Redis 的单机瓶颈,将其从主从架构变成了分布式集群架构;4.0 版本的 lazy-free 彻底解决了大 KEY 删除阻塞的运维痛点, modules 则将 Redis 的功能进行了进一步拓展,使其能够实现更多能力,比如 RedisSearch 带来了全文搜索能力; 5.0 版本的 Stream 使 Redis 真正意义上成为了轻量级的消息队列; 6.0 版本的多 IO 和 SSL 提高了 Redis 的性能和安全性。

所有核心特性都在保持 Redis 稳定性的前提下,围绕着性能、扩展性、安全场景和拓展四个方面不断增强。

2)命名规则

Redis 从发布至今,一直遵循着自己的命名规则:

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

可以通过官网来下载自己感兴趣的版本进行源码阅读:

历史发布版本的源码:https://download.redis.io/releases

🅰️ 2、Redis 7 新特性概述

地址:https://github.com/redis/redis/releases

1)部分新特性总览

2022 年 4 月正式发布的 Redis 7.0 是目前 Redis 历史版本中变化最大的版本。

首先,它有超过 50 个以上新增命令;其次,它有大量核心特性的新增和改进。

在这里插入图片描述

  • Redis Functions (了解)

在这里插入图片描述

Functions 彻底解决了过去 Lua 脚本在持久化上含糊不清的问题,安全性也得到了大幅度提高,通过使用 Functions 即可完全避免 Lua 脚本丢失的问题了,极大减轻了运维压力。

  • Client-eviction

在这里插入图片描述

Redis 7.0 新增了 client-invocation 参数,能够从全局的角度对 Redis 连接总内存占用进行控制。举个例子,如果连接总内存占用超过配置上限, Redis 会优先清理内存占用较大的连接,在一定程度上实现了对内存占用和数据内存占用的隔离控制,能够更好地管理 Redis 内存,节约内存使用成本,无须再预留过多内存。

  • Multi-part AOF

在这里插入图片描述

AOF 触发 Rewrite 的时候,Redis 会先将全量数据做内存快照,然后落盘。落盘的漫长过程中会产生增量数据,此时 Redis 会开辟一块新的内存空间来记录这些增量数据,这就带来了额外的内存开销。在极端情况下,AOF Rewrite 过程中的额外内存占用会与 Redis 的数据内存几乎相等,极易发生 OOM ,因此也被迫需要在操作系统中预留更多内存来避免 Redis OOM 的发生,这也是 Redis 内存资源浪费的重要原因。

此外,在 AOF Rewrite 的最后,Redis 会将全量数据与增量数据做一次合并操作,导致一份数据带来两次磁盘 IO。 同时在 AOF 的合并过程中,主进程和子进程之间的数据交互也会占用 CPU 资源。

所以在 AOF Rewrite 过程中,内存、IO、CPU 都会被占用,而这些都是额外的负担,非常影响业务。因此,通常需要将 AOF 自动 Rewrite 改在业务低峰期,通过脚本触发,甚至关闭 AOF。

Multi-part AOF 是对 AOF 的彻底改造。它将 AOF 分为三个部分,分别是 base AOF、incr AOF 和 History AOF 。其中 base AOF 用来记录 AOF Rewrite 时刻的全量内存数据,incr AOF 用来记录 rewrite 过程中所有增量数据。incr AOF 的出现,使 Redis 不需要再开辟新的内存空间来记录增量数据。而多文件设计的理念也使得新版 AOF 无需做数据合并,因为它的全量和增量被放在不同文件中,天然隔离。在 AOF React 的最后,此前的历史 AOF 文件都会成为 history AOF 被直接删除,因而也不存在合并。

  • ACL V2

在这里插入图片描述

Redis 6.0 大版本中引入了 ACL v1,虽然能够实现一定程度的权限控制,但实用性并不强,比如无法支持粒度至 KEY 的权限访问控制,所有 KEY 的权限必须一致。而在 Redis 7.0 中, ACL v2 正式支持粒度至 KEY 的权限访问控制,可以轻松实现账户对不同 KEY 有不同的权限访问控制。

基于 ACL v2,过去 Redis 常被诟病的业务权限难以维护管理的问题也将得到彻底解决。

  • 新增命令(了解)

新增 ZMPOP,BZMPOP,LMPOP,BLMPOP 等新命令,对于 EXPIRE 和 SET 命令,新增了更多的命令参数选项。

例如,ZMPOP 的格式如下:ZMPOP numkeys key [key ...] MIN|MAX [COUNT count],而 BZMPOP 是 ZMPOP 的阻塞版本。

  • listpack 替代 ziplist

listpack 是用来替代 ziplist 的新数据结构,在 7.0 版本已经没有 ziplist 的配置了(6.0 版本仅部分数据类型作为过渡阶段在使用)

🅰️ 3、部分新特性说明

大体和之前的 Redis 版本保持一致和稳定,主要是自身底层性能和资源利用率上的 优化和提高,如果你生产上系统稳定,不用着急升级到最新的 Redis 7 版本,当然,如果你是从零开始的新系统,可以直接上 Redis 7。

1)多 AOF 文件支持

7.0 版本中一个比较大的变化就是 AOF 文件由一个变成了多个,主要分为两种类型:基本文件(base files)、增量文件(incr files),请注意这些文件名称是复数形式说明每一类文件不仅仅只有一个。在此之外还引入了一个清单文件(manifest)用于跟踪文件以及文件的创建和应用顺序(恢复)。

2)config 命令增强

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

3)限制客户端内存使用 Client-eviction

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

  • maxmemory-clients 1g
  • maxmemory-clients 10%

4)listpack 紧凑列表调整

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

5)访问安全性增强 ACLV2

在 redis.conf 配置文件中,protected-mode 默认为 yes,只有当你希望你的客户端在没有授权的情况下可以连接到 Redis server 的时候可以将 protected-mode 设置为 no。

6)Redis Functions

Redis 函数,一种新的通过服务端脚本扩展 Redis 的方式,函数与数据本身一起存储。

7)RDB 保存时间调整

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

8)命令新增和变动

Zset(有序集合)增加 ZMPOP、BZMPOP、ZINTERCARD 等命令;
Set(集合)增加 SINTERCARD 命令;
List(列表)增加 LMPOP、BLMPOP ,从提供的键名列表中的第一个非空列表键中弹出一个或多个元素。

9)性能资源利用率、安全等改进

自身底层部分优化改动,Redis 核心在许多方面进行了重构和改进。

主动碎片整理 V2:增强版主动碎片整理,配合 Jemalloc 版本更新,更快更智能,延时更低。

HyperLogLog 改进:在 Redis5.0 中,HyperLogLog 算法得到改进,优化了计数统计时的内存使用效率,7 更加优秀。

更好的内存统计报告。

🌈 Redis 安装配置

01、环境准备

🅰️ 1、环境准备

如何查看自己的 Linux 是 32 位还是 64 位?准备 64 位的

getconf LONG_BIT
# 返回是多少就是几位

1)Linux 环境安装 Redis 必须先具备 gcc 编译环境

  • 什么是 gcc

GCC 是 Linux 下的一个编译程序,是 C 程序的编译工具。

GCC(GNU Compiler Collection)是 GNU(GNU’s Not Unix)计划提供的编译器家族,它能够支持 C,C++,Objective-C,Fortran,Java 和 Ada 等等程序设计语言前端,同时能够运行在 x86,x86-64,IA-64,PowerPC,SPARC 和 Alpha 等等几乎目前所有的硬件平台上。鉴于这些特征,以及 GCC 编译代码的高效性,使得 GCC 成为绝大多数自由软件开发编译的首选工具。虽然对于程序员们来说,编译器只是一个工具,除了开发和维护人员,很少有人关注编译器的发展,但是 GCC 的影响力是如此之大,它的性能提升甚至有望改善所有的自由软件的运行效率,同时它的内部结构的变化也体现出现代编译器发展的新特征。

  • 查看是否安装 gcc
gcc -v
  • 安装 gcc

安装 Redis 之前需要具备 c++ 库环境

yum -y install gcc- c++

🅰️ 2、版本选择

1)查看自己 Redis 版本的命令

redis-server -v

安全 Bug 按照官网提示,升级成为 6.0.8 及以上。

本次使用 Redis 7.0。

02、Redis 安装

🅰️ 1、安装操作

1)下载获得 redis-7.0.0.tar.gz 后将它放入 Linux 目录 /opt

wget https://download.redis.io/releases/redis-7.0.0.tar.gz

ls -l redis-7.0.0.tar.gz

2)解压

tar -zxvf redis-7.0.0.tar.gz

3)编译安装

# 进入目录
cd redis-7.0.0

# 在redis-7.0.0目录下执行make命令编译安装
make && make install

....
# 安装成功
Hint: It's a good idea to run 'make test' ;)

4)查看默认安装目录:usr/local/bin

Linux 下的 /usr/local 类似 Windows 系统的 C:\Program Files

在安装目录下有以下几个工具:

  • redis-benchmark:性能测试工具,服务启动后运行该命令,看看性能如何
  • redis-check-aof:修复有问题的 AOF 文件
  • redis-check-dump:修复有问题的 dump.rdb 文件
  • redis-cli:Redis 客户端连接工具
  • redis-sentinel:Redis 集群使用
  • redis-server:Redis 服务器启动命令

🅰️ 2、运行操作

1)配置文件

将默认的配置文件 redis.conf 拷贝到自己定义好的一个路径下,比如 /myredis

cd redis-7.0.0

mkdir /myredis

cp redis.conf /myredis/redis7.conf

2)修改 /myredis 目录下 redis7.conf 配置文件做初始化设置

redis.conf 配置文件,改完后确保生效,记得重启

基础配置:

  • 后台启动

默认 daemonize no,修改为 daemonize yes

  • 关闭保护模式,保证应用服务可连接

默认 protected-mode yes,修改为 protected-mode no

  • 配置连接 IP

默认 bind 127.0.0.1 改为 直接注释掉(默认 bind 127.0.0.1 只能本机访问)或改成本机 IP 地址,否则影响远程 IP 连接。

  • 配置密码

添加 Redis 连接密码,改为 requirepass 123456(密码)

3)启动服务

在 /usr/local/bin 目录下运行 redis-server,启用 /myredis 目录下的 redis.conf 配置文件。

redis-server /myredis/redis7.conf

ps -ef | grep redis | grep -v grep

4)连接服务

redis-cli -a 123456 -p 6379

查看服务端和客服端运行:

ps -ef | grep redis

5)关闭

单实例关闭:

redis-cli -a 123456 shutdown

多实例关闭,指定端口关闭:

redis-cli -p 6379 shutdown

03、Redis 卸载

1)停止 redis-server 服务

redis-cli -p 6379 shutdown

ps -ef | grep redis

2)删除 /usr/local/lib 目录下与 redis 相关的文件

# 查看
ls -l /usr/local/bin/redis-*
# 删除
rm -rf /usr/local/bin/redis-*

ls -l /usr/local/bin/redis-*

🌈 Redis 数据类型与命令

01、Redis 支持的数据类型概述

官网:https://redis.io/docs/data-types

StringsListsSetsHashesSorted setsStreamsGeospatialBitmapsBitfieldsHyperLogLog

在这里插入图片描述

这里说的数据类型是 value 的数据类型,key 的类型都是字符串

🅰️ 1、字符串 String

String 是 Redis 最基本的类型,一个 key 对应一个 value。

String 类型是二进制安全的,意思是 Redis 的 String 可以包含任何数据,比如图片或者序列化的对象 。

String 类型是 Redis 最基本的数据类型,一个 Redis 中字符串 value 最大可以是 512M

🅰️ 2、列表 List

Redis 列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的 头部(左边)或 尾部(右边)。

它的底层实际是个 双端链表,最多可以包含 2^32 - 1 个元素 (4294967295 每个列表超过 40 亿个元素)。

🅰️ 3、哈希表 Hash

Redis Hash 是一个 String 类型的 field(字段)和 value(值)的映射表,Hash 特别适合用于存储对象。

Redis 中每个 Hash 可以存储 2^32 - 1 键值对。

🅰️ 4、集合 Set

Redis 的 Set 是 String 类型的 无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,集合对象的编码可以是 intset 或者 hashtable。

Redis 中 Set 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。

集合中最大的成员数为 2^32 - 1 (4294967295,每个集合可存储40多亿个成员)。

🅰️ 5、有序集合 ZSet

ZSet(sorted set:有序集合)

Redis ZSet 和 Set 一样也是 String 类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个 double 类型的分数,Redis 正是通过分数来为集合中的成员进行从小到大的排序。

ZSet 的成员是唯一的,但分数(score)却可以重复。

ZSet 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 2^32 - 1

🅰️ 6、地理空间 GEO

Redis GEO 主要用于存储 地理位置 信息,并对存储的信息进行操作,包括:

  • 添加地理位置的坐标;
  • 获取地理位置的坐标;
  • 计算两个位置之间的距离;
  • 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。

🅰️ 7、基数统计 HyperLogLog

HyperLogLog 是用来做 基数统计 的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定且是很小的。

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

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素数据。

🅰️ 8、位图 Bitmap

Bit arrays(or simply bitmaps,可以称之为 位图

在这里插入图片描述

上图由许许多多的小格子组成,每一个格子里面只能放 1 或者 0,用它来判断 Y/N 状态。说的专业点,每一个小格子就是一个个 bit。

由 0 和 1 状态表现的二进制位的 bit 数组。

🅰️ 9、位域 Bitfield

通过 Bitfield 命令可以一次性操作多个 比特位域(指的是连续的多个比特位),它会执行一系列操作并返回一个响应数组,这个数组中的元素对应参数列表中的相应操作的执行结果。

即通过 Bitfield 命令可以一次性对多个比特位域进行操作。

🅰️ 10、流 Stream

Redis Stream 是 Redis 5.0 版本新增加的数据结构。

Redis Stream 主要用于消息队列(MQ,Message Queue)。Redis 本身是有一个 Redis 发布订阅(pub/sub)来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。

简单来说发布订阅(pub/sub)可以分发消息,但无法记录历史消息。

而 Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

02、Redis key 的操作命令

🅰️ ​1、命令查询

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

中文参考:http://www.redis.cn/commands.html、http://doc.redisfans.com

🅰️ 2、与 Redis 键相关的基本命令

  • KEYS * :查看当前库所有的 key,会阻塞。
  • KEYS pattern :查找所有符合给定模式(pattern)的 key 。
  • DEL key :用于在 key 存在时删除 key。
  • ⭕️ unlink key :非阻塞删除,仅仅将 key 从 keyspace 元数据中删除,真正的删除会在后续异步中操作。
  • DUMP key :序列化给定 key ,并返回被序列化的值。
  • TYPE key :返回 key 所储存的值的数据类型。
  • EXISTS key :检查给定 key 是否存在。
  • EXPIRE key seconds :为给定 key 设置过期时间,以秒计。
  • EXPIREAT key timestamp :EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置过期时间。 不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳。
  • PEXPIRE key milliseconds :设置 key 的过期时间以毫秒计。
  • PEXPIREAT key milliseconds-timestamp :设置 key 过期时间的时间戳(unix timestamp)以毫秒计。
  • MOVE key dbindex[0-15] :将当前数据库的 key 移动到给定的数据库 db 当中。
  • SELECT dbindex :切换数据库【0-15】,默认为 0 号库。
  • PERSIST key :移除 key 的过期时间,key 将持久保持不过期。
  • PTTL key :以毫秒为单位返回 key 的剩余的过期时间。
  • TTL key :以秒为单位,返回 key 的剩余生存时间(TTL,time to live)-1 表示永不过期,-2 表示已过期。
  • RANDOMKEY :从当前数据库中随机返回一个 key 。
  • RENAME key newkey :修改 key 的名称。
  • RENAMENX key newkey :仅当 newkey 不存在时,将 key 改名为 newkey 。
  • dbsize :查看当前数据库 key 的数量。
  • flushdb :清空当前库。
  • flushall :清空全部库。

命令不区分大小写,而 key 是区分大小写的。

帮助命令:help @类型help @stringhelp @listhelp @hyperloglog、…

03、Redis 字符串(String)

文档介绍:https://redis.io/docs/data-types/strings

相关命令:https://redis.io/commands/?group=string

🅰️ 1、最常用

SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]

1)SET 命令有 EX、PX、NX、XX 以及 KEEPTTL 五个可选参数,其中 KEEPTTL 为 6.0 版本添加的可选参数,其它为 2.6.12 版本添加的可选参数。

  • EX seconds:以秒为单位设置过期时间;
  • PX milliseconds:以毫秒为单位设置过期时间;
  • EXAT timestamp:设置以秒为单位的 UNIX 时间戳所对应的时间为过期时间;
  • PXAT milliseconds-timestamp:设置以毫秒为单位的 UNIX 时间戳所对应的时间为过期时间;
  • NX:键不存在的时候设置键值;
  • XX:键存在的时候设置键值;
  • KEEPTTL:保留设置前指定键的生存时间;
  • GET:返回指定键原本的值,若键不存在时返回 nil 。

SET 命令使用 EX、PX、NX 参数,其效果等同于 SETEX、PSETEX、SETNX 命令。根据官方文档的描述,未来版本中SETEX、PSETEX、SETNX 命令可能会被淘汰。

EXAT、PXAT 以及 GET 为 Redis 6.2 新增的可选参数。

2)返回值:

  • 设置成功则返回 OK;返回 nil 为未执行 SET 命令,如不满足 NX、XX 条件等情况。

  • 若使用 GET 参数,则返回该键原来的值,该键不存在时返回 nil 。

🅰️ ​2、同时设置/获取多个键值

语法:

# 同时设置一个或多个 key-value 对; 返回值 OK 
MSET key value [key value ...]

# 获取所有(一个或多个)给定 key 的值; 返回 list of values
MGET key [key ...]

# 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在; 返回值 1 or 0
MSETNX key value [key value ...]

案例:

redis> MSET key1 "Hello" key2 "World"
"OK"
redis> GET key1
"Hello"
redis> GET key2
"World"
redis> MGET key1 key2 nonexisting
1) "Hello"
2) "World"
3) (nil)
redis> MSETNX key1 "Hello" key2 "World"
(integer) 1
redis> MSETNX key2 "new" key3 "msetnx"
(integer) 0
redis> MGET key1 key2 key3
1) "Hello"
2) "World"
3) (nil)

🅰️ 3、获取指定区间范围内的值

语法:

# 获取指定区间范围内的值,类似 between...and 的关系,从零到负一表示全部
GETRANGE key start end

# 设置指定区间范围内的值
SETRANGE key offset value

案例:

redis> SET mykey "This is a string"
"OK"
redis> GETRANGE mykey 0 3
"This"
redis> GETRANGE mykey -3 -1
"ing"
redis> GETRANGE mykey 0 -1
"This is a string"
redis> SET key1 "Hello World"
"OK"
redis> SETRANGE key1 6 "Redis"
(integer) 11
redis> GET key1
"Hello Redis"

🅰️ 4、数值增减

语法:

# 递增 1
INCR key

# 递增指定步数
INCRBY key increment

举例:

redis> SET key1 "10"
"OK"
redis> INCR key1
(integer) 11
redis> GET key1
"11"
redis> INCRBY key1 5
(integer) 16
redis> GET key1 
"16"

语法:

# 递减 1
DECR key

# 递减指定步数
DECRBY key decrement

举例:

redis> SET mykey "10"
"OK"
redis> DECR mykey
(integer) 9
redis> DECRBY mykey 3
(integer) 6

一定要是数字才能进行加减:

redis> SET mykey "1234293482390480948029348230948"
"OK"
redis> DECR mykey
(error) value is not an integer or out of range

🅰️ ​5、获取字符串长度和内容追加

语法:

# 获取字符串长度
STRLEN key
# 内容追加
APPEND key value

如果键已经存在并且是字符串,则此命令将在字符串末尾附加值。若键不存在,则创建该键并将其设置为空字符串,所以在这种特殊情况下,APPEND 将类似于 SET。

举例:

redis> SET mykey "Hello world"
"OK"
redis> STRLEN mykey
(integer) 11
redis> STRLEN nonexisting
(integer) 0
redis> EXISTS mykey
(integer) 0
redis> APPEND mykey "Hello"
(integer) 5
redis> APPEND mykey " World"
(integer) 11
redis> GET mykey
"Hello World"

🅰️ 6、GETSET

GETSET key value

原子地将 key 设置为 value,并返回存储在 key 中的旧值。成功 SET 操作后,将放弃与密钥相关的任何先前生存时间。

举例:

redis> SET mykey "Hello"
"OK"
redis> GETSET mykey "World"
"Hello"
redis> GET mykey
"World"

🅰️ 7、应用场景

比如抖音无限点赞某个视频或者商品,点一下加一次:

redis> INCR items:1
(integer) 1
redis> INCR items:1
(integer) 2
redis> INCR items:1
(integer) 3

分布式锁:

SET key value [NX | XX] [EX seconds | PX milliseconds]
redis> SET lock:item:1 1 EX 10 NX
"OK"
redis> SET lock:item:1 1 EX 10 NX
(nil)

04、Redis 列表(List)

文档介绍:https://redis.io/docs/data-types/lists

相关命令:https://redis.io/commands/?group=list

Redis 列表是字符串值的双端链表。容量是 2^32 - 1(4,294,967,295)个元素,大概 40 多亿。

Redis 列表通常用于:

  • 实现堆栈和队列;
  • 为后台工作系统构建队列管理。

在 left、right 都可以插入添加,如果键不存在,创建新的链表;如果键已存在,新增内容;如果值全移除,对应的键也就消失了。

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

🅰️ 1、最常用 lpushrpushlrange

# 在列表的开头插入所有指定值。如果键不存在,则在执行操作之前将其创建为空列表
LPUSH key element [element ...]

RPUSH key element [element ...]

LRANGE key start stop

LRANGE 返回存储在键处的列表的指定元素。偏移量 start 和 stop 是从零开始的索引,0 是列表的第一个元素(列表的开头),1 是下一个元素,依此类推。

这些偏移也可以是负数,表示从列表末尾开始的偏移。例如,-1 是列表的最后一个元素,-2 是倒数第二个元素,依此类推。

举例:

redis> LPUSH list1 1 2 3
(integer) 3
redis> LRANGE list1 0 -1
1) "3"
2) "2"
3) "1"
redis> RPUSH list2 10 11 12
(integer) 3
redis> LRANGE list2 0 -1
1) "10"
2) "11"
3) "12"

🅰️ 2、最常用 lpoprpop

语法:

LPOP key [count]

删除并返回存储在列表的第一个元素。默认情况下,该命令从列表的开头弹出一个元素。当提供可选的 count 参数时,根据列表的长度,取出包含最多 count 个元素。

RPOP key [count]

删除并返回存储在列表的最后一个元素。默认情况下,该命令从列表末尾弹出一个元素。当提供可选的 count 参数时,根据列表的长度,取出包含最多 count 个元素。

举例:

redis> RPUSH list1 1 2 3 4 5 6
(integer) 6
redis> LPOP list1
"1"
redis> RPOP list1
"6"
redis> RPOP list1 2
1) "5"
2) "4"
redis> LRANGE list1 0 -1
1) "2"
2) "3"

🅰️ 3、命令 LINDEX

语法:

LINDEX key index

返回列表中索引下标的元素。索引从 0 开始,因此 0 表示第一个元素,1 表示第二个元素,依此类推。可以使用负索引来指定从列表尾部开始的元素。-1 表示最后一个元素,-2 表示倒数第二个元素,依此类推。

如果键处的值不是列表,则返回错误。

举例:

redis> LPUSH list1 1 2 3 4
(integer) 4
redis> LINDEX list1 0
"4"
redis> LINDEX list1 -1
"1"

🅰️ 4、命令 LLEN

语法:

LLEN key

返回键列表的长度。若键不存在,则将其解释为空列表,并返回 0。当键中存储的值不是列表时,将返回错误。

🅰️ 5、命令 LREM

语法:

LREM key count element

从 left 往 right 删除 count 个值等于 element 的元素首次计数,返回的值为实际删除的数量。

  • count > 0 :移除等于元素从头部移动到尾部的元素。
  • count < 0 :删除等于元素从尾部移动到头部的元素。
  • count = 0 :删除等于元素的所有元素。

注意,不存在的键被视为空列表,所以当键不存在时,命令将始终返回 0。

举例:

redis> LPUSH list1 1 1 2 2 3 3 3 4
(integer) 8
redis> LREM list1 2 3
(integer) 2
redis> LRANGE list1 0 -1
1) "4"
2) "3"
3) "2"
4) "2"
5) "1"
6) "1"
redis> LREM list1 0 2
(integer) 2
redis> LRANGE list1 0 -1
1) "4"
2) "3"
3) "1"
4) "1"

🅰️ 6、命令 LTRIM

LTRIM key start stop

截取指定范围的值后再赋值给 key。

redis> RPUSH list1 1 2 3 4 5
(integer) 5
redis> LTRIM list1 1 3
"OK"
redis> LRANGE list1 0 -1
1) "2"
2) "3"
3) "4"

🅰️ 7、命令 RPOPLPUSH

RPOPLPUSH source destination

原子的返回并删除存储在源位置的列表的最后一个元素(尾部),并将该元素推送到存储在目标位置的列表中的第一个元素(头部)。

redis> RPUSH list1 1 2 3 4
(integer) 4
redis> RPOPLPUSH list1 list2
"4"
redis> LRANGE list1 0 -1
1) "1"
2) "2"
3) "3"
redis> LRANGE list2 0 -1
1) "4"

🅰️ 8、命令 LSET

LSET key index element

将索引处的列表元素设置为元素。超出范围的索引返回错误。

redis> LPUSH list1 1 2 3
(integer) 3
redis> LSET list1 1 0
"OK"
redis> LRANGE list1 0 -1
1) "3"
2) "0"
3) "1"

🅰️ 9、命令 LINSERT

LINSERT key <BEFORE | AFTER> pivot element

在列表某个已有值的前后再添加具体值。当键不存在时,它被视为空列表,不执行任何操作。当键存在但不是列表值时,将返回错误。

redis> LPUSH list1 r e d i s r
(integer) 6
redis> LINSERT list1 BEFORE r a
(integer) 7
redis> LRANGE list1 0 -1
1) "a"
2) "r"
3) "s"
4) "i"
5) "d"
6) "e"
7) "r"

🅰️ 10、应用场景

例如微信公众号订阅的消息:

  • 关注的公众号发布了新文章(文章编号 10,11),就会安装进我的 List
LPUSH subscribe:wuid 10 11
  • 查看自己订阅的全部文章,类似分页,下面 0~10 就是一次显示 10 条
LRANGE subscribe:wuid 0 9

05、Redis 哈希(Hash)

文档介绍:https://redis.io/docs/data-types/hashes

相关命令:https://redis.io/commands/?group=hash

每个 Hash 最多可以存储 4294967295(2^32-1)个字段值对。

大多数 Redis Hash 命令都是 O(1)。一些命令(如 HKEYS、HVALS 和 HGETALL)是 O(n),其中 n 是字段值对的数量。

🅰️ 1、常用的 hsethgethmsethmgethgetallhdel

语法:

# 存储
HSET key field value [field value ...]

# 获取单个
HGET key field

# 已标记过期
HMSET key field value [field value ...]

# 获取多个
HMGET key field [field ...]

# 获取所有
HGETALL key

# 删除
HDEL key field [field ...]

举例:

redis> HSET hash1 f1 v1
(integer) 1
redis> HGET hash1 f1
"v1"
redis> HMSET hash1 f2 v2 f3 v3
"OK"
redis> HMGET hash1 f2 f3
1) "v2"
2) "v3"
redis> HGETALL hash1
1) "f1"
2) "v1"
3) "f2"
4) "v2"
5) "f3"
6) "v3"
redis> HDEL hash1 f1 f2
(integer) 2
redis> HGETALL hash1
1) "f3"
2) "v3"

🅰️ 2、命令 HLEN

# 返回存储键的哈希中包含的字段数
HLEN key

🅰️ 3、命令 HEXISTS

# 判断hash是否存在field键
HEXISTS key field

如果哈希包含字段,则返回 1 。如果哈希不包含字段或键不存在,则返回 0 。

🅰️ 4、常用的 HKEYSHVALS

语法:

# 返回 hash 所有的 field 
HKEYS key

# 返回 hash 所有的 values
HVALS key

举栗:

redis> HSET myhash field1 "Hello"
(integer) 1
redis> HSET myhash field2 "World"
(integer) 1
redis> HVALS myhash
1) "Hello"
2) "World"
redis> HKEYS myhash
1) "field1"
2) "field2"

🅰️ 5、命令 HINCRBYHINCRBYFLOAT

语法:

HINCRBY key field increment

HINCRBYFLOAT key field increment

将存储在 key 中的哈希的字段的数字递增。如果密钥不存在,则创建一个保存哈希的新密钥。如果字段不存在,则在执行操作之前将值设置为 0 。

举栗:

redis> HSET myhash field 5
(integer) 1
redis> HINCRBY myhash field 1
(integer) 6
redis> HINCRBY myhash field -1
(integer) 5
redis> HSET mykey field 10.50
(integer) 1
redis> HINCRBYFLOAT mykey field 0.1
"10.6"
redis> HINCRBYFLOAT mykey field -5
"5.6"

🅰️ 6、命令 HSETNX

语法:

HSETNX key field value

仅当 field 字段还不存在时,才将存储在 key 的哈希中的字段设置为值。如果 key 不存在,则创建一个保存哈希的新 key。如果字段已存在,则此操作无效。

举栗:

redis> HSETNX myhash field "Hello"
(integer) 1
redis> HSETNX myhash field "World"
(integer) 0
redis> HGET myhash field
"Hello"

🅰️ 7、应用场景

小型商城购物车可用方案:

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

06、Redis 集合(Set)

文档介绍:https://redis.io/docs/data-types/sets

相关命令:https://redis.io/commands/?group=set

最多可以存储 4294967295(2^32-1)个元素。

Redis 集合是唯一字符串(成员)的无序集合。

  • 跟踪唯一项目(例如,跟踪访问给定博客文章的所有唯一 IP 地址)。
  • 表示关系(例如,具有给定角色的所有用户的集合)。
  • 执行常见的集合操作,如交集、并集和差集。

🅰️ 1、添加元素 SADD

语法:

SADD key member [member ...]

将指定的成员添加到集合。已存在此集合成员的将被忽略。如果键不存在,则在添加指定成员之前创建一个新集。

当键中存储的值不是集合时,将返回错误。

示例:

redis> SADD set1 "HELLO" "WORLD" "REDIS" "HELLO"
(integer) 3
redis> SMEMBERS set1
1) "REDIS"
2) "WORLD"
3) "HELLO"

🅰️ 2、遍历集合 SMEMBERS

语法:

SMEMBERS key

返回键的集合值的所有元素。

示例:

redis> SADD set1 "HELLO" "WORLD" "REDIS" "HELLO"
(integer) 3
redis> SMEMBERS set1
1) "REDIS"
2) "WORLD"
3) "HELLO"

🅰️ 3、判断元素是否在集合 SISMEMBER/SMISMEMBER

语法:

SISMEMBER key member

SMISMEMBER key member [member ...]

成员是集合的元素返回 1,否则返回 0。

判断每个成员是否是集合的元素。对于每个成员,如果值是集合的元素,返回 1;如果不是集合的元素或键不存在,则返回 0。

示例:

redis> SADD myset "one"
(integer) 1
redis> SISMEMBER myset "one"
(integer) 1
redis> SISMEMBER myset "two"
(integer) 0
redis> SMISMEMBER myset "one" "two"
1) (integer) 1
2) (integer) 0

🅰️ 4、删除元素 SREM

语法:

SREM key member [member ...]

从集合中删除指定的元素。将忽略不是此集合元素的指定成员。如果键不存在,则将其视为空集,此命令返回 0。

当键存储的值不是集合时,将返回错误。

返回值为整数,从集合中删除的成员数量,不包括不存在的成员。

示例:

redis> SADD myset 1 2 3 4
(integer) 4
redis> SREM myset 1 3
(integer) 2
redis> SMEMBERS myset
1) "2"
2) "4"

🅰️ 5、获取集合的元素个数 SCARD

语法:

SCARD key

返回集合的集合基数(元素数量)。如果键不存在则返回 0。

示例:

redis> SADD set1 1 2 3
(integer) 3
redis> SCARD set1
(integer) 3

🅰️ 6、随机展现集合的元素 SRANDMEMBER

语法:

SRANDMEMBER key [count]

当只使用 key 参数时,从 key 集合中返回一个随机元素。集合内元素不会删除

如果使用 count 参数为正数,则返回一个不同元素的数组。数组的长度是 count 或集合的基数(SCARD),以较低者为准。

如果使用 count 参数为负数,则允许命令多次返回同一元素。返回的元素数是指定计数的绝对值。

示例:

redis> SADD myset one two three
(integer) 3
redis> SRANDMEMBER myset
"two"
redis> SRANDMEMBER myset 2
1) "two"
2) "one"
redis> SRANDMEMBER myset -5
1) "one"
2) "two"
3) "two"
4) "one"
5) "one"

🅰️ 7、随机弹出集合的元素 SPOP

语法:

SPOP key [count]

从集合中删除并返回一个或多个随机元素。

默认情况下,该命令从集合中弹出一个元素。当提供可选的 count 参数时,弹出最多 count 个元素或集合的基数个元素。

示例:

redis> SADD set1 1 2 3 4 5
(integer) 5
redis> SPOP set1
"3"
redis> SPOP set1 3
1) "2"
2) "1"
3) "5"
redis> SMEMBERS set1
1) "4"
redis> SPOP set1 3
1) "4"
redis> SMEMBERS set1
(empty array)

🅰️ 8、将成员从源集合移动到目标集合 SMOVE

语法:

SMOVE source destination member

将成员从源集合移动到目标集合。此操作是原子操作。

如果源集合不存在或不包含指定的元素,则不执行任何操作,并返回 0。否则,元素将从源集中删除并添加到目标集中。如果指定的元素已存在于目标集中,则仅从源集中删除该元素。

示例:

redis> SADD set1 1 2 3
(integer) 3
redis> SADD set2 3 4
(integer) 2
redis> SMOVE set1 set2 1
(integer) 1
redis> SMOVE set1 set2 3
(integer) 1
redis> SMEMBERS set1
1) "2"
redis> SMEMBERS set2
1) "1"
2) "3"
3) "4"

🅰️ 9、集合运算

现存在集合 A(元素 a b c 1 2)、集合 B(元素 1 2 3 a z)。

1)集合的差集运算 A - B,即属于 A 但不属于 B 的元素构成的集合

SDIFF key [key ...]

返回由第一个集合和所有连续集合之间的差异产生的集合的元素。不存在的键被认为是空集。

示例:

> SADD KEYA a b c 1 2
5
> SADD KEYB 1 2 3 a z
5
> SDIFF KEYA KEYB
b
c
> SDIFF KEYB KEYA
3
z

2)集合的并集运算 A ∪ B,属于 A 或者属于 B 的元素合并后的集合

SUNION key [key ...]

返回由所有给定集合的并集产生的集合的元素。

示例:

> SADD KEYA a b c 1 2
5
> SADD KEYB 1 2 3 a z
5
> SUNION KEYA KEYB
a
b
c
1
2
3
z

3)集合的交集运算 A ∩ B,属于 A 同时也属于 B 的共同拥有的元素构成的集合

SINTER key [key ...]

返回由所有给定集合的交集产生的集合的元素。

不存在的键被视为空集。如果其中一个键是空集,则生成的集也是空的(因为与空集相交的集总是导致空集)。

示例:

redis> SADD KEYA a b c 1 2
(integer) 5
redis> SADD KEYB 1 2 3 a z
(integer) 5
redis> SINTER KEYA KEYB
1) "1"
2) "a"
3) "2"
redis> SINTER KEYA KEYB KEYC
(empty array)

4)SDIFFSTORE

语法:

SDIFFSTORE destination key [key ...]

该命令等于 SDIFF,但不返回结果集,而是存储在 destination 集合。如果 destination 集合已经存在,则会覆盖。

返回值为结果集中的元素数量。

示例:

> SADD k1 A B C
3
> SADD k2 C D E
3
> SDIFFSTORE key k1 k2
2
> SMEMBERS key
A
B

5)SUNIONSTORE

语法:

SUNIONSTORE destination key [key ...]

该命令等于 SUNION,但不返回结果集,而是存储在 destination 集合。如果 destination 集合已经存在,则会覆盖。

返回值为结果集中的元素数量。

示例:

> SADD k1 A B C
3
> SADD k2 C D E
3
> SUNIONSTORE key k1 k2
5
> SMEMBERS key
A
B
C
D
E

6)SINTERSTORE

语法:

SINTERSTORE destination key [key ...]

该命令等于 SINTER,但不返回结果集,而是存储在 destination 集合。如果 destination 集合已经存在,则会覆盖。

返回值为结果集中的元素数量。

示例:

> SINTERSTORE key k1 k2
1
> SMEMBERS key
C

7)Redis 7 新命令 SINTERCARD

语法:

SINTERCARD numkeys key [key ...] [LIMIT limit]

该命令类似于 SINTER,但它只返回结果的基数,而不是返回结果集。返回所有给定集合的交集所产生的集合的基数。

示例:

redis> SADD KEYA a b c 1 2
(integer) 5
redis> SADD KEYB 1 2 3 a z
(integer) 5
redis> SINTERCARD 2 KEYA KEYB 
(integer) 3
redis> SINTERCARD 2 KEYA KEYB LIMIT 2
(integer) 2

🅰️ 10、应用场景

1)微信抽奖小程序

  • 用户参与抽奖
SADD key uid
  • 显示多少人参与抽奖
SCARD key
  • 随机抽奖 N 人
SRANDMEMBER key 2  # 随机抽奖2个人,元素不删除

SPOP key 3  # 随机抽奖3个人,元素会删除

2)微信朋友圈点赞查看点赞朋友

  • 新增点赞
SADD pub:msgid uid
  • 取消点赞
SREM pub:msgid uid
  • 显示点赞的朋友
SMEMBERS pub:msgid
  • 点赞数
SCARD pub:msgid
  • 判断是否点赞过
SISMEMBER pub:msgid uid

3)可能认识的人

  • 用户 A 的好友,用户 B 可能认识
SDIFF ua ub
  • 用户 B 的好友,用户 A 可能认识
SDIFF ub ua

07、Redis 有序集合(Sorted sets)

文档介绍:https://redis.io/docs/data-types/sorted-sets

相关命令:https://redis.io/commands/?group=sorted-set

Redis 有序集合是按关联分数排序的唯一字符串(元素)的集合。当多个字符串具有相同的分数时,这些字符串按字典顺序排列。

有序集合是通过包含跳表和哈希表的双端数据结构实现的,因此每次添加元素时,Redis 都会执行 O(log(N)) 操作。

有序集合的一些用例包括:

  • 排行榜。例如,可以使用排序集轻松维护在线游戏中最高分数的有序列表。
  • 速率限制器。例如,可以使用排序集构建滑动窗口速率限制器,以防止过多的 API 请求。

🅰️ 1、添加元素 ZADD

语法:

ZADD key [NX | XX] [GT | LT] [CH] [INCR] score member [score member ...]

将具有指定分数的所有指定成员添加到排序集。可以指定多个分数/成员对。如果指定的成员已经是排序集的成员,则会更新分数并将元素重新插入到正确的位置,以确保正确排序。

如果 key 不存在,则创建一个以指定成员作为唯一成员的新排序集。如果键存在但不是有序集,则返回错误。

可选参数:

  • NX:仅添加新元素。不更新已存在的元素。
  • XX:只更新已存在的元素。不添加新元素。
  • GT:仅当新分数大于当前分数时才更新现有元素。该参数不会阻止添加新元素。
  • LT:仅当新分数小于当前分数时才更新现有元素。该参数不会阻止添加新元素。
  • INCR:ZADD 其行为类似于 ZINCRBY。在此参数下只能指定一对分数元素。

示例:

redis> ZADD zset1 60 a 70 b 80 c 90 d
(integer) 4

🅰️ 2、获取元素 ZRANGE

语法:

ZRANGE key start stop [BYSCORE | BYLEX] [REV] [LIMIT offset count] [WITHSCORES]

按照元素分数从小到大的顺序返回索引从 start 到 stop 之间的所有元素。

示例:

redis> ZADD zset1 60 a 70 b 80 c 90 d
(integer) 4
redis> ZRANGE zset1 0 -1 WITHSCORES
1) "a"
2) "60"
3) "b"
4) "70"
5) "c"
6) "80"
7) "d"
8) "90"
redis> ZRANGE zset1 0 -1
1) "a"
2) "b"
3) "c"
4) "d"

🅰️ 3、获取元素 ZREVRANGE

从 Redis 6.2.0 版开始,此命令被视为已弃用。使用带 REV 参数的 ZRANGE 替换它。

语法:

ZREVRANGE key start stop [WITHSCORES]

返回排序集合中指定的元素范围。元素被认为是从最高到最低的顺序。对于分数相等的元素,使用降序词典顺序。

除了相反的顺序,ZREVRANGE 与 ZRANGE 类似。

示例:

redis> ZADD zset1 60 a 70 b 80 c 90 d
(integer) 4
redis> ZRANGE zset1 0 -1 WITHSCORES
1) "a"
2) "60"
3) "b"
4) "70"
5) "c"
6) "80"
7) "d"
8) "90"
redis> ZREVRANGE zset1 0 -1 WITHSCORES
1) "d"
2) "90"
3) "c"
4) "80"
5) "b"
6) "70"
7) "a"
8) "60"

🅰️ 4、获取元素 ZRANGEBYSCORE

从 Redis 6.2.0 版开始,此命令被视为已弃用。使用带 BYSCORE 参数的 ZRANGE 替换它。

语法:

ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

返回排序集合中 SCORE 介于 min 和 max 之间的所有元素(包括 SCORE 等于 min 或 max 的元素)。元素被认为是从低到高分排序的。

具有相同分数的元素按字典顺序返回(这是从 Redis 中的排序集实现的属性得出的,不涉及进一步的计算)。

可选的 LIMIT 参数只能用于获取匹配元素的范围(类似于 SQL 中的 SELECT LIMIT 偏移量,count)。负计数返回偏移量中的所有元素。如果偏移量很大,则在返回元素之前,需要遍历已排序集合中的偏移元素,这可能会增加 O(N) 时间复杂性。

示例:

redis> ZADD zset1 60 a 70 b 80 c 90 d
(integer) 4
redis> ZRANGEBYSCORE zset1 60 70 # 60<= SCORE <=70
1) "a"
2) "b"
redis> ZRANGEBYSCORE zset1 60 70 WITHSCORES
1) "a"
2) "60"
3) "b"
4) "70"
redis> ZRANGEBYSCORE zset1 (60 70 # 60< SCORE <=70
1) "b"
redis> ZRANGEBYSCORE zset1 (60 (80 # 60< SCORE <80
1) "b"
redis> ZRANGEBYSCORE zset1 (60 (90 LIMIT 1 1
1) "c"
redis> ZRANGEBYSCORE zset1 (60 (90 LIMIT 0 -1
1) "b"
2) "c"

🅰️ 5、ZSCORE

ZSCORE key member

返回排序集合中元素的得分。如果排序集合中不存在成员,或者键不存在,则返回 nil 。

redis> ZADD myzset 1 "one"
(integer) 1
redis> ZSCORE myzset "one"
"1"
redis> ZSCORE myset "two"
(nil)

🅰️ 6、ZCARD

ZCARD key

返回排序集合基数(元素数)。

举例:

redis> ZADD myzset 1 "one"
(integer) 1
redis> ZADD myzset 2 "two"
(integer) 1
redis> ZCARD myzset
(integer) 2

🅰️ 7、ZCOUNT

ZCOUNT key min max

返回排序集中得分介于最小值和最大值之间的元素个数。

redis> ZADD myzset1 1 "one" 2 "two" 3 "three"
(integer) 3
redis> ZCOUNT myzset1 1 3
(integer) 3
redis> ZCOUNT myzset1 (1 3
(integer) 2

🅰️ 8、ZINCRBY

ZINCRBY key increment member

按增量递增排序集合中元素的分数。如果成员在排序集中不存在,则将以增量作为其分数。如果键不存在,将创建一个新的排序集,其中指定成员作为其唯一成员。

分数值应为数值的字符串表示形式,并接受双精度浮点数。可以提供负值来减少分数。

redis> ZADD myzset 1 "one"
(integer) 1
redis> ZINCRBY myzset 2 "one"
"3"
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "one"
2) "3"
redis> ZINCRBY myzset -1 "one"
"2"
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "one"
2) "2"

🅰️ 9、删除元素 ZREM

ZREM key member [member ...]

删除集合中指定的元素。

redis> ZADD myzset1 1 "one" 2 "two" 3 "three"
(integer) 3
redis> ZREM myzset1 "one" "two" "redis"
(integer) 2
redis> ZRANGE myzset1 0 -1
1) "three"

🅰️ 10、ZMPOP

ZMPOP numkeys key [key ...] <MIN | MAX> [COUNT count]

从键名列表中的第一个非空排序集合中弹出一个或多个元素(即成员分数对)。

当使用 MIN 修饰符时,弹出的元素是第一个非空排序集中得分最低的元素。MAX 修饰符时会弹出得分最高的元素。可选 COUNT 参数可用于指定要弹出的元素数,默认设置为 1。弹出元素的数量是排序集的基数或 COUNT 值。

举例:

redis> ZADD myzset 1 "one" 2 "two" 3 "three"
(integer) 3
redis> ZMPOP 1 myzset MIN
1) "myzset"
2) 1) 1) "one"
      2) "1"
redis> ZRANGE myzset 0 -1 WITHSCORES
1) "two"
2) "2"
3) "three"
4) "3"
redis> ZMPOP 1 myzset MAX COUNT 10
1) "myzset"
2) 1) 1) "three"
      2) "3"
   2) 1) "two"
      2) "2"
redis> ZADD myzset2 4 "four" 5 "five" 6 "six"
(integer) 3
redis> ZMPOP 2 myzset myzset2 MIN COUNT 10
1) "myzset2"
2) 1) 1) "four"
      2) "4"
   2) 1) "five"
      2) "5"
   3) 1) "six"
      2) "6"
redis> ZRANGE myzset 0 -1 WITHSCORES
(empty array)
redis> ZMPOP 2 myzset myzset2 MAX COUNT 10
(error) object of type 'NoneType' has no len()
redis> ZRANGE myzset2 0 -1 WITHSCORES
(empty array)

🅰️ 11、ZRANK/ZREVRANK

ZRANK key member [WITHSCORE]

ZREVRANK key member [WITHSCORE]

ZRANK 返回排序集合中元素的排名,分数从低到高排序。

ZREVRANK 返回排序集合中元素的排名,分数从高到低排序。(逆序)

举例:

redis> ZADD myzset 1 "one" 2 "two" 3 "three"
(integer) 3
redis> ZRANK myzset "one"
(integer) 0
redis> ZRANK myzset "three"
(integer) 2
redis> ZREVRANK myzset "one"
(integer) 2
redis> ZREVRANK myzset "three"
(integer) 0

🅰️ 12、应用场景

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

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

销售 2 件商品 1001:

ZINCRBY goods:sellsort 2 1001

销售 10 件商品 1002:

ZINCRBY goods:sellsort 10 1002

查看商品销量前 10 名:

ZRANGE goods:sellsort 0 9 REV WITHSCORES

08、Redis 位图(bitmap)

文档介绍:https://redis.io/docs/data-types/bitmaps

相关命令:https://redis.io/commands/?group=bitmap

Redis 位图是字符串数据类型的扩展,可以将字符串视为位向量。

Bit arrays(or simply bitmaps,可以称之为位图),由 0 和 1 状态表现的二进制位的 bit 数组

在这里插入图片描述

说明:用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。

位图本质是数组,它是基于 String 数据类型的按位的操作。该数组由多个二进制位组成,每个二进制位都对应一个偏移量(称之为一个索引)。

Bitmap 支持的最大位数是 2^32 位,它可以极大的节约存储空间,使用 512M 内存就可以存储多达 42.9 亿的字节信息(2^32 = 4294967296)。最大长度为 512 MB。

Bitmap 的偏移量是从 0 开始算的。

主要用于状态统计场景:

  • 用户是否登陆过 Y / N,每日签到状态
  • 钉钉打卡上下班,签到状态统计

🅰️ 1、设置位值 SETBIT

SETBIT key offset value

设置或清除存储在键的字符串值中偏移处的位。该值可以是 0 或 1。

当键不存在时,将创建一个新的字符串值。偏移量参数必须大于或等于 0,并且小于 2^32(位图限制为512MB)。当键的字符串增长时,添加的位被设置为 0。

设置的返回值是存储在 key 的原始位的值。

示例:

redis> SETBIT key 7 1
(integer) 0
redis> SETBIT key 7 0
(integer) 1
redis> GET key
""

🅰️ 2、获取位值 GETBIT

GETBIT key offset

返回存储在 key 的字符串值中偏移量处的位值。

当偏移量超出字符串长度时,字符串被假定为具有 0 位的连续空间。

示例:

redis> SETBIT mykey 7 1
(integer) 0
redis> GETBIT mykey 0
(integer) 0
redis> GETBIT mykey 7
(integer) 1
redis> GETBIT mykey 100
(integer) 0

🅰️ 3、STRLEN

统计字节数占用多少。不是字符串长度而是占据几个字节,超过 8 位后自己按照 8 位一组一 byte 再扩容。

示例:

redis> SETBIT K2 0 1
(integer) 0
redis> SETBIT K2 7 1
(integer) 0
redis> STRLEN K2
(integer) 1
redis> SETBIT K2 8 1
(integer) 0
redis> STRLEN K2
(integer) 2

0-7 位一组;8-15 位一组。

🅰️ 4、BITCOUNT

BITCOUNT key [start end [BYTE | BIT]]

计算字符串中设置位的数量(总体计数)。全部键里面含有 1 的有多少个。

示例:

redis> SETBIT login:u1:202303 0 1
(integer) 0
redis> SETBIT login:u1:202303 1 1
(integer) 0
redis> SETBIT login:u1:202303 2 1
(integer) 0
redis> SETBIT login:u1:202303 3 1
(integer) 0
redis> SETBIT login:u1:202303 6 1
(integer) 0
redis> BITCOUNT login:u1:202303
(integer) 5
redis> BITCOUNT login:u1:202303 0 31
(integer) 5
redis> BITCOUNT login:u1:202303 0 31 BYTE
(integer) 5
redis> BITCOUNT login:u1:202303 0 31 BIT
(integer) 5

🅰️ 5、BITOP

BITOP <AND | OR | XOR | NOT> destkey key [key ...]

在多个键(包含字符串值)之间执行逐位操作,并将结果存储在目标键中。

BITOP 命令支持四种按位操作:ANDORXORNOT,因此调用该命令的有效形式为:

BITOP AND destkey srckey1 srckey2 srckey3 ... srckeyN

BITOP OR destkey srckey1 srckey2 srckey3 ... srckeyN

BITOP XOR destkey srckey1 srckey2 srckey3 ... srckeyN

BITOP NOT destkey srckey

NOT 是特殊的,它只接受一个输入键,因为它执行位的反转,所以它只作为一元运算符有意义。

操作结果始终存储在 destkey 键中。

示例:

一系统的用户有 1000W,做个用户 id 和位置的映射:比如 0 号位对应用户 id:uid-230101-don;比如 1 号位对应用户 id:uid-230101-xxx;…

现在需要计算2023年3月8日/9日两天都签到的用户数。

redis> SETBIT 20230308 0 1
(integer) 0
redis> SETBIT 20230308 1 1
(integer) 0
redis> SETBIT 20230308 2 1
(integer) 0
redis> SETBIT 20230308 3 1
(integer) 0
redis> SETBIT 20230309 0 1
(integer) 0
redis> SETBIT 20230309 2 1
(integer) 0
redis> BITCOUNT 20230308
(integer) 4
redis> BITCOUNT 20230309
(integer) 2
redis> BITOP AND destkey 20230308 20230309
(integer) 1
redis> BITCOUNT destkey
(integer) 2

得出 8日/9日 两天都签到的用户数 2 人。

🅰️ 6、应用场景

  • 一年365天,全年天天登陆占用多少字节?
redis> SETBIT UID1 0 1
(integer) 0
redis> SETBIT UID1 100 1
(integer) 0
redis> SETBIT UID1 364 1
(integer) 0
redis> BITCOUNT UID1
(integer) 3
redis> STRLEN UID1
(integer) 46

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

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

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

09、Redis 基数统计(HyperLogLog)

🅰️ ​1、场景需求

  • 统计某个网站的 UV、统计某个文章的 UV;
  • 搜索网站用户搜索关键词的数量;
  • 统计用户每天搜索不同词条个数;

UV:Unique Visitor,独立访客,一般理解为客户端 IP。需要去重考虑。

🅰️ 2、是什么

文档介绍:https://redis.io/docs/data-types/probabilistic/hyperloglogs

相关命令:https://redis.io/commands/?group=hyperloglog

Redis 在 2.8.9 版本添加了 HyperLogLog 结构。

Redis HyperLogLog 是用来做 基数统计 的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

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

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

去重复统计功能的基数估计算法 —— HyperLogLog

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

基数统计:用于统计一个集合中不重复的元素个数,就是对集合去重复后剩余元素的计算。

🅰️ 3、基本命令

1)添加

PFADD key [element [element ...]]

将所有元素添加到存储在指定为第一个参数的变量名称处的 HyperLogLog 数据结构中。如果键不存在,则创建该键。

返回:如果至少 1 个 HyperLogLog 内部寄存器被更改,则为 1。否则为 0。

2)获取基数

PFCOUNT key [key ...]

使用单个键调用时,返回由存储在指定键的 HyperLogLog 数据结构计算的近似基数,如果键不存在,则返回 0。

当使用多个键调用时,通过将存储在键的 HyperLogLogs 内部合并为临时 HyperLogLog,返回传递的 HyperLogLog 并集的近似基数。

返回的基数并不精确,而是近似值,标准误差为 0.81%。

3)合并一个或多个 HyperLogLog

PFMERGE destkey [sourcekey [sourcekey ...]]

将多个 HyperLogLog 值合并为一个唯一值,该值将近似于源 HyperLogLog 结构的观察集的并集的基数。

举例:

redis> PFADD hll1 foo bar zap a
(integer) 1
redis> PFADD hll2 a b c foo
(integer) 1
redis> PFMERGE hll3 hll1 hll2
"OK"
redis> PFCOUNT hll3
(integer) 6

10、Redis 地理空间(GEO)

文档介绍:https://redis.io/docs/data-types/geospatial

相关命令:https://redis.io/commands/?group=geo

Redis 在 3.2 版本以后增加了地理位置 GEO 的处理。

🅰️ ​1、场景需求

移动应用程序,可以查找当前位置最近的电动汽车充电站、美食店铺、附近的人等。

地球上的地理位置是使用二维的经纬度表示,经度范围 (-180, 180],纬度范围 (-90, 90],只要确定一个点的经纬度就可以名取得他在地球的位置。

举例:滴滴打车,最直观的操作就是实时记录更新各个车的位置,然后当要找车时,在数据库中查找距离我们(坐标 x0,y0)附近 R 公里范围内部的车辆。

使用如下伪 SQL:

select taxi from position where x0-r < x < x0 + r and y0-r < y < y0+r;

❌ 但是这样会有什么问题呢?

  1. 查询性能问题,如果并发高,数据量大这种查询是要搞垮数据库。
  2. 这个查询的是一个矩形访问,而不是以我为中心 R 公里为半径的圆形访问。
  3. 精准度的问题,地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差。

Redis 地理空间(GEO)数据类型对于查找给定半径或边界框内的附近点非常有用。

🅰️ 2、添加坐标 GEOADD

GEOADD key [NX | XX] [CH] longitude latitude member [longitude latitude member ...]

将指定的地理空间元素(经度、纬度、名称)添加到指定的键。数据以排序集的形式存储在键中,从而可以使用 GEOSEARCH 命令查询项目。

该命令采用标准格式 x,y 的参数,因此必须在纬度之前指定经度。可以索引的坐标是有限制的:非常靠近极点的区域是不可索引的。

注意:没有 GEODEL 命令,因为可以使用 ZREM 删除元素。GEO 索引结构只是一个排序集。

示例:

redis> GEOADD city 116.403963 39.915119 "天安门" 116.403414 39.924091 "故宫" 116.024067 40.362639 "长城"
(integer) 3
redis> TYPE city  ## GEO 索引结构只是一个排序集
"zset"
redis> ZRANGE city 0 -1
1) "天安门"
2) "故宫"
3) "长城"
redis> 

🅰️ 3、获取坐标 GEOPOS

GEOPOS key [member [member ...]]

从键里面返回所有给定位置元素的位置(经度和纬度)。

当通过坐标填充地理空间索引时,GEOADD 坐标会转换为 52 位 geohash,因此返回的坐标可能不完全是用于添加元素的坐标,但可能会引入小错误。

该命令返回一个数组,其中每个元素都是一个两元素数组,表示作为参数传递给命令的每个成员名称的经度和纬度 (x,y)。不存在的元素将返回 NULL。

示例:

redis> GEOADD city 116.403963 39.915119 "天安门" 116.403414 39.924091 "故宫" 116.024067 40.362639 "长城"
(integer) 3
redis> GEOPOS city 天安门 故宫 长城
1) 1) "116.40396326780319214"
   2) "39.91511970338637383"
2) 1) "116.40341609716415405"
   2) "39.92409008156928252"
3) 1) "116.02406591176986694"
   2) "40.36263993239462167"
redis>

🅰️ 4、GEOHASH

GEOHASH key [member [member ...]]

返回一个或多个元素在地理空间索引的排序集值中的位置的有效 Geohash 字符串。GEOHASH 算法生成的 base32 编码值。

该命令返回 11 个字符的 Geohash 字符串,因此与 Redis 内部 52 位表示相比,精度不会丢失。

redis> GEOHASH city 天安门 故宫 长城
1) "wx4g0f6f2v0"
2) "wx4g0gfqsj0"
3) "wx4t85y1kt0"
redis>

🅰️ 5、获取两个坐标的距离 GEODIST

GEODIST key member1 member2 [M | KM | FT | MI]

返回两个给定位置之间的距离。

计算距离时假设地球是一个完美的球体,因此在边缘情况下可能存在高达 0.5% 的误差。

参数:

  • M:米
  • KM:千米
  • FT:英尺
  • MI:英里

该命令以指定单位的双精度型(表示为字符串)形式返回距离,如果缺少一个或两个元素,则返回 NULL。

redis> GEODIST city 天安门 故宫 km
"0.9988"
redis> GEODIST city 天安门 故宫 m
"998.8332"
redis> 

🅰️ 6、查找范围内的坐标 GEORADIUS

从 Redis 6.2.0 版开始,此命令被视为已弃用。

使用 BYRADIUS 参数将其替换为 GEOSEARCH 和 GEOSEARCHSTORE 命令。

GEORADIUS key longitude latitude radius <M | KM | FT | MI>
  [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC]
  [STORE key] [STOREDIST key]

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

GEORADIUS city 116.418017 39.914402 10 km WITHDIST WITHCOORD COUNT 10 WITHHASH DESC
GEORADIUS city 116.418017 39.914402 10 km WITHDIST WITHCOORD WITHHASH COUNT 10 DESC

参数:

  • WITHDIST:在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
  • WITHCOORD:将位置元素的经度和维度也一并返回。
  • WITHHASH:以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
  • COUNT:限定返回的记录数。

举例:

redis> GEORADIUS city 116.418017 39.914402 10 km WITHDIST WITHCOORD WITHHASH COUNT 10 DESC
1) 1) "故宫"
   2) "1.6470"
   3) (integer) 4069885568908290
   4) 1) "116.40341609716415405"
      2) "39.92409008156928252"
2) 1) "天安门"
   2) "1.2016"
   3) (integer) 4069885555089531
   4) 1) "116.40396326780319214"
      2) "39.91511970338637383"
redis> 

🅰️ 7、查找范围内的坐标 GEORADIUSBYMEMBER

从 Redis 6.2.0 版开始,此命令被视为已弃用。

使用 BYRADIUS 和 FROMMEMBER 参数将其替换为 GEOSEARCH 和GEOSEARCHSTORE 命令。

GEORADIUSBYMEMBER key member radius <M | KM | FT | MI> [WITHCOORD]
  [WITHDIST] [WITHHASH] [COUNT count [ANY]] [ASC | DESC] [STORE key]
  [STOREDIST key]

此命令与 GEORADIUS 完全相同,唯一的区别是,它不采用经度和纬度值作为要查询区域的中心,而是采用已存在于由排序集表示的地理空间索引中的成员的名称。

以指定成员的位置作为查询的中心。

举例:

redis> GEORADIUSBYMEMBER city 天安门 10 km WITHDIST WITHCOORD WITHHASH COUNT 10 DESC
1) 1) "故宫"
   2) "0.9988"
   3) (integer) 4069885568908290
   4) 1) "116.40341609716415405"
      2) "39.92409008156928252"
2) 1) "天安门"
   2) "0.0000"
   3) (integer) 4069885555089531
   4) 1) "116.40396326780319214"
      2) "39.91511970338637383"
redis> 

🅰️ 8、查找范围内的坐标 GEOSEARCH

版本 6.2.0 新增命令,用于替换上边两个命令。

GEOSEARCH key <FROMMEMBER member | FROMLONLAT longitude latitude>
  <BYRADIUS radius <M | KM | FT | MI> | BYBOX width height <M | KM |
  FT | MI>> [ASC | DESC] [COUNT count [ANY]] [WITHCOORD] [WITHDIST]
  [WITHHASH]

返回地理空间信息的排序集的成员,这些成员位于给定形状指定的区域边界内。此命令扩展了 GEORADIUS 命令,因此除了在圆形区域内搜索外,它还支持在矩形区域内搜索。

使用此命令代替已弃用的 GEORADIUS 和 GEORADIUSBYMEMBER 命令。

查询的中心点由以下强制选项之一提供:

  • FROMMEMBER:使用排序集合中给定的现有 member 的位置。
  • FROMLONLAT:使用给定的 经度纬度 位置。

查询的形状由以下强制选项之一提供:

  • BYRADIUS:类似于 GEORADIUS,根据给定的 radius 在圆形区域内搜索。
  • BYBOX:在轴对齐的矩形内搜索,由 heightwidth 确定。

默认情况下,匹配项返回时未排序。要对它们进行排序,请使用以下两个选项之一:

  • ASC:相对于中心点,从最近到最远对返回的项目进行排序。
  • DESC:相对于中心点,从最远到最近对返回的项目进行排序。

示例:

redis> GEOSEARCH city FROMLONLAT 116.418017 39.914402 BYRADIUS 10 km
1) "天安门"
2) "故宫"
redis> GEOSEARCH city FROMLONLAT 116.418017 39.914402 BYRADIUS 10 km WITHDIST WITHCOORD WITHHASH
1) 1) "天安门"
   2) "1.2016"
   3) (integer) 4069885555089531
   4) 1) "116.40396326780319214"
      2) "39.91511970338637383"
2) 1) "故宫"
   2) "1.6470"
   3) (integer) 4069885568908290
   4) 1) "116.40341609716415405"
      2) "39.92409008156928252"
redis>
redis> GEOSEARCH city FROMMEMBER 天安门 BYBOX 10 10 km WITHDIST WITHCOORD WITHHASH
1) 1) "天安门"
   2) "0.0000"
   3) (integer) 4069885555089531
   4) 1) "116.40396326780319214"
      2) "39.91511970338637383"
2) 1) "故宫"
   2) "0.9988"
   3) (integer) 4069885568908290
   4) 1) "116.40341609716415405"
      2) "39.92409008156928252"
redis> 

11、Redis 流(Stream)

文档介绍:https://redis.io/docs/data-types/streams

相关命令:https://redis.io/commands/?group=stream

🅰️ 1、是什么

Redis 5.0 之前,Redis 消息队列的2种方案:

1)List 实现消息队列

按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

所以常用来做 异步队列使用,将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。

在这里插入图片描述

List 实现方式其实就是点对点的模式。

2)发布订阅模式(Pub/Sub)

Redis 发布订阅(pub/sub)有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。而且也没有 ACK 机制来保证数据的可靠性,假设一个消费者都没有,那消息就直接被丢弃了。

简单来说发布订阅(pub/sub)可以分发消息,但无法记录历史消息。

在这里插入图片描述

Redis 5.0 版本新增了一个更强大的数据结构 —— Stream。即 Redis 版的 MQ 消息中间件 + 阻塞队列。

Redis Stream 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

🅰️ 2、能干嘛

实现消息队列,它支持消息的持久化、支持自动生成全局唯一 ID、支持 ACK 确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠。

🅰️ 3、底层结构和原理说明

Redis Stream 的结构如下所示,它有一个消息链表,将所有加入的消息都串起来,每个消息都有一个唯一的 ID 和对应的内容:

在这里插入图片描述

每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用 xadd 命令追加消息时自动创建。

1)Message Content :消息内容。

2)Consumer Group :消费组,通过 XGROUP CREATE 命令创建,同一个消费组可以有多个消费者。

3)Last_delivered_id :游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。

4)Consumer :消费者,消费组中的消费者。

5)Pending_ids :消费者会有一个状态变量,用于记录被当前消费已读取但未 ACK 的消息 ID,如果客户端没有 ACK,这个变量里面的消息 ID 会越来越多,一旦某个消息被 ACK 它就开始减少。这个 pending_ids 变量在 Redis 官方被称之为 PEL(Pending Entries List),记录了当前已经被客户端读取的消息,但是还没有 ACK(Acknowledge character:确认字符),它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。

🅰️ 4、基本命令理论简介

1)队列相关指令

  • XADD :添加消息到队列末尾;
  • XTRIM :限制 Stream 的长度,如果已经超长会进行截取;
  • XDEL :删除消息;
  • XLEN :获取 Stream 中的消息长度;
  • XRANGE :获取消息列表(可以指定范围),忽略已经删除的消息;
  • XREVRANGE :和 XRANGE 相比区别在于反向获取,ID 从大到小;
  • XREAD :获取消息(阻塞/非阻塞),返回大于指定 ID 的消息。

2)消费组相关指令

  • XGROUP CREATE :创建消费者组;
  • XREADGROUP GROUP :读取消费者组中的消息;
  • XACK :将消息标记为"已处理";
  • XGROUP SETID :为消费者组设置新的最后递送消息 ID;
  • XGROUP DELCONSUMER :删除消费者;
  • XGROUP DESTROY :删除消费者组;
  • XPENDING :打印待处理消息的详细信息;
  • XCLAIM :转移消息的归属权(长期未被处理/无法处理的消息,转交给其他消费者组进行处理);
  • XINFO :打印 Stream \ Consumer \ Group 的详细信息;
    • XINFO GROUPS :打印消费者组的详细信息;
    • XINFO STREAM :打印 Stream 详细信息;
    • XINFO CONSUMERS :打印消费者的详细信息。

3)四个特殊符号

  • - + :最小和最大可能出现的 ID;
  • $ :表示只消费新的消息,当前流中最大的 ID,可用于将要到来的信息;
  • > :用于 XREADGROUP 命令,表示迄今还没有发送给组中使用者的信息,会更新消费者组的最后 ID;
  • * :用于 XADD 命令中,让系统自动生成 ID。

1、队列相关指令操作

🅱️ 1、添加消息到队列末尾 XADD

语法:

XADD key [NOMKSTREAM] [<MAXLEN | MINID> [= | ~] threshold
  [LIMIT count]] <* | id> field value [field value ...]

XADD 用于向 Stream 队列中添加消息,如果指定的 Stream 队列不存在,则该命令执行时会新建一个 Stream 队列。* 号表示服务器自动生成 MessageID,后面顺序跟着一堆 业务 field / value。

生成的消息 ID,有两部分组成,毫秒时间戳-该毫秒内产生的第1条消息

示例:

redis> XADD mystream * field1 value1 field2 value2 field3 value3
"1678430091999-1"

信息条目指的是序列号,在相同的毫秒下序列号从 0 开始递增,序列号是 64 位长度,理论上在同一毫秒内生成的数据量无法到达这个级别,因此不用担心序列号会不够用。millisecondsTime 指的是 Redis 节点服务器的本地时间,如果存在当前的毫秒时间戳比以前已经存在的数据的时间戳小的话(本地时间钟后跳),那么系统将会采用以前相同的毫秒创建新的 ID,也即 Redis 在增加信息条目时会检查当前 ID 与上一条目的 ID, 自动纠正错误的情况,一定要保证后面的 ID 比前面大,一个流中信息条目的 ID 必须是单调增的,这是流的基础。

客户端显示传入规则:

Redis 对于 ID 有强制要求,格式必须是 时间戳-自增ID 这样的方式,且后续 ID 大于前一个 ID。

Stream 的消息内容,也就是图中的 Message Content 它的结构类似 Hash 结构,以 field-value 的形式存在。

  • key :队列名称,如果不存在就创建。
  • id :消息 id,使用 * 表示由 Redis 生成;可以自定义,但是要自己保证递增性。
  • field value : 记录项。

🅱️ 2、获取消息列表 XRANGE

语法:

XRANGE key start end [COUNT count]

用于获取消息列表(可以指定范围),忽略删除的消息。

  • start :表示开始值,- 代表最小值。
  • end :表示结束值,+ 代表最大值。
  • count :表示最多获取多少个值。

示例:

redis> XADD stream1 * k1 v1 k2 v2
"1678431243920-0"
redis> XADD stream1 * k3 v3 k4 v4
"1678431257954-0"
redis> XRANGE stream1 - +
1) 1) "1678431243920-0"
   2) 1) "k1"
      2) "v1"
      3) "k2"
      4) "v2"
2) 1) "1678431257954-0"
   2) 1) "k3"
      2) "v3"
      3) "k4"
      4) "v4"

🅱️ 3、反向获取消息列表 XREVRANGE

语法:

XREVRANGE key end start [COUNT count]

与 XRANGE 的区别在于,获取消息列表元素的方向是相反的,end 在前,start 在后。

示例:

redis> XADD stream1 * k1 v1 k2 v2
"1678431405813-0"
redis> XADD stream1 * k3 v3 k4 v4
"1678431409400-0"
redis> XREVRANGE stream1 + -
1) 1) "1678431409400-0"
   2) 1) "k3"
      2) "v3"
      3) "k4"
      4) "v4"
2) 1) "1678431405813-0"
   2) 1) "k1"
      2) "v1"
      3) "k2"
      4) "v2"
redis> 

🅱️ 4、删除消息 XDEL

语法:

XDEL key id [id ...]

使用 XDEL 删除消息(ID)。从流中删除指定的条目,并返回实际删除的条目数。

示例:

redis> XADD stream1 * k1 v1
"1678431756796-0"
redis> XADD stream1 * k2 v2
"1678431763476-0"
redis> XRANGE stream1 - +
1) 1) "1678431756796-0"
   2) 1) "k1"
      2) "v1"
2) 1) "1678431763476-0"
   2) 1) "k2"
      2) "v2"
redis> XDEL stream1 1678431756796-0
(integer) 1
redis> XRANGE stream1 - +
1) 1) "1678431763476-0"
   2) 1) "k2"
      2) "v2"
redis> 

🅱️ 5、获取消息长度 XLEN

语法:

XLEN key

用于获取 Stream 队列的消息的长度。

redis> XLEN stream1
(integer) 1

🅱️ 6、截取消息长度 XTRIM

语法:

XTRIM key <MAXLEN | MINID> [= | ~] threshold [LIMIT count]

用于对 Stream 的长度进行截取,如超长会进行截取。

1)MAXLEN

允许的最大长度,只要流的长度超过指定的 threshold,就移除条目,其中 threshold 是正整数。

redis> XADD stream1 * k1 v1
"1678435003936-0"
redis> XADD stream1 * k2 v2
"1678435010172-0"
redis> XADD stream1 * k3 v3
"1678435015141-0"
redis> XRANGE stream1 - +
1) 1) "1678435003936-0"
   2) 1) "k1"
      2) "v1"
2) 1) "1678435010172-0"
   2) 1) "k2"
      2) "v2"
3) 1) "1678435015141-0"
   2) 1) "k3"
      2) "v3"
redis> XTRIM stream1 MAXLEN 2
(integer) 1
redis> XRANGE stream1 - +
1) 1) "1678435010172-0"
   2) 1) "k2"
      2) "v2"
2) 1) "1678435015141-0"
   2) 1) "k3"
      2) "v3"
redis> 

2)MINID

允许的最小 ID,从某个 ID 值开始比该 ID 值小的将会被移除。其中 threshold 是流 ID。

redis> XADD stream1 * k1 v1
"1678436286856-0"
redis> XADD stream1 * k2 v2
"1678436291292-0"
redis> XADD stream1 * k3 v3
"1678436296114-0"
redis> XRANGE stream1 - +
1) 1) "1678436286856-0"
   2) 1) "k1"
      2) "v1"
2) 1) "1678436291292-0"
   2) 1) "k2"
      2) "v2"
3) 1) "1678436296114-0"
   2) 1) "k3"
      2) "v3"
redis> XTRIM stream1 MINID 1678436296114-0
(integer) 2
redis> XRANGE stream1 - +
1) 1) "1678436296114-0"
   2) 1) "k3"
      2) "v3"
redis> 

🅱️ 7、获取消息(阻塞/非阻塞) XREAD

语法:https://redis.io/commands/xread

XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]

用于获取消息(阻塞/非阻塞),只会返回大于指定 ID 的消息。从一个或多个流中读取数据,仅返回 ID 大于调用者给定的最后接收到的 ID 的条目。

COUNT :对于每个流,最多读取每个流多少条消息。

BLOCK :是否已阻塞的方式读取消息,默认不阻塞,如果 milliseconds 设置为 0,表示永远阻塞。

1)非阻塞

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

0-0 :代表从最小的 ID 开始获取 Stream 中的消息,当不指定 count,将会返回 Stream 中的所有消息,注意也可以使用 0 / 00 / 000 同等效果。

> XADD stream1 * k1 v1
1678438374296-0
> XADD stream1 * k2 v2
1678438380663-0
> XRANGE stream1 - +
1678438374296-0
k1
v1
1678438380663-0
k2
v2
> XREAD COUNT 1 STREAMS stream1 $
null
> XREAD COUNT 1 STREAMS stream1 0-0
stream1
1678438374296-0
k1
v1
> XREAD COUNT 3 STREAMS stream1 00
stream1
1678438374296-0
k1
v1
1678438380663-0
k2
v2

2)阻塞

客户端 B:BLOCK 0 此时会永久阻塞

> XREAD COUNT 1 BLOCK 0 STREAMS stream1 $

客户端 A:添加消息

> XADD stream1 * k3 v3
1678438810766-0

客户端 A 添加消息后,客户端 B 则会取出最新消息:

> XREAD COUNT 1 BLOCK 0 STREAMS stream1 $
stream1
1678438810766-0
k3
v3

3)小总结(类似 Java 中的阻塞队列)

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

在这里插入图片描述

2、消费组相关指令操作

🅱️ 1、创建消费者组 XGROUP CREATE

语法:

XGROUP CREATE key group <id | $> [MKSTREAM] [ENTRIESREAD entries-read]

创建消费者组的时候必须指定 ID。

ID 为 0 表示从头开始消费。为 $ 表示只消费新的消息,当前 Stream 内消息会全部忽略。

当同名消费者组已存在时,该命令返回错误 BUSYGROUP。默认情况下,创建消费组的目标 key 流是需要存在,如果不存在则返回错误。如果流不存在,可以使用可选参数 MKSTREAM 来自动创建长度为 0 的流。

  • 0 :表示从 Stream 头部开始消费;
  • $ :表示从 Stream 尾部开始消费;

示例:

> XGROUP CREATE mystream mygroupA 0
OK
> XGROUP CREATE mystream mygroupB $
OK

🅱️ 2、读取消息 XREADGROUP GROUP

语法:

XREADGROUP GROUP group consumer [COUNT count] [BLOCK milliseconds]
  [NOACK] STREAMS key [key ...] id [id ...]

读取消费组中的消息。

  • > :表示从第一条尚未被消费的消息开始读取。

示例:

消费组 mygroupA 内的消费者 consumer1 从 stream1 消息队列中读取所有消息。

> XGROUP CREATE stream1 mygroupA 0
OK
> XGROUP CREATE stream1 mygroupB $
OK
> XREADGROUP GROUP mygroupA consumer1 STREAMS stream1 >
stream1
1678438374296-0
k1
v1
1678438380663-0
k2
v2
> XREADGROUP GROUP mygroupA consumer1 STREAMS stream1 >
null
> XREADGROUP GROUP mygroupA consumer2 STREAMS stream1 >
null

Stream 中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费组内的其他消费者读取了,即同一个消费组里的消费者不能消费同一条消息。上述例子的 XREADGROUP 命今再执行一次,此时读到的就是空值。

❓ 2)不同消费组的消费者可以消费同一条消息吗?

> XGROUP CREATE stream1 mygroupC 0
OK
> XREADGROUP GROUP mygroupC consumer1 STREAMS stream1 >
stream1
1678438374296-0
k1
v1
1678438380663-0
k2
v2
> XREADGROUP GROUP mygroupC consumer1 STREAMS stream1 >
null

可见,不同消费组的消费者可以消费同一条消息。

❓ 3)消费组的目的?

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

举例:

> XGROUP CREATE stream1 mygroupD 0
OK
> XREADGROUP GROUP mygroupD consumer1 COUNT 1 STREAMS stream1 >
stream1
1678438374296-0
k1
v1
> XREADGROUP GROUP mygroupD consumer2 COUNT 1 STREAMS stream1 >
stream1
1678438380663-0
k2
v2
> XREADGROUP GROUP mygroupD consumer3 COUNT 1 STREAMS stream1 >
null

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

解答:

Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息保底措施,直到消费者使用 XACK 命令通知 Streams “消息已经处理完成”。

消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 XACK 命令确认消息已经被消费完成。

在这里插入图片描述

🅱️ 3、查询消费者消费的信息 XPENDING

XPENDING key group [[IDLE min-idle-time] start end count [consumer]]

查询每个消费组内所有消费者(已读取、但尚未确认)的消息。

举例:

> XPENDING stream1 mygroupA
3
1678438374296-0    #所有消费者读取的消息最小ID
1678438810766-0    #所有消费者读取的消息最大ID
consumer1     #一个消费者一次性读3条
3
> XPENDING stream1 mygroupD
3
1678438374296-0
1678438810766-0
consumer1    #以下3个消费者分别读取1条
1
consumer2
1
consumer3
1

查看某个消费者具体读取了哪些数据。

返回 4 个属性:

  • 消息的 ID;
  • 获取消息且仍需确认的消费者的名称。我们称其为消息的当前所有者;
  • 自上次将此消息传递给此消费者以来经过的毫秒数;
  • 该消息被传送的次数。

举例:

> XPENDING stream1 mygroupD - + 10 consumer1
1678438374296-0
consumer1
815248
1

消费者 consumer1 已读取的消息的 ID 是 1678438374296-0。一旦消息 1678438374296-0 被 consumer1 处理了 consumer1 就可以使用 XACK 命令通知 Streams,然后这条消息就会被删除。

🅱️ 4、消息处理完成确认 XACK

XACK key group id [id ...]

向消息队列确认消息处理已完成。

举例:

> XPENDING stream1 mygroupC - + 10 consumer1    #没有ack之前有3条记录读取过
1678438374296-0
consumer1
1150855
1
1678438380663-0
consumer1
1150855
1
1678438810766-0
consumer1
1150855
1
> XACK stream1 mygroupC 1678438374296-0    #ack成功确认一条,返回1
1
> XPENDING stream1 mygroupC - + 10 consumer1    #ack签收了1条,剩2条没有ack
1678438380663-0
consumer1
1176623
1
1678438810766-0
consumer1
1176623
1

🅱️ 5、查询信息 XINFO

XINFO 用于信息 Stream \ Consumer \ Group 的详细信息。

1)获取消费者组中的消费者信息

XINFO CONSUMERS key group

返回消费者列表,属性:

  • name:消费者的名
  • pending:PEL 中的条目数:消费者的待处理消息,这些消息是已发送但尚未确认的消息
  • idle:自消费者上次尝试交互以来经过的毫秒数(例如:XREADGROUP、XCLAIM、XAUTOLAIM)
  • inactive:自消费者上次成功交互以来经过的毫秒数(例如:实际将某些条目读取到 PEL 中的 XREADGROUP,实际声明某些条目的 XCLAIM / XAUTOCLAIM)

示例:

> XINFO CONSUMERS mystream mygroup
1) 1) name
   2) "Alice"
   3) pending
   4) (integer) 1
   5) idle
   6) (integer) 9104628
   7) inactive
   8) (integer) 18104698

2)获取消费者组的信息

XINFO GROUPS key

返回消费者组列表,属性:

  • name : 消费者组的名称
  • consumers : 组中消费者的数量
  • pending :组待处理条目列表(PEL)的长度,这些消息已发送但尚未确认
  • last-delivered-id:最后交付给该组消费者的消息的 ID
  • entries-read :传递给组消费者的最后一个消息的逻辑“读取计数器”
  • lag :流中仍在等待传递给组消费者的消息数,或者当无法确定该数字时为 NULL。

示例:

> XINFO GROUPS mystream
1)  1) "name"
    2) "mygroup"
    3) "consumers"
    4) (integer) 2
    5) "pending"
    6) (integer) 2
    7) "last-delivered-id"
    8) "1638126030001-0"
    9) "entries-read"
   10) (integer) 2
   11) "lag"
   12) (integer) 0

3)获取流的信息

XINFO STREAM key [FULL [COUNT count]]

示例:

> XINFO STREAM mystream
 1) "length"
 2) (integer) 2
 3) "radix-tree-keys"
 4) (integer) 1
 5) "radix-tree-nodes"
 6) (integer) 2
 7) "last-generated-id"
 8) "1638125141232-0"
 9) "max-deleted-entry-id"
10) "0-0"
11) "entries-added"
12) (integer) 2
13) "groups"
14) (integer) 1
15) "first-entry"
16) 1) "1638125133432-0"
    2) 1) "message"
       2) "apple"
17) "last-entry"
18) 1) "1638125141232-0"
    2) 1) "message"
       2) "banana"

12、Redis 位域(bitfield)

了解即可。

文档介绍:https://redis.io/docs/data-types/bitfields

🅰️ 1、是什么

Redis 位域允许设置、递增和获取任意位长度的整数值。例如,可以操作从无符号 1 位整数到有符号 63 位整数的任何值。

这些值使用二进制编码的 Redis 字符串存储。位域支持原子读、写和增量操作,使它们成为管理计数器和类似数值的不错选择。

BITFIELD 命令可以将一个 Redis 字符串看作是一个由二进制位组成的数组,并对这个数组中任意偏移进行访问 。 可以使用该命令对一个有符号的 5 位整型数的第 1234 位设置指定值,也可以对一个 31 位无符号整型数的第 4567 位进行取值。类似地,本命令可以对指定的整数进行自增和自减操作,可配置的上溢和下溢处理操作。

BITFIELD 命今可以在一次调用中同时对多个位范围进行操作作:它接受一系列待执行的操作作为参数,并返回一个数组,数组中的每个元素就是对应操作的执行结果。

🅰️ 2、能干嘛

  • 位域修改
  • 溢出控制

BITFIELD 命令的作用在于它能够将很多小的整数储存到一个长度较大的位图中,又或者将一个非常庞大的键分割为多个较小的键来进行储存,从而非常高效地使用内存,使得 Redis 能够得到更多不同的应用一特别是在实时分析领域 BITFIELD 能够以指定的方式对计算溢出进行控制的能力,使得它可以被应用于这一领域。

大白话讲就是:

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

hello 等价于 01101000 01100101 01101100 01101100 01101111

🅰️ 3、基本命令语法

以原子方式设置、递增和读取一个或多个值。

BITFIELD key [GET encoding offset | [OVERFLOW <WRAP | SAT | FAIL>]
  <SET encoding offset value | INCRBY encoding offset increment>
  [GET encoding offset | [OVERFLOW <WRAP | SAT | FAIL>]
  <SET encoding offset value | INCRBY encoding offset increment>
  ...]]
  • GET encoding offset :返回指定的位域;
  • SET encoding offset value :设置指定位域的值并返回它的原值;
  • INCRBY encoding offset increment :自增或自减(increment 为负数)指定位域的值并返回它的新值;
  • OVERFLOW <WRAP | SAT | FAIL> :设置溢出行为来改变调用 INCRBY 指令的后序操作;

当需要一个整型时,有符号整型需在位数前加 i,无符号在位数前加 u。例如,u8 是一个 8 位的无符号整型, i16 是一个 16 位的有符号整型。

🅰️ 4、基本命令操作

Ascii 码表:https://ascii.org.cn

1)返回指定的位域:GET encoding offset

对照表:

在这里插入图片描述

hello 等价于 01101000 01100101 01101100 01101100 01101111

> SET fieldkey hello
OK
> BITFIELD fieldkey GET i8 0
104
> BITFIELD fieldkey GET i8 8
101
> BITFIELD fieldkey GET i8 16
108
> BITFIELD fieldkey GET i8 24
108
> BITFIELD fieldkey GET i8 32
111

BITFIELD 命令可以将一个 Redis 字符串看作是一个由二进制位组成的数组,并对这个数组中任意偏移进行访问 。可以使用该命令对一个有符号的 5 位整型数的第 1234 位设置指定值,也可以对一个 31 位无符号整型数的第 4567 位进行取值。类似地,本命令可以对指定的整数进行自增和自减操作,可配置的上溢和下溢处理操作。

2)设置指定位域的值并返回它的原值:SET encoding offset value

> SET fieldkey hello
OK
> BITFIELD fieldkey GET i8 0
104
> BITFIELD fieldkey GET i8 8
101
> BITFIELD fieldkey GET i8 16
108
> BITFIELD fieldkey GET i8 24
108
> BITFIELD fieldkey GET i8 32
111
> BITFIELD fieldkey SET i8 8 120   #从第9个位开始,将接下来8个位用有符号数120(字母x)替换
101
> GET fieldkey
hxllo

3)自增或自减(increment 为负数)指定位域的值并返回它的新值:INCRBY encoding offset increment

> SET fieldkey hello
OK
> BITFIELD fieldkey INCRBY u4 2 1   #从第3个位开始,对接下来的4位无符号数+1
11
> BITFIELD fieldkey INCRBY u4 2 1
12
> BITFIELD fieldkey INCRBY u4 2 1
13
> BITFIELD fieldkey INCRBY u4 2 1
14
> BITFIELD fieldkey INCRBY u4 2 1
15
> BITFIELD fieldkey INCRBY u4 2 1   #默认overflow为wrap,即循环溢出
0

4)溢出控制 OVERFLOW <WRAP | SAT | FAIL>

WRAP :使用回绕(wrap around)方法处理有符号整数和无符号整数的溢出情况。

> SET k1 a
OK
> BITFIELD k1 GET i8 0  #a对应的ascII码值97
97
> BITFIELD k1 SET i8 0 128  #i8表示有符号8位二进制,范围 (-128-127)
97
> BITFIELD k1 GET i8 0  #默认overflow为wrap,即循环溢出
-128

SAT :使用饱和计算(saturation arithmetic)方法处理溢出,下溢计算的结果为最小的整数值,而上溢计算的结果为最大的整数值。

> SET k1 a
OK
> BITFIELD k1 GET i8 0
-128
> BITFIELD k1 OVERFLOW SAT SET i8 0 128
-128
> BITFIELD k1 GET i8 0  #上溢 最大的整数值
127

FAIL :命令将拒绝执行那些会导致上溢或者下溢情况出现的计算,并向用户返回空值表示计算未被执行。

> BITFIELD k1 OVERFLOW FAIL SET i8 0 222
null

🌈 Redis 持久化

01、总体介绍

官网介绍:https://redis.io/docs/management/persistence

持久性是指将数据写入持久存储,如固态磁盘(SSD)。Redis 提供了一系列持久性选项。其中包括:

  • RDB(Redis Database):RDB 持久性以指定的时间间隔执行数据集的时间点快照。
  • AOF(Append Only File):AOF 持久性记录服务器接收到的每个写入操作。然后可以在服务器启动时再次回放这些操作,重建原始数据集。使用与 Redis 协议本身相同的格式记录命令。
  • 无持久性(No persistence):可以完全禁用持久性。在纯缓存时使用。
  • RDB + AOF:也可以在同一实例中组合使用 AOF 和 RDB。

在这里插入图片描述

02、持久化 RDB(Redis Database)

🅰️ 1、是什么

RDB(Redis 数据库):RDB 持久性以指定的时间间隔执行数据集的时间点快照。

把某一时刻的数据和状态以文件的形式写到磁盘上,也就是快照。这样一来即使故障宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件就称为 RDB 文件(dump.rdb)。RDB 就是 Redis DataBase 的缩写。

🅰️ 2、能干嘛

在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是 Snapshot 内存快照,它恢复时再将硬盘快照文件直接读回到内存里。

Redis 的数据都在内存中,保存备份时它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中。

RDB 保存的是 dump.rdb 文件。

1、RDB 使用案例

文档说明:https://redis.io/docs/management/persistence/#snapshotting

工作原理:https://redis.io/docs/management/persistence/#how-it-works

默认情况下,Redis 将数据集的快照保存在磁盘上,保存在一个名为 dump.rdb 的二进制文件中。如果数据集中至少有 M 个更改,可以配置 Redis,使其每隔 N 秒保存一次数据集,也可以手动调用 SAVEBGSAVE 命令。

🅱️ 1、配置文件的改变

自动触发。

1)Redis 6.2 之前的版本

redis.conf 配置文件中的 SNAPSHOTTING 下配置 save 参数,来触发 Redis 的 RDB 持久化条件,比如 save m n :表示 m 秒内数据集存在 n 次修改时,自动触发 bgsave

  • save 900 1 :每隔 900s (15min),如果有超过 1 个 key 发生了变化,就写一份新的 RDB 文件;
  • save 300 10 :每隔 300s (5min),如果有超过 10 个 key 发生了变化,就写一份新的 RDB 文件;
  • save 60 10000 :每隔 60s (1min),如果有超过 10000 个 key 发生了变化,就写一份新的 RDB 文件;
################################ SNAPSHOTTING  ################################
#
# Save the DB on disk:
#
#   save <seconds> <changes>
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behaviour will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""

save 900 1
save 300 10
save 60 10000

2)Redis 6.2 以及 Redis 7.0 之后的版本

################################ SNAPSHOTTING  ################################

# Save the DB to disk.
#
# save <seconds> <changes> [<seconds> <changes> ...]
#
# Redis will save the DB if the given number of seconds elapsed and it
# surpassed the given number of write operations against the DB.
#
# Snapshotting can be completely disabled with a single empty string argument
# as in following example:
#
# save ""
#
# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 change was performed
#   * After 300 seconds (5 minutes) if at least 100 changes were performed
#   * After 60 seconds if at least 10000 changes were performed
#
# You can set these explicitly by uncommenting the following line.
#
save 3600 1 300 100 60 10000

🅱️ ​2、自动触发演示

1)修改默认配置

Redis 7.0 版本,按照 redis.conf 里配置的规则:

save <seconds> <changes> [<seconds> <changes> ...]

本次案例配置为 5 秒 2 次修改:

save 5 2

2)修改 dump 文件保存路径

默认:

# The filename where to dump the DB
dbfilename dump.rdb

# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir ./

自定义修改的路径:

dir ./db

可以进入 Redis 里用 CONFIG GET dir 获取已配置的路径:

CONFIG GET dir

3)修改 dump 文件名称:

# The filename where to dump the DB
dbfilename dump6379.rdb

4)触发备份。当 5 秒 2 次修改时触发

> SET K1 V1
OK
> SET K2 V2
OK

路径 db 文件下,生成文件:dump6379.rdb

5)如何恢复

🌵 物理恢复,服务和备份分机隔离

将备份文件 dump.rdb 移动到 Redis 安装目录并启动服务即可。

将 dump6379_bak.rdb 备份文件修改为 dump6379.rdb,启动 Redis 服务:

> KEYS *
K2
K1

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

❌ 备份成功后故意用命令 flushdb 清空 Redis,看看是否可以恢复数据?不能!!!

将 dump6379.rdb 备份成 dump6379_bak.rdb,然后 flushdb 执行后会默认自动生成 rdb 文件。

执行 flushall / flushdb 命令也会产生 dump.rdb 文件,但里面是空的,无数据。

🅱️ 3、手动触发演示

Redis 提供了两个命令来手动⽣成 RDB ⽂件,分别是 SAVEBGSAVE 命令。

1)命令 SAVE

文档介绍:https://redis.io/commands/save

在主程序中执⾏会 阻塞 当前 Redis 服务器,直到持久化工作完成执行 save 命令期间,Redis 不能处理其他命令,线上禁止使用

在这里插入图片描述

> SAVE
OK

2)命令 BGSAVE(默认)

文档介绍:https://redis.io/commands/bgsave

语法:

BGSAVE [SCHEDULE]

Redis 会在后台异步进行快照操作,不阻塞 ,快照同时还可以响应客户端请求,该触发方式会 fork 一个子进程由子进程复制持久化过程。

执行 BGSAVE,正常情况下,会立即返回 OK。Redis forks ,父进程继续为客户端提供服务,子进程将数据保存在磁盘上,然后退出。

如果已经有后台保存在运行,或者有另一个非后台保存进程在运行,特别是正在进行的 AOF 重写,则会返回错误。

如果使用 BGSAVE SCHEDULE,则当 AOF 重写正在进行时,该命令将立即返回 OK,并计划在下一次机会运行后台保存。

客户端可以使用 LASTSAVE 命令来检查操作是否成功。

返回值:

如果 BGSAVE 启动正确,则启动后台保存,或者与 SCHEDULE 子命令一起使用时安排后台保存。

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

Redis 会使用 BGSAVE 对当前内存中的所有数据做快照,这个操作是子进程在后台完成的,这就允许主进程同时可以修改数据。

在这里插入图片描述

> BGSAVE
Background saving started

3)命令 LASTSAVE

可以通过 LASTSAVE 命令获取最后一次成功执行快照的时间。

> LASTSAVE
1678960442

Linux 查看:

date -d @1678960442

2、RDB 优缺点

🅱️ 1、优点

文档介绍:https://redis.io/docs/management/persistence/#rdb-advantages

  • RDB 是 Redis 数据的一个非常紧凑的单文件时间点表示。RDB 文件非常适合备份。例如,可能在最近的 24 小时内每小时归档一次 RDB 文件,并在 30 天内每天保存一个 RDB 快照。这可以在发生灾难时轻松恢复不同版本的数据集。
  • RDB 非常适合灾难恢复,它是一个可以传输到远程数据中心或 Amazon S3(可能已加密)的压缩文件。
  • RDB 最大限度地提高了 Redis 的性能,因为 Redis 父进程为了持久化而需要做的唯一工作就是派生一个将完成所有其余工作的子进程。父进程永远不会执行磁盘 I/O 或类似操作。
  • 与 AOF 相比,RDB 允许使用大数据集更快地重启。
  • 在副本上,RDB 支持重启和故障转移后的部分重新同步。

总结:

  1. 适合大规模的数据恢复;
  2. 按照业务定时备份;
  3. 对数据完整性和一致性要求不高; ⭕️
  4. RDB 文件在内存中的加载速度要比 AOF 快得多。

🅱️ 2、缺点

文档介绍:https://redis.io/docs/management/persistence/#rdb-disadvantages

  • 如果需要在 Redis 停止工作时(例如断电后)将数据丢失的可能性降到最低,那么 RDB 并不好。您可以配置生成 RDB 的不同保存点(例如,在对数据集至少 5 分钟和 100 次写入之后,可以有多个保存点)。但是,通常会每五分钟或更长时间创建一次 RDB 快照,因此,如果 Redis 由于任何原因在没有正确关闭的情况下停止工作,您应该准备好丢失最新分钟的数据。
  • RDB 需要经常 fork() 以便使用子进程在磁盘上持久化。如果数据集很大,fork()可能会很耗时,并且如果数据集很大并且 CPU 性能不是很好,可能会导致 Redis 停止为客户端服务几毫秒甚至一秒钟。AOF 也需要 fork() 但频率较低,您可以调整要重写日志的频率,而不需要对持久性进行任何权衡。

总结:

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

🅱️ 3、数据丢失案例

Redis 意外宕机将丢失从当前至最近一次快照期间的数据。

# 正常录入数据
> SET K1 V2
OK
> SET K2 V2
OK
# 此时满足 5秒修改2次 生成 dump 文件
> SET K3 V3  
OK
# 服务出故障(kill -9 故意模拟意外 down 机)

# Redis 重启恢复,查看数据
> KEYS *
K2
K1
# 数据K3丢失了,证明RDB数据丢失缺点

🅰️ 3、如何检查修复 dump.rdb 文件

如何检查修复损坏的 dump.rdb 文件呢?

当服务出故障造成 dump.rdb 文件数据不完整,可以使用 Redis 自带的 redis-check-rdb 工具进行修复。

>redis-check-rdb dumpfile/dump6379.rdb
[offset 0] Checking RDB file dumpfile/dump6379.rdb
[offset 27] AUX FIELD redis-ver = '7.0.9'
[offset 41] AUX FIELD redis-bits = '64'
[offset 53] AUX FIELD ctime = '1679033301'
[offset 65] AUX FIELD used-mem = '0'
[offset 81] AUX FIELD aof-preamble = '0'
[offset 83] Selecting DB ID 0
[offset 109] Checksum OK
[offset 109] \o/ RDB looks OK! \o/
[info] 2 keys read
[info] 0 expires
[info] 0 already expired
>

🅰️ 4、哪些情况会触发 RDB 快照

  • 配置文件中默认的快照配置;
  • 手动 SAVEBGSAVE 命令;
  • 执行 FLUSHALLFLUSHDB 命令也会产生 dump.rdb 文件,但数据是空的;
  • 执行 SHUTDOWN 且没有设置开启 AOF 持久化;
  • 主从复制时,主节点自动触发;

🅰️ 5、如何禁用快照

1)动态所有停止 RDB 保存规则的方法,执行命令:

redis-cli config set save ""

2)配置文件快照禁用:

# Snapshotting can be completely disabled with a single empty string argument
# as in following example:
#
save ""

3、RDB 优化配置项详解

配置文件 SNAPSHOTTING 模块:

🅱️ 1、​保存数据到磁盘

################################ SNAPSHOTTING  ################################

# Save the DB to disk.
#
# save <seconds> <changes> [<seconds> <changes> ...]
#
# Redis will save the DB if the given number of seconds elapsed and it
# surpassed the given number of write operations against the DB.
#
# Snapshotting can be completely disabled with a single empty string argument
# as in following example:
#
# save ""
#
# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 change was performed
#   * After 300 seconds (5 minutes) if at least 100 changes were performed
#   * After 60 seconds if at least 10000 changes were performed
#
# You can set these explicitly by uncommenting the following line.
#
save 3600 1 300 100 60 10000

🅱️ 2、后台保存出错时停止继续写入

默认 yes。如果配置成 no,表示不在乎数据不一致或者有其他的手段发现和控制这种不一致,那么在快照写入失败时,也能确保 Redis 继续接受新的写请求。

# By default Redis will stop accepting writes if RDB snapshots are enabled
# (at least one save point) and the latest background save failed.
# This will make the user aware (in a hard way) that data is not persisting
# on disk properly, otherwise chances are that no one will notice and some
# disaster will happen.
#
# If the background saving process will start working again Redis will
# automatically allow writes again.
#
# However if you have setup your proper monitoring of the Redis server
# and persistence, you may want to disable this feature so that Redis will
# continue to work as usual even if there are problems with disk,
# permissions, and so forth.
stop-writes-on-bgsave-error yes

🅱️ 3、是否压缩 RDB 文件

默认 yes。对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,Redis 会采用 LZF 算法进行压缩。如果你不想消耗 CPU 来进行压缩的话,可以设置为关闭此功能。

# Compress string objects using LZF when dump .rdb databases?
# By default compression is enabled as it's almost always a win.
# If you want to save some CPU in the saving child set it to 'no' but
# the dataset will likely be bigger if you have compressible values or keys.
rdbcompression yes

🅱️ 4、RDB 文件合法性的校验

默认 yes。在存储快照后,还可以让 Redis 使用 CRC64 算法来进行数据校验,但是这样做会增加大约 10% 的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能。

# Since version 5 of RDB a CRC64 checksum is placed at the end of the file.
# This makes the format more resistant to corruption but there is a performance
# hit to pay (around 10%) when saving and loading RDB files, so you can disable it
# for maximum performances.
#
# RDB files created with checksum disabled have a checksum of zero that will
# tell the loading code to skip the check.
rdbchecksum yes

🅱️ 5、RDB 文件名

# The filename where to dump the DB
dbfilename dump.rdb

🅱️ 6、是否删除主从复制时产生 RDB 文件

在没有持久性的情况下删除复制中使用的 RDB 文件启用。默认情况下 no,此选项是禁用的。

# Remove RDB files used by replication in instances without persistence
# enabled. By default this option is disabled, however there are environments
# where for regulations or other security concerns, RDB files persisted on
# disk by masters in order to feed replicas, or stored on disk by replicas
# in order to load them for the initial synchronization, should be deleted
# ASAP. Note that this option ONLY WORKS in instances that have both AOF
# and RDB persistence disabled, otherwise is completely ignored.
#
# An alternative (and sometimes better) way to obtain the same effect is
# to use diskless replication on both master and replicas instances. However
# in the case of replicas, diskless is not always an option.
rdb-del-sync-files no

🅱️ 7、工作目录

RDB 文件的写入目录。AOF 文件也使用此目录。此目录必须是存在的。

# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir ./

03、持久化 AOF(Append Only File)

文档介绍:https://redis.io/docs/management/persistence

🅰️ 1、是什么

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

默认情况下,Redis 是没有开启 AOF 的。开启 AOF 功能需要设置配置:appendonly yes

AOF 保存的是 appendonly.aof 文件。

🅰️ 2、能干嘛

文档介绍:https://redis.io/docs/management/persistence/#append-only-file

RDB 模式不是完美的。如果运行 Redis 的计算机停止运行,电源线故障,或者不小心 kill -9 关闭实例,那么写入 Redis 的最新数据将丢失。虽然这对某些应用程序来说可以接受,但对于需要完全持久性的应用,这些情况,单独使用 Redis 快照是不可行的。

AOF 是 Redis 的一种替代、完全持久的策略。它在 1.1 版中提供。

开启 AOF,每当 Redis 收到更改数据集的命令(例如 SET)时,它都会将其附加到 AOF 中。当重新启动 Redis 时,它将重新读取 AOF 中命令以重建数据。

自 Redis 7.0.0,Redis 使用了多部分 AOF 机制。也就是说,原始的单个 AOF 文件被拆分为基本文件(最多一个)和增量文件(可能有多个)。基本文件表示重写 AOF 时存在的数据的初始快照(RDB 或 AOF 格式)。增量文件包含自上一个基本 AOF 文件创建以来的增量更改。所有这些文件都放在一个单独的目录中,并由清单文件跟踪。

🅰️ 3、AOF 持久化工作流程

在这里插入图片描述

  1. Client 作为命令的来源,会有多个源头以及源源不断的请求命令。
  2. 在这些命令到达 Redis Server 以后并不是直接写入 AOF 文件,会将这些命令先放入 AOF 缓存区中进行保存。这里的 AOF 缓冲区实际上是内存中的一片区域,存在的目的是当这些命令达到一定量以后再写入磁盘,避免频繁的磁盘 IO 操作。
  3. AOF 缓冲会根据 AOF 缓冲区 同步文件的三种写回策略,将命令写入磁盘上的 AOF 文件。
  4. 随着写入 AOF 内容的增加为避免文件膨胀,会根据规则进行命令的合并(又称 AOF 重写),从而起到 AOF 文件压缩的目的。
  5. 当 Redis Server 服务器重启的时候会从 AOF 文件加载数据。

🅰️ 4、AOF 缓冲区三种写回策略

文档介绍:https://redis.io/docs/management/persistence/#how-durable-is-the-append-only-file

配置文件 redis.conf 在 APPEND ONLY MODE 模块:

# The fsync() call tells the Operating System to actually write data on disk
# instead of waiting for more data in the output buffer. Some OS will really flush
# data on disk, some other OS will just try to do it ASAP.
#
# Redis supports three different modes:
#
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.
#
# The default is "everysec", as that's usually the right compromise between
# speed and data safety. It's up to you to understand if you can relax this to
# "no" that will let the operating system flush the output buffer when
# it wants, for better performances (but if you can live with the idea of
# some data loss consider the default persistence mode that's snapshotting),
# or on the contrary, use "always" that's very slow but a bit safer than
# everysec.
#
# More details please check the following article:
# http://antirez.com/post/redis-persistence-demystified.html
#
# If unsure, use "everysec".

# appendfsync always
appendfsync everysec
# appendfsync no
  • always :同步写回,每个写命令执行完立刻同步地将日志写回磁盘;
  • everysec :每秒写回,每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔 1 秒把缓冲区中的内容写入磁盘;
  • no :操作系统控制的写回,每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
写回机制优点缺点
always同步写回可靠性高,基本不丢失数据每个写命令都要落盘,性能影响较大
everysec每秒写回性能中宕机时丢失 1 秒内的数据
no操作系统控制的写回性能高宕机时丢失数据较多

1、AOF 启动/恢复/修复

🅱️ 1、配置文件说明(6 VS 7)

1)开启 AOF

默认是 no 关闭,设置为 yes 就标识开启 AOF 持久化支持。

############################## APPEND ONLY MODE ###############################

# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability. For instance using the default data fsync policy
# (see later in the config file) Redis can lose just one second of writes in a
# dramatic event like a server power outage, or a single write if something
# wrong with the Redis process itself happens, but the operating system is
# still running correctly.
#
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
#
# Please check http://redis.io/topics/persistence for more information.

appendonly yes

2)使用默认写回策略,每秒钟

# The fsync() call tells the Operating System to actually write data on disk
# instead of waiting for more data in the output buffer. Some OS will really flush
# data on disk, some other OS will just try to do it ASAP.
#
# Redis supports three different modes:
#
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.
#
# The default is "everysec", as that's usually the right compromise between
# speed and data safety. It's up to you to understand if you can relax this to
# "no" that will let the operating system flush the output buffer when
# it wants, for better performances (but if you can live with the idea of
# some data loss consider the default persistence mode that's snapshotting),
# or on the contrary, use "always" that's very slow but a bit safer than
# everysec.
#
# More details please check the following article:
# http://antirez.com/post/redis-persistence-demystified.html
#
# If unsure, use "everysec".

# appendfsync always
appendfsync everysec
# appendfsync no

3)AOF 文件的保存路径

  • Redis 6 版本

AOF 保存文件的位置和 RDB 保存文件的位置一样,都是通过 redis.conf 配置文件的 dir 配置项。

  • Redis 7 版本

新增 appenddirname 配置项。

# For convenience, Redis stores all persistent append-only files in a dedicated
# directory. The name of the directory is determined by the appenddirname
# configuration parameter.

appenddirname "appendonlydir"

最终 AOF 保存的路径为:dir + appenddirname

4)AOF 文件的保存名称

  • Redis 6 版本

有且仅有一个 AOF 文件

# The name of the append only file (default: "appendonly.aof")
appendfilename "appendonly.aof"
  • Redis 7 版本

Redis7.0 Multi Part AOF 的设计,官网说明:https://redis.io/docs/management/persistence/#append-only-file

# The base name of the append only file.
#
# Redis 7 and newer use a set of append-only files to persist the dataset
# and changes applied to it. There are two basic types of files in use:
#
# - Base files, which are a snapshot representing the complete state of the
#   dataset at the time the file was created. Base files can be either in
#   the form of RDB (binary serialized) or AOF (textual commands).
# - Incremental files, which contain additional commands that were applied
#   to the dataset following the previous file.
#
# In addition, manifest files are used to track the files and the order in
# which they were created and should be applied.
#
# Append-only file names are created by Redis following a specific pattern.
# The file name's prefix is based on the 'appendfilename' configuration
# parameter, followed by additional information about the sequence and type.
#
# For example, if appendfilename is set to appendonly.aof, the following file
# names could be derived:
#
# - appendonly.aof.1.base.rdb as a base file.
# - appendonly.aof.1.incr.aof, appendonly.aof.2.incr.aof as incremental files.
# - appendonly.aof.manifest as a manifest file.

appendfilename "appendonly.aof"
  • base :表示基础 AOF,它一般由子进程通过重写产生,该文件最多只有一个。基本文件

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

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

  • manifest :为了管理这些 AOF 文件,引入了一个 manifest (清单)文件来跟踪、管理这些 AOF。同时,为了便于 AOF 备份和拷贝,将所有的 AOF 文件和 manifest 文件放入一个单独的文件目录中,目录名由 appenddirname 配置。清单文件

🅱️ 2、正常恢复

正常写操作,可以见到生成 AOF 文件到指定的目录(appendonlydir):

appendonly.aof.1.base.rdb
appendonly.aof.1.incr.aof
appendonly.aof.manifest

1)示例:写入数据,重启 Redis 然后重新加载。

写入数据,关闭 Redis:

* Server initialized
* Creating AOF base file appendonly.aof.1.base.rdb on server start
* Creating AOF incr file appendonly.aof.1.incr.aof on server start
* Ready to accept connections tcp
* User requested shutdown...
# There is a child rewriting the AOF. Killing it!
* Calling fsync() on the AOF file.
* Saving the final RDB snapshot before exiting.
* DB saved on disk
* Removing the pid file.
# Redis is now ready to exit, bye bye...

重新启动 Redis:

* Server initialized
* Reading RDB base file on AOF loading...
* Loading RDB produced by version 7.0.9
* RDB age 321 seconds
* RDB memory usage when created 0.44 Mb
* RDB is base AOF
* Done loading RDB, keys loaded: 0, keys expired: 0.
* DB loaded from base file appendonly.aof.1.base.rdb: 0.001 seconds
* DB loaded from incr file appendonly.aof.1.incr.aof: 0.000 seconds
* DB loaded from append only file: 0.001 seconds
* Opening AOF incr file appendonly.aof.1.incr.aof on server start
* Ready to accept connections tcp

成功从 AOF 文件恢复数据:

> KEYS *
K1

🅱️ 3、异常恢复

1)故意在正常的 AOF 文件追加写入一些数据,模拟网络闪断,AOF 文件写入出错:

vim /myredis/appendonlydir/appendonly.aof.1.incr.aof

2)重启 Redis ,就会进行 AOF 文件的载入,但是此时发现 Redis 启动失败

# Bad file format reading the append only file appendonly.aof.3.incr.aof: make a backup of your AOF file, then use ./redis-check-aof --fix <filename.manifest>

3)AOF 文件异常修复命令:redis-check-aof --fix 进行修复

redis-check-aof --fix /myredis/appendonlydir/appendonly.aof.1.incr.aof
Start checking Old-Style AOF
AOF /myredis/appendonlydir/appendonly.aof.1.incr.aof format error
AOF analyzed: filename=/myredis/appendonlydir/appendonly.aof.1.incr.aof, size=93550, ok_up_to=93530, ok_up_to_line=1566, diff=20
This will shrink the AOF /myredis/appendonlydir/appendonly.aof.1.incr.aof from 93550 bytes, with 20 bytes, to 93530 bytes
Continue? [y/N]: y
Successfully truncated AOF /myredis/appendonlydir/appendonly.aof.1.incr.aof

4)重启 Redis 正常,数据也正常

🅰️ 5、AOF 的优缺点

1)优点

文档介绍:https://redis.io/docs/management/persistence/#aof-advantages

  • 使用 AOF Redis 更加持久:可以有不同的 fsync 策略:根本不 sync、每秒 fsync、每次查询时 fsync。使用每秒 fsync 的默认策略,写入性能仍然很棒。fsync 是使用后台线程执行的,当没有 fsync 正在进行时,主线程将努力执行写入,因此只会丢失一秒钟的写入。
  • AOF 日志是一个仅附加日志,因此不会出现寻道问题,也不会在断电时出现损坏问题。即使由于某种原因(磁盘已满或其他原因)日志以写一半的命令结尾,redis-check-aof 工具也能够轻松修复它。
  • 当 AOF 变得太大时,Redis 能够在后台自动重写 AOF。重写是完全安全的,因为当 Redis 继续附加到旧文件时,会使用创建当前数据集所需的最少操作集生成一个全新的文件,一旦第二个文件准备就绪,Redis 就会切换两者并开始附加到新的那一个。
  • AOF 以易于理解和解析的格式依次包含所有操作的日志。您甚至可以轻松导出 AOF 文件。例如,即使不小心使用 FLUSHALL 命令刷新了所有内容,只要在此期间没有执行日志重写,仍然可以通过停止服务器、删除最新命令并重新启动 Redis 来加载保存的数据集。

2)缺点

文档介绍:https://redis.io/docs/management/persistence/#aof-disadvantages

  • AOF 文件通常比相同数据集的等效 RDB 文件大。
  • 根据确切的 fsync 策略,AOF 可能比 RDB 慢。一般来说,将 fsync 设置为每秒性能仍然非常高。在禁用 fsync 的情况下,即使在高负载下,它也应该与 RDB 一样快。即使在巨大的写入负载的情况下,RDB 仍然能够提供关于最大延迟的更多保证。

2、AOF 重写机制

文档介绍:https://redis.io/docs/management/persistence/#log-rewriting

🅱️ 1、是什么

由于 AOF 持久化是 Redis 不断将写命令记录到 AOF 文件中,随着 Redis 不断的进行,AOF 的文件会越来越大,文件越大,占用服务器内存越大以及 AOF 恢复要求时间越长。

为了解决这个问题,Redis 新增了重写机制,当 AOF 文件的大小超过所设定的峰值时,Redis 就会 自动 启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集或者可以 手动 使用命令 bgrewriteaof 来重新。

🅱️ 2、触发机制

1)官网默认配置

# Automatic rewrite of the append only file.
# Redis is able to automatically rewrite the log file implicitly calling
# BGREWRITEAOF when the AOF log size grows by the specified percentage.
#
# This is how it works: Redis remembers the size of the AOF file after the
# latest rewrite (if no rewrite has happened since the restart, the size of
# the AOF at startup is used).
#
# This base size is compared to the current size. If the current size is
# bigger than the specified percentage, the rewrite is triggered. Also
# you need to specify a minimal size for the AOF file to be rewritten, this
# is useful to avoid rewriting the AOF file even if the percentage increase
# is reached but it is still pretty small.
#
# Specify a percentage of zero in order to disable the automatic AOF
# rewrite feature.

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

注意 ,同时满足,且的关系才会触发:
1、根据上次重写后的 aof 大小,判断当前 aof 大小是不是增长了 100% (1 倍);
2、需要重写的 AOF 满足文件的大小(64mb)。

2)自动触发

满足配置文件中的选项后,Redis 会记录上次重写时的 AOF 大小。

默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时自动触发重写机制。

3)手动触发

客户端向服务器发送 bgrewriteaof 命令。

🅱️ 3、案例说明

1)需求说明

举个例子:比如有个 key

一开始 set k1 v1

然后改成 set k1 v2

最后改成 set k1 v3

如果不重写,那么这 3 条语句都在 aof 文件中,内容占空间不说启动的时候都要执行一遍,共计 3 条命令。

但是,我们实际效果只需要 set k1 v3 这一条,所以,开启重写后,只需要保存 set k1 v3 就可以只需要保留最后一次修改值,相当于给 aof 文件瘦身减肥,性能更好。

AOF 重写不仅降低了文件的占用空间,同时更小的 AOF 也可以更快地被 Redis 启动加载。

2)需求验证

启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。

3)步骤

  • 前期配置准备

1、开启 aof,默认是 no 关闭,设置为 yes 就打开 aof 持久化支持

appendonly yes

2、重写峰值修改为 1k

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 1k

3、关闭混合,设置为 no

# Redis can create append-only base files in either RDB or AOF formats. Using
# the RDB format is always faster and more efficient, and disabling it is only
# supported for backward compatibility purposes.
aof-use-rdb-preamble no

4、删除之前的全部 aof 和 rdb 文件,清除干扰项

  • 自动触发案例

1、完成上述正确配置,重启 Redis 服务器,执行 set k1 v1 查看 aof 文件是否正常

appendonly.aof.1.base.rdb
appendonly.aof.1.incr.aof
appendonly.aof.manifest

2、查看三大配置文件

// 几种类型文件的前缀,后跟有关序列和类型的附加信息
appendfilename "appendonly.aof
// 新版本增加的目录配置项目
appenddirname "appendonlydir"
// 有如下的aof文件存在
// 1.基本文件(一个)
appendonly.aof.1.base.rdb
// 2.增量文件(一个或多个)
appendonly.aof.1.incr.aof
appendonly.aof.2.incr.aof
// 3.清单文件(一个)
appendonly.aof.manifest

3、对同一个 k1,不停的重复值 set 值,aof 文件慢慢变大,到峰值后启动重写机制

[001956] 22 Apr 15:05:04.133 * Starting automatic rewriting of AOF on 107400% growth
[001956] 22 Apr 15:05:04.138 * Creating AOF incr file appendonly.aof.2.incr.aof on background rewrite
[001956] 22 Apr 15:05:04.145 * Background append only file rewriting started by pid 13544
[001956] 22 Apr 15:05:04.356 # fork operation complete
[001956] 22 Apr 15:05:04.366 * Background AOF rewrite terminated with success
[001956] 22 Apr 15:05:04.367 * Successfully renamed the temporary AOF base file temp-rewriteaof-bg-1956.aof into appendonly.aof.2.base.rdb
[001956] 22 Apr 15:05:04.370 * Removing the history file appendonly.aof.1.incr.aof in the background
[001956] 22 Apr 15:05:04.370 * Removing the history file appendonly.aof.1.base.rdb in the background
[001956] 22 Apr 15:05:04.372 * Background AOF rewrite finished successfully

查看 aof 文件:

appendonly.aof.2.base.rdb
appendonly.aof.2.incr.aof
appendonly.aof.manifest
  • 手动触发案例

客户端向服务器发送 bgrewriteaof 命令:

> BGREWRITEAOF
Background append only file rewriting started

Redis 立马重写 aof 文件:

[001956] 22 Apr 15:10:08.835 * Creating AOF incr file appendonly.aof.3.incr.aof on background rewrite
[001956] 22 Apr 15:10:08.844 * Background append only file rewriting started by pid 5048
[001956] 22 Apr 15:10:09.036 # fork operation complete
[001956] 22 Apr 15:10:09.044 * Background AOF rewrite terminated with success
[001956] 22 Apr 15:10:09.045 * Successfully renamed the temporary AOF base file temp-rewriteaof-bg-1956.aof into appendonly.aof.3.base.rdb
[001956] 22 Apr 15:10:09.048 * Removing the history file appendonly.aof.2.incr.aof in the background
[001956] 22 Apr 15:10:09.048 * Removing the history file appendonly.aof.2.base.rdb in the background
[001956] 22 Apr 15:10:09.050 * Background AOF rewrite finished successfully

查看 aof 文件:

appendonly.aof.3.base.rdb
appendonly.aof.3.incr.aof
appendonly.aof.manifest

4)结论

也就是说 AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。

AOF 文件重写触发机制:通过 redis.conf 配置文件中的 auto-aof-rewrite-percentage 默认值为 100,以及 auto-aof-rewrite-min-size 64mb 配置,也就是说默认 Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发 AOF 重写。

🅱️ 4、重写原理

文档介绍:https://redis.io/docs/management/persistence/#how-it-works-1

1、在重写开始前,Redis 会创建一个“重写子进程”,这个子进程会读取现有的 AOF 文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。

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

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

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

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

🅰️ 6、AOF 优化配置项详解

配置文件 APPEND ONLY MODE 模块:

配置指令配置含义配置示例
appendonly是否开启 AOFappendonly yes
appendfilename文件名称appendfilename “appendonly.aof”
appendfsync同步方式appendfsync always
appendfsync everysec
appendfsync no
no-appendfsync-on-rewriteAOF 重写期间是否同步no-appendfsync-on-rewrite no
auto-aof-rewrite-percentage
auto-aof-rewrite-min-size
重写触发配置、文件重写策略auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

04、RDB+AOF 混合持久化

🅰️ 1、官网建议

文档介绍:https://redis.io/docs/management/persistence/#ok-so-what-should-i-use

如果想要数据安全性,则应该使用这两种持久性方法。

如果非常关心数据,但仍然可以接受发生故障发生时几分钟的数据丢失,那么可以简单地单独使用 RDB。

🅰️ 2、RDB vs AOF

1)问题

  1. 可否共存?
  2. 如果共存,启动恢复谁的?

2)默认配置

# By default Redis asynchronously dumps the dataset on disk. This mode is
# good enough in many applications, but an issue with the Redis process or
# a power outage may result into a few minutes of writes lost (depending on
# the configured save points).
#
# The Append Only File is an alternative persistence mode that provides
# much better durability. For instance using the default data fsync policy
# (see later in the config file) Redis can lose just one second of writes in a
# dramatic event like a server power outage, or a single write if something
# wrong with the Redis process itself happens, but the operating system is
# still running correctly.
# 
# 可以同时启用 AOF 和 RDB 持久性,而不会出现问题。如果在启动时启用了 AOF,Redis 将加载 AOF,即文件具有更好的耐用性保证。
# AOF and RDB persistence can be enabled at the same time without problems.
# If the AOF is enabled on startup Redis will load the AOF, that is the file
# with the better durability guarantees.
#
# Please check https://redis.io/topics/persistence for more information.

appendonly yes

3)数据恢复顺序和加载流程

在同时开启 AOF 和 RDB 持久化时,重启时只会加载 AOF 文件,不会加载 RDB 文件。

在这里插入图片描述

🅰️ 3、怎么选?用那个?

RDB 持久化方式能够在指定的时间间隔能对数据进行快照存储。

AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF 命令以 Redis 协议追加保存每次写的操作到文件末尾。

同时开启两种持久化方式:

在这种情况下,当 Redis 重启的时候会优先载入 AOF 文件来恢复原始的数据,因为在通常情况下 AOF 文件保存的数据集要比 RDB 文件保存的数据集要完整。

RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。那要不要只使用 AOF 呢?
建议不要,因为 RDB 更适合用于备份数据库(AOF 在不断变化不好备份),留着 RDB 作为一个保障。

🅰️ 4、RDB+AOF 混合方式

结合了 RDB 和 AOF 的优点,既能快速加载又能避免丢失过多的数据。

1、开启混合方式设置

设置 aof-use-rdb-preamble 的值为 yes。yes 表示开启,设置为 no 表示禁用。

# Redis can create append-only base files in either RDB or AOF formats. Using
# the RDB format is always faster and more efficient, and disabling it is only
# supported for backward compatibility purposes.
aof-use-rdb-preamble yes

2、RDB+AOF 的混合方式

结论:RDB 镜像做全量持久化,AOF 做增量持久化

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

05、纯缓存模式

同时关闭 RDB、AOF 持久化机制。

1、禁用 RDB

save ""

禁用 RDB 持久化模式下,仍然可以使用命令 savebgsave 生成 rdb 文件。

2、禁用 AOF

appendonly no

禁用 AOF 持久化模式下,仍然可以使用命令 bgrewriteaof 生成 aof 文件。

🌈 Redis 事务

🅰️ 1、是什么

文档介绍:https://redis.io/docs/interact/transactions

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

🅰️ 2、能干嘛

在一个队列中,一次性、顺序性、排他性的执行一系列命令。

Redis 事务可以一次执行多个命令, 并且带有以下三个重要的保证:

  • 批量操作在发送 EXEC 命令前被放入队列缓存;
  • 收到 EXEC 命令后进入事务执行,事务中任意命令执行失败,其余的命令依然被执行;
  • 在事务执行过程,其他客户端提交的命令请求不会插入到事务执行命令序列中。

一个事务从开始到执行会经历以下三个阶段:

  • 开始事务;
  • 命令入队;
  • 执行事务。

🅰️ 3、Redis 事务 VS 关系型数据库事务

1、单独的隔离操作

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

2、没有隔离级别的概念

因为事务提交前任何指令都不会被实际执行,也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这种问题了。

3、不保证原子性

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

4、排它性

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

01、使用案例

文档介绍:https://redis.io/docs/interact/transactions

Redis 事务是使用 MULTI 命令开启的。命令总是以“OK”作为回复。此时,用户可以发出多个命令。Redis 不会立即执行这些命令,而是将它们放入一个队列。当调用 EXEC 命令后,将执行队列的所有命令。

调用 DISCARD 将清空事务队列并退出事务。

🅰️ 1、Redis 事务相关的常用命令

  • MULTI :标记一个事务块的开始。
  • EXEC :执行所有事务块内的命令。
  • DISCARD :取消事务,放弃执行事务块内的所有命令。
  • WATCH key [key ...] :监视一个(或多个)key ,如果在事务执行之前这个(或这些)key 被其他命令所改动,那么事务将被打断。
  • UNWATCH :取消 WATCH 命令对所有 key 的监视。

🅰️ 2、案例一:正常执行

文档介绍:https://redis.io/docs/interact/transactions/#usage

使用命令 MULTIEXEC

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> INCR age
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) OK
3) OK
4) (integer) 1
127.0.0.1:6379> get age
"1"
127.0.0.1:6379>

单个 Redis 命令的执行是原子性的,但 Redis 没有在事务上增加任何维持原子性的机制,所以 Redis 事务的执行并不是原子性的。

事务可以理解为一个打包的批量执行脚本,但批量指令并非原子化的操作,中间某条指令的失败不会导致前面已做指令的回滚,也不会造成后续的指令不做。

🅰️ 3、案例二:放弃事务

文档介绍:https://redis.io/docs/interact/transactions/#discarding-the-command-queue

使用命令 MULTIDISCARD

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> INCR age
QUEUED
127.0.0.1:6379> DISCARD
OK
127.0.0.1:6379> get age
"1"
127.0.0.1:6379>

🅰️ 4、案例三:命令放入队列时错误,全体失败

文档介绍:https://redis.io/docs/interact/transactions/#errors-inside-a-transaction

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 v11
QUEUED
127.0.0.1:6379> set k2 v22
QUEUED
127.0.0.1:6379> set k3
(error) ERR wrong number of arguments for 'set' command
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get k2
"v2"
127.0.0.1:6379>

一个命令语法出错,全体连坐。如果任何一个命令语法有错 Redis 会直接返回错误,所有的命今都不会执行。

🅰️ 5、案例四:命令执行时错误,自身命令失败,不影响其他命令执行

文档介绍:https://redis.io/docs/interact/transactions/#errors-inside-a-transaction

127.0.0.1:6379> set name don
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 v111
QUEUED
127.0.0.1:6379> incr name
QUEUED
127.0.0.1:6379> set age 6
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get k1
"v111"
127.0.0.1:6379> get age
"6"
127.0.0.1:6379> get name
"don"
127.0.0.1:6379>

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

  • 不会回滚的解释

文档介绍:https://redis.io/docs/interact/transactions/#what-about-rollbacks

Redis 不支持事务回滚,因为支持回滚会对 Redis 的简单性和性能产生重大影响。

注意和传统数据库事务区别,不一定要么一起成功要么一起失败。

🅰️ 6、案例五:watch 监控

Redis 使用 Watch 来提供乐观锁定,类似于 CAS(Check-and-Set)

文档介绍:https://redis.io/docs/interact/transactions/#optimistic-locking-using-check-and-set

示例:初始化 k1 和 balance 两个 key,先监控再开启 multi,保证两个 key 变动在同一个事务内。

127.0.0.1:6379> set k1 abc
OK
127.0.0.1:6379> set balance 100
OK
127.0.0.1:6379> watch balance
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 abc2
QUEUED
127.0.0.1:6379> set balance 200
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
127.0.0.1:6379> get k1
"abc2"
127.0.0.1:6379> get balance
"200"
127.0.0.1:6379>

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

第一个第5步执行结果返回为空(监控的数据已被其他用户修改),也就是相当于是失败。

在这里插入图片描述

unwatch 放弃监控,即使在事务中的数据被其他用户修改了,也不影响。

在这里插入图片描述

一旦执行了 exec ,之前加的监控锁都会被取消掉了。

当客户端连接丢失的时候(比如退出链接),所有的监控锁都会被取消监视。

🅰️ 4、小总结

开启事务:以 MULTI 开始一个事务。

入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面。

执行事务:由 EXEC 命令触发事务。

放弃事务:使用 DISCARD 命令放弃事务。

🌈 Redis 管道

🅰️ 1、目前的问题

面试题:如何优化频繁命令往返造成的 Redis 性能瓶颈?

⭕️ 问题由来:

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

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

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

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

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

✅ 解决思路:引出管道这个概念

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

在这里插入图片描述

🅰️ 2、是什么

文档介绍:https://redis.io/docs/manual/pipelining

定义:Pipeline 是为了解决 RTT 往返回时间,仅仅是将命令打包一次性发送,对整个 Redis 的执行不造成其它任何影响。

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

🅰️ 3、使用案例

$(echo -en "PING\r\n SET mykey redis7\r\nGET mykey\r\nINCR visitor\r\nINCR visitor\r\n"; sleep 10) | nc localhost 6379

通过使用 PING 命令查看 Redis 服务是否可用;之后设置 mykey 的值为 redis7,然后获取 mykey 的值;最后执行 visitor 自增 2 次。

在返回的结果中可以看到这些命令一次性向 Redis 服务提交,并最终一次性读取所有服务端的响应。

命令写在文件中,一次执行:

[xxx]# cat test.txt 
set mykey redis7
incr visitor
[xxx]# cat test.txt | redis-cli --pipe
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 2
[xxx]# redis-cli
127.0.0.1:6379> get mykey
"redis7"
127.0.0.1:6379> get visitor
"1"
127.0.0.1:6379> 

🅰️ 4、小总结

1、Pipeline 与原生批量命令对比

  • 原生批量命令是原子性(例如:mset,mget),pipeline 是非原子性。
  • 原生批量命令一次只能执行一种命令,pipeline 支持批量执行不同命令。
  • 原生批命令是服务端实现,而 pipeline 需要服务端与客户端共同完成。

2、Pipeline 与事务对比

  • 事务具有原子性,管道不具有原子性。
  • 管道一次性将多条命令发送到服务器,事务是一条一条的发,事务只有在接收到 exec 命令后才会执行,管道不会。
  • 执行事务时会阻塞其他命令的执行,而执行管道中的命令时不会。

3、使用 Pipeline 的注意事项

  • pipeline 缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令。
  • 使用 pipeline 组装的命令个数不能太多,不然数据量过大客户端阻塞的时间可能过久,同时服务端此时也被迫回复一个队列答复,占用很多内存。

🌈 Redis 发布订阅

🅰️ 1、是什么

文档介绍:https://redis.io/docs/interact/pubsub

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

Redis 可以实现消息中间件 MQ 的功能,通过发布订阅实现消息的引导和分流。项目中不推荐使用该功能。

🅰️ 2、能干嘛

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

在这里插入图片描述

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

在这里插入图片描述

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

🅰️ 3、相关命令

1)命令 1

SUBSCRIBE channel [channel ...]

订阅给定的一个或多个频道的信息。

推荐先执行订阅后再发布消息,订阅成功之前发布的消息是收不到的。

订阅的客户端每次可以收到一个含有 3 个参数的消息:

  • 消息的种类
  • 始发频道的名称
  • 实际的消息内容

2)命令 2

PUBLISH channel message

发布消息到指定的频道。

示例:开启 3 个客户端,演示客户端 A、B 订阅消息,客户端 C 发布消息

客户端 A:

127.0.0.1:6379> SUBSCRIBE ch1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "ch1"
3) (integer) 1

客户端 B:

127.0.0.1:6379> SUBSCRIBE ch2
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "ch2"
3) (integer) 1

客户端 C:

127.0.0.1:6379> PUBLISH ch1 helloch1
(integer) 1
127.0.0.1:6379> PUBLISH ch2 helloch2
(integer) 1
127.0.0.1:6379>

客户端 A 接收到消息:

1) "message"
2) "ch1"
3) "helloch1"

客户端 B 接收到消息:

1) "message"
2) "ch2"
3) "helloch2"

3)命令 3

PSUBSCRIBE pattern [pattern ...]

按照模式批量订阅,订阅一个或多个符合给定模式(支持 *? 号之类的)的频道。

示例:

127.0.0.1:6379> PSUBSCRIBE ch*
Reading messages... (press Ctrl-C to quit)
1) "psubscribe"
2) "ch*"
3) (integer) 1
1) "pmessage"
2) "ch*"
3) "ch1"
4) "helloch1"
1) "pmessage"
2) "ch*"
3) "ch2"
4) "helloch2"

4)命令 4

查看订阅与发布系统状态

PUBSUB subcommand [argument [argument ...]]
  • 由活跃频道组成的列表
PUBSUB CHANNELS
  • 某个频道有几个订阅者
PUBSUB NUMSUB [channel [channel ...]]
  • 只统计使用 PSUBSCRIBE 命令执行的,返回客户端订阅的唯一 模式的数量
PUBSUB NUMPAT

示例:

127.0.0.1:6379> PUBSUB NUMPAT
(integer) 0
127.0.0.1:6379> PUBSUB CHANNELS
1) "ch2"
2) "ch1"
127.0.0.1:6379> PUBSUB NUMSUB ch1 ch2
1) "ch1"
2) (integer) 1
3) "ch2"
4) (integer) 1
127.0.0.1:6379>

5)命令 5

取消订阅

UNSUBSCRIBE [channel [channel ...]]

6)命令 6

退订所有给定模式的频道

PUNSUBSCRIBE [pattern [pattern ...]]

🅰️ 4、总结

Redis 可以实现消息中间件 MQ 的功能,通过发布订阅实现消息的引导和分流。不推荐项目中使用。

Pub/Sub 缺点:

  • 发布的消息在 Redis 系统中不能持久化,因此,必须先执行订阅,再等待消息发布。如果先发布了消息,那么该消息由于没有订阅者,消息将被直接丢弃。
  • 消息只管发送,对于发布者而言消息是即发即失的,不管接收,也没有 ACK 机制,无法保证消息的消费成功。

以上的缺点导致 Redis 的 Pub/Sub 模式就像个小玩具,在生产环境中几乎无用武之地,为此 Redis 5.0 版本新增了 Stream 数据结构,不但支持多播,还支持数据持久化,相比 Pub/Sub 更加的强大。

🌈 Redis 复制(replication)

01、Redis 复制的介绍

Redis 如何通过复制支持高可用性和故障切换。

🅰️ 1、是什么

文档介绍:https://redis.io/docs/management/replication

Redis 复制,即主从复制,Master 以写为主,Slave 以读为主。

当 Master 数据变化的时候,自动将新的数据异步同步到其链接的 Slave 数据库。

🅰️ 2、能干嘛

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

02、Redis 复制操作案例

配从库不配主库

🅰️ 1、权限问题

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

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

Redis 配置文件的 REPLICATION 模块可以看到:

# If the master is password protected (using the "requirepass" configuration
# directive below) it is possible to tell the replica to authenticate before
# starting the replication synchronization process, otherwise the master will
# refuse the replica request.
#
# masterauth <master-password>

🅰️ 2、基本操作命令

1)命令:info replication

可以查看复制节点的主从关系和配置信息。

2)命令:replicaof 主库IP 主库端口

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

################################# REPLICATION #################################

# Master-Replica replication. Use replicaof to make a Redis instance a copy of
# another Redis server. A few things to understand ASAP about Redis replication.
#
#   +------------------+      +---------------+
#   |      Master      | ---> |    Replica    |
#   | (receive writes) |      |  (exact copy) |
#   +------------------+      +---------------+
#
# 1) Redis replication is asynchronous, but you can configure a master to
#    stop accepting writes if it appears to be not connected with at least
#    a given number of replicas.
# 2) Redis replicas are able to perform a partial resynchronization with the
#    master if the replication link is lost for a relatively small amount of
#    time. You may want to configure the replication backlog size (see the next
#    sections of this file) with a sensible value depending on your needs.
# 3) Replication is automatic and does not need user intervention. After a
#    network partition replicas automatically try to reconnect to masters
#    and resynchronize with them.
#
# replicaof <masterip> <masterport>

3)命令:slaveof 主库IP 主库端口

每次与 master 断开之后,都需要重新连接,除非你配置进 redis.conf 文件。

在运行期间修改 slave 节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系转而和新的主数据库同步,重新拜码头。

4)命令:slaveof no one

使当前数据库停止与其他数据库的同步关系,转成主数据库。

🅰️ 3、操作案例前置说明与准备

1)环境准备

准备 3 台 Redis,一个 Master 两个 Slave,配置文件分别命名为 redis6380.conf、redis6390.conf、redis6391.conf,且三边网络相互 ping 通且注意防火墙配置。

2)涉及三大命令

  • 主从复制:replicaof 主库IP 主库端口
  • 改换门庭:slaveof 新主库IP 新主库端口
  • 自立为王:slaveof no one

3)修改配置文件,分别修改 6380、6390、6391 配置

3.1)配置 Redis 为后台运行

################################# GENERAL #####################################

# By default Redis does not run as a daemon. Use 'yes' if you need it.
# Note that Redis will write a pid file in /var/run/redis.pid when daemonized.
# When Redis is supervised by upstart or systemd, this parameter has no impact.
daemonize yes

3.2)移除绑定 IP

# bind 127.0.0.1 -::1

3.3)关闭保护模式

# protected-mode no

3.4)指定端口

port 6380

3.5)指定当前工作目录 dir

# The working directory.
#
# The DB will be written inside this directory, with the filename specified
# above using the 'dbfilename' configuration directive.
#
# The Append Only File will also be created inside this directory.
#
# Note that you must specify a directory here, not a file name.
dir ./replication_db

3.6)指定 pid 文件名字 pidfile

# If a pid file is specified, Redis writes it where specified at startup
# and removes it at exit.
#
# When the server runs non daemonized, no pid file is created if none is
# specified in the configuration. When the server is daemonized, the pid file
# is used even if not specified, defaulting to "/var/run/redis.pid".
#
# Creating a pid file is best effort: if Redis is not able to create it
# nothing bad happens, the server will start and run normally.
#
# Note that on modern Linux systems "/run/redis.pid" is more conforming
# and should be used instead.
pidfile /var/run/redis_6380.pid

3.7)指定 log 文件名字 logfile

# Specify the log file name. Also the empty string can be used to force
# Redis to log on the standard output. Note that if you use standard
# output for logging but daemonize, logs will be sent to /dev/null
logfile "redis_6380.log"

3.8)设置密码 requirepass

requirepass 123456

3.9)配置 dump.rdb 名字

# The filename where to dump the DB
dbfilename dump6380.rdb

3.10)配置 aof 文件 appendfilename(本步骤可选,非必须)

appenddirname "appendonlydir"

3.11)从机访问主机的通行密码 masterauth,必须配置(从机需要配置,主机不用)

replicaof 192.168.10.10 6380

masterauth 123456

🅰️ 4、主从复制模式 1 :一主多仆

1)方案1:配置文件固定配置

配从(库)不配主(库)。配置文件执行:replicaof 主库IP 主库端口

1、从库 6390/6391 分别配置

replicaof 192.168.10.10 6380

masterauth 123456

2、先 master 后 slave 依次启动

redis-server redis_6380.conf
redis-server redis_6390.conf
redis-server redis_6391.conf

3、主从关系查看

查看主机日志:

Synchronization with replica 127.0.0.1:6390 succeeded
//...
Synchronization with replica 127.0.0.1:6391 succeeded

查看从机日志:

Connecting to MASTER 127.0.0.1:6380
//...
Master replied to PING, replication can continue...

4、使用命令查看 info replication

主机 6380:

> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6390,state=online,offset=910,lag=1
slave1:ip=127.0.0.1,port=6391,state=online,offset=910,lag=1
master_failover_state:no-failover
master_replid:96594b73268e78fc974cba842d21820ccb7accbd
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:910
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:910

从机 6390:

> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:980
slave_repl_offset:980
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:96594b73268e78fc974cba842d21820ccb7accbd
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:980
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:980

看出成功实现主从关系绑定。

2)方案2:命令操作手动指定

可以使用命令:slaveof 主库IP 主库端口 将一个主机关联到另一台主机上当从机。

1、准备一台 Redis 6392 ,配置文件中不配置 replicaof 配置项,启动后查看信息标注为 master。

> 127.0.0.1@6392 connected!
> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:2a64b6440bad02f011281082fa9c263bb724d7fa
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

2、输入命令 slaveof 127.0.0.1 6380 绑定主从关系,角色变成 slave

> slaveof 127.0.0.1 6380
OK
> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_read_repl_offset:14970
slave_repl_offset:14970
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:0c1e89fbd5de7a6f6d3346f9cf9a692bd9aeb67a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14970
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:14971
repl_backlog_histlen:0

3、此时查看主机关系,标明有三个从机

> info replication
# Replication
role:master
connected_slaves:3
slave0:ip=127.0.0.1,port=6390,state=online,offset=15026,lag=1
slave1:ip=127.0.0.1,port=6391,state=online,offset=15026,lag=1
slave2:ip=127.0.0.1,port=6392,state=online,offset=15026,lag=1
master_failover_state:no-failover
master_replid:0c1e89fbd5de7a6f6d3346f9cf9a692bd9aeb67a
master_replid2:96594b73268e78fc974cba842d21820ccb7accbd
master_repl_offset:15026
second_repl_offset:12479
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:12479
repl_backlog_histlen:2548

🅰️ 5、主从的常见问题

1)从机可以执行写命令吗? 不能

> set k2 v2
READONLY You can't write against a read only replica.

2)slave 是从头开始复制还是从切入点开始复制?

从机在启动后,会一次性复制主机上的数据,之后增量复制主机上的数据。

3)主机宕机后,从机会变成主机吗?

不会,从机保持不变化,从机数据可以正常查询使用;等待主机重新启动,继续完成工作。

Successful partial resynchronization with master.
MASTER <-> REPLICA sync: Master accepted a Partial Resynchronization.

4)主机宕机后,重启后主从关系还在吗?从机还能否顺利复制?

主从关系还在,从机也正常复制。

> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6390,state=online,offset=13021,lag=0
slave1:ip=127.0.0.1,port=6391,state=online,offset=13021,lag=0
master_failover_state:no-failover
master_replid:0c1e89fbd5de7a6f6d3346f9cf9a692bd9aeb67a
master_replid2:96594b73268e78fc974cba842d21820ccb7accbd
master_repl_offset:13021
second_repl_offset:12479
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:12479
repl_backlog_histlen:543

5)某台从机宕机后,主机继续写操作,从机重启后他的数据能保证完整吗?

完整的,从机重启后会同步

Successful partial resynchronization with master.
MASTER <-> REPLICA sync: Master accepted a Partial Resynchronization.

🅰️ 6、主从复制模式 2 :薪火相传

一个 slave 可以是下一个 slave 的 master,slave 同样可以接收其他 slave 的连接和同步请求,那么该 slave 作为了链条中下一个的 master。可以有效减轻主 master 的写压力。

中途变更关系转向:会清除现有同步的数据,重新建立关系后,同步 master 最新的数据。

使用命令:slaveof 新主库IP 新主库端口

从机 6391 执行,关联到 6390 从机上,关系如下:

> 127.0.0.1@6391 connected!
> slaveof 127.0.0.1 6390
OK
> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6390
master_link_status:up
master_last_io_seconds_ago:11
master_sync_in_progress:0
slave_read_repl_offset:17094
slave_repl_offset:17094
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:0c1e89fbd5de7a6f6d3346f9cf9a692bd9aeb67a
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:17094
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:13330
repl_backlog_histlen:3765

此时主机 6380 的关系如下,只带着一个从机 6390:

> 127.0.0.1@6380 connected!
> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6390,state=online,offset=17220,lag=1
master_failover_state:no-failover
master_replid:0c1e89fbd5de7a6f6d3346f9cf9a692bd9aeb67a
master_replid2:96594b73268e78fc974cba842d21820ccb7accbd
master_repl_offset:17220
second_repl_offset:12479
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:12479
repl_backlog_histlen:4742

此时从机 6390 的关系如下,角色还是 slave,其下挂着 slave0 6391:

> 127.0.0.1@6390 connected!
> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:3
master_sync_in_progress:0
slave_read_repl_offset:17290
slave_repl_offset:17290
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:1
slave0:ip=127.0.0.1,port=6391,state=online,offset=17290,lag=0
master_failover_state:no-failover
master_replid:0c1e89fbd5de7a6f6d3346f9cf9a692bd9aeb67a
master_replid2:96594b73268e78fc974cba842d21820ccb7accbd
master_repl_offset:17290
second_repl_offset:12479
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:17290

此时主机 6380 写入数据,会同步到从机 6390,从机 6391 也会从从机 6390 同步更新的数据。

🅰️ 7、主从复制模式 3 :反客为主

当前数据库停止与其他数据库的同步关系,转成主数据库。

使用命令:slaveof no one

> 127.0.0.1@6391 connected!
> slaveof no one
OK
> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:c327d18094230018430868f062b510d31338f36c
master_replid2:0c1e89fbd5de7a6f6d3346f9cf9a692bd9aeb67a
master_repl_offset:17739
second_repl_offset:17740
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:13330
repl_backlog_histlen:4410
> set k10 v10
OK

解除同步关系后,角色变成了 master,也即可以写入数据了。

03、Redis 复制工作原理

文档介绍:https://redis.io/docs/management/replication/#how-redis-replication-works

🅰️ 1、从机启动,发起初次同步请求

slave 启动成功连接到 master 后会发送一个 sync 命令。

slave 首次全新连接 master,一次完全同步(全量复制)将被自动执行,slave 自身原有数据会被 master 数据覆盖清除。

🅰️ 2、首次连接,全量复制

master 节点收到 sync 命令后,会开始在后台保存快照(即 RDB 持久化,主从复制时会触发 RDB),同时收集所有接收到的用于修改数据集命令缓存起来,master 节点执行 RDB 持久化完后,master 将 rdb 快照文件和所有缓存的命令发送到所有 slave,以完成一次完全同步。

slave 节点在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化。

🅰️ 3、心跳持续,保持通信

master 节点会定期向 slave 节点发出 PING 包,周期默认是 10 秒。

# Master send PINGs to its replicas in a predefined interval. It's possible to
# change this interval with the repl_ping_replica_period option. The default
# value is 10 seconds.
#
# repl-ping-replica-period 10

🅰️ 4、进入平稳,增量复制

master 继续将新的所有收集到的修改命令自动依次传给 slave 完成增量同步。

🅰️ 5、从机下线,重连续传

master 会检查 backlog 里面的 offset,master 和 slave 都会保存一个复制的 offset 还有一个 masterId,offset 是保存在 backlog 中的。master 只会把已经复制的 offset 后面的数据复制给 slave,类似断点续传。

🅰️ 1、复制的缺点

1)复制延时,信号衰减

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

2)主机宕机了怎么办?

主机宕机了,从机保持不变,在 slave 节点当中不会自动重选一个 master ,需要人工手动干预。麻烦。

🌈 Redis 哨兵(sentinel)

非集群 Redis 的高可用性。

Redis Sentinel 在不使用 Redis Cluster 时为 Redis 提供高可用性。

Redis Sentinel 还提供其他附带任务,例如监控、通知,并充当客户端的配置提供者。

🅰️ 1、是什么

文档介绍:https://redis.io/docs/management/sentinel

哨兵巡查监控后台 master 主机是否故障,如果故障了根据投票数自动将某一个从库转换为新主库,继续对外提供服务。

哨兵的作用:

1)监控 Redis 运行状态,包括 master 和 slave

2)当 master down 机,能自动将 slave 切换成新 master

在这里插入图片描述

🅰️ 2、能干嘛,即 Sentinel 功能

  • 主从监控:监控主从 Redis 库运行是否正常
  • 消息通知:哨兵可以将故障转移的结果发送给客户端
  • 故障转移:如果 Master 异常,则会进行主从切换,将其中一个 Slave 作为新 Master
  • 配置中心:客户端通过连接哨兵来获得当前 Redis 服务的主节点地址

Redis Sentinel 是一个分布式系统。

Sentinel 本身设计为在多个 Sentinel 进程一起协作的配置中运行。多个 Sentinel 进程协作的优点如下:

1)当多个 Sentinel 一致认为给定的 master 不再可用时,就会执行故障检测。这降低了误报的可能性。

2)即使并非所有 Sentinel 进程都在工作,Sentinel 也会工作,从而使系统能够抵御故障。

文档介绍:https://redis.io/docs/management/sentinel/#sentinel-as-a-distributed-system

01、Redis Sentinel 操作案例

文档介绍:https://redis.io/docs/management/sentinel/#sentinel-quick-start

🅰️ 1、Redis Sentinel 架构说明

在这里插入图片描述

  • 3 个哨兵:自动监控和维护集群,不存放数据,只是吹哨人
  • 1 主 2 从 Redis 服务:用于数据的存储与查询

🅰️ 2、Redis Sentinel 配置文件说明

新建 sentinel.conf 配置文件文件,名字固定。

文档参考:https://redis.io/docs/management/sentinel/#configuring-sentinel

重点参数项说明:

1)protected-mode :安全保护模式

2)port :端口

3)daemonize :是否以后台 daemon 方式运行

4)pidfile :pid 文件路径

5)logfile :日志文件路径

6)dir :工作目录

7)sentinel monitor <master-name> <ip> <redis-port> <quorum> :设置要监控的 master 服务器

quorum 表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数。

# sentinel monitor <master-name> <ip> <redis-port> <quorum>
#
# Tells Sentinel to monitor this master, and to consider it in O_DOWN
# (Objectively Down) state only if at least <quorum> sentinels agree.
#
# Note that whatever is the ODOWN quorum, a Sentinel will require to
# be elected by the majority of the known Sentinels in order to
# start a failover, so no failover can be performed in minority.
#
# Replicas are auto-discovered, so you don't need to specify replicas in
# any way. Sentinel itself will rewrite this configuration file adding
# the replicas using additional configuration options.
# Also note that the configuration file is rewritten when a
# replica is promoted to master.
#
# Note: master name should not include special characters or spaces.
# The valid charset is A-z 0-9 and the three characters ".-_".
sentinel monitor mymaster 127.0.0.1 6379 2
  • quorum 是指需要就主机不可访问这一事实达成一致的 Sentinel 数量,以便真正将主机标记为失败,并在可能的情况下最终启动故障转移过程。
  • quorum 仅用于检测故障。为了实际执行故障转移,其中一名 Sentinel 需要被选为故障转移的领导者,并获得继续进行故障转移的授权。只有大多数 Sentinel 进程投票时才会发生这种情况。

例如,如果有 5 个 Sentinel 进程,并且给定主进程的法定人数设置为 2,则会发生以下情况:

  • 如果两个 Sentinel 同时同意主节点无法访问,则两个 Sentinel 之一将尝试启动故障转移。
  • 如果总共至少有 3 个 Sentinel 可达,则故障转移将获得授权并实际启动。

8)sentinel auth-pass <master-name> <password> :master 设置了密码,连接 master 服务的密码

# sentinel auth-pass <master-name> <password>
#
# Set the password to use to authenticate with the master and replicas.
# Useful if there is a password set in the Redis instances to monitor.
#
# Note that the master password is also used for replicas, so it is not
# possible to set a different password in masters and replicas instances
# if you want to be able to monitor these instances with Sentinel.
#
# However you can have Redis instances without the authentication enabled
# mixed with Redis instances requiring the authentication (as long as the
# password set is the same for all the instances requiring the password) as
# the AUTH command will have no effect in Redis instances with authentication
# switched off.
#
# Example:
#
# sentinel auth-pass mymaster MySUPER--secret-0123passw0rd

9)sentinel down-after-milliseconds <master-name> <milliseconds> :指定多少毫秒之后,主节点没有应答哨兵,此时哨兵主观上认为主节点下线。

# sentinel down-after-milliseconds <master-name> <milliseconds>
#
# Number of milliseconds the master (or any attached replica or sentinel) should
# be unreachable (as in, not acceptable reply to PING, continuously, for the
# specified period) in order to consider it in S_DOWN state (Subjectively
# Down).
#
# Default is 30 seconds.
sentinel down-after-milliseconds mymaster 30000

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

# sentinel parallel-syncs <master-name> <numreplicas>
#
# How many replicas we can reconfigure to point to the new replica simultaneously
# during the failover. Use a low number if you use the replicas to serve query
# to avoid that all the replicas will be unreachable at about the same
# time while performing the synchronization with the master.
sentinel parallel-syncs mymaster 1

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

# sentinel failover-timeout <master-name> <milliseconds>
#
# Default is 3 minutes.
sentinel failover-timeout mymaster 180000

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

# NOTIFICATION SCRIPT
#
# sentinel notification-script <master-name> <script-path>
#
# Example:
#
# sentinel notification-script mymaster /var/redis/notify.sh

13)sentinel client-reconfig-script <master-name> <script-path> :客户端重新配置主节点参数脚本

# CLIENTS RECONFIGURATION SCRIPT
#
# sentinel client-reconfig-script <master-name> <script-path>
#
# Example:
#
# sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

🅰️ 3、本案例 Sentinel 配置文件

因成本问题,本案例中采用的 3 个哨兵都配置在同一台机器中。

分别创建配置文件 sentinel_26380.conf、sentinel_26381.conf、sentinel_26382.conf。内容如下:

protected-mode no
port 26380
daemonize yes
pidfile /var/run/redis-sentinel-26380.pid
logfile "sentinel-26380.log"
dir /tmp
sentinel monitor mymaster 127.0.0.1 6380 2
sentinel auth-pass mymaster 123456

🅰️ 4、启动运行测试

1)先启动一主二从 3 个 Redis 实例,测试正常的主从复制。

注意:

主机 6380 后续可能会变成从机,需要设置访问新主机的密码,设置 masterauth 项访问密码,不然后续可能报错 master_link_status:down

masterauth 123456

分别启动 3 个 Redis 实例,确保一主二从正常运行。

2)再启动 3 个哨兵,完成监控。

文档介绍:https://redis.io/docs/management/sentinel/#running-sentinel

redis-server sentinel_6380.conf --sentinel
redis-server sentinel_6381.conf --sentinel
redis-server sentinel_6382.conf --sentinel

sentinel_6380.conf 启动日志如下:

* Running mode=sentinel, port=26380.
* Sentinel new configuration saved on disk
# Sentinel ID is d950c6551aa09c371009651a14eef3d806d8b1b3
# +monitor master mymaster 127.0.0.1 6380 quorum 2
* +slave slave 127.0.0.1:6390 127.0.0.1 6390 @ mymaster 127.0.0.1 6380
* Sentinel new configuration saved on disk
* +slave slave 127.0.0.1:6391 127.0.0.1 6391 @ mymaster 127.0.0.1 6380
* Sentinel new configuration saved on disk
* +sentinel sentinel ef9a8ead3e48f38610d4e6a417f6c46872b52c4e 127.0.0.1 26381 @ mymaster 127.0.0.1 6380
* Sentinel new configuration saved on disk
* +sentinel sentinel fc07a99f9485786d7ae0517d30ef6f479ad3a62a 127.0.0.1 26382 @ mymaster 127.0.0.1 6380
* Sentinel new configuration saved on disk

根据打印的日志:Sentinel new configuration saved on disk,再次查看配置文件 sentinel_6380.conf 多了如下内容:

# Generated by CONFIG REWRITE
latency-tracking-info-percentiles 50 99 99.9
user default on nopass sanitize-payload ~* &* +@all
sentinel myid d950c6551aa09c371009651a14eef3d806d8b1b3
sentinel config-epoch mymaster 0
sentinel leader-epoch mymaster 0
sentinel current-epoch 0

sentinel known-replica mymaster 127.0.0.1 6391

sentinel known-replica mymaster 127.0.0.1 6390

sentinel known-sentinel mymaster 127.0.0.1 26382 fc07a99f9485786d7ae0517d30ef6f479ad3a62a

sentinel known-sentinel mymaster 127.0.0.1 26381 ef9a8ead3e48f38610d4e6a417f6c46872b52c4e

3)启动 3 个哨兵监控后再测试一次主从复制

一切正常:

> 127.0.0.1@6380 connected!
> set k9 v9
OK
> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6390,state=online,offset=62276,lag=0
slave1:ip=127.0.0.1,port=6391,state=online,offset=62290,lag=0
master_failover_state:no-failover
master_replid:6e7c680cf396cfc21ea90fd4919924287e66d684
master_replid2:0c1e89fbd5de7a6f6d3346f9cf9a692bd9aeb67a
master_repl_offset:62290
second_repl_offset:17249
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:17249
repl_backlog_histlen:45042

4)手动关闭 6380 主服务器,模拟 master 宕机

SHUTDOWN

🅰️ 5、 问题思考❓

  1. 两台从机数据是否 OK ?
  2. 是否会从剩下的 2 台从机选出新的 master ?
  3. 之前 down 机的 master 机器重启回来,谁将会是新老大?会不会双 master 冲突?

1)问题一

从机数据正常查询:

> 127.0.0.1@6390 connected!
> get key
hello

> 127.0.0.1@6391 connected!
> get key
hello

2)问题二/三

选出新的 master:

> 127.0.0.1@6390 connected!
> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6391,state=online,offset=365903,lag=1
master_failover_state:no-failover
master_replid:da5603777a8fcbeff561d8bcb79054c85bbb13fb
master_replid2:6e7c680cf396cfc21ea90fd4919924287e66d684
master_repl_offset:366302
second_repl_offset:294663
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:17249
repl_backlog_histlen:349054
> set k10 v10
OK

可见现在 6390 被选为主机,从机一个 6391。主机 6390 可以写入数据,且同步到从机 6391。

也可以查看 sentinel-26380.log 日志:

# +sdown master mymaster 127.0.0.1 6380
# +odown master mymaster 127.0.0.1 6380 #quorum 2/2
# +new-epoch 1
# +try-failover master mymaster 127.0.0.1 6380
* Sentinel new configuration saved on disk
# +vote-for-leader d950c6551aa09c371009651a14eef3d806d8b1b3 1
# fc07a99f9485786d7ae0517d30ef6f479ad3a62a voted for d950c6551aa09c371009651a14eef3d806d8b1b3 1
# ef9a8ead3e48f38610d4e6a417f6c46872b52c4e voted for d950c6551aa09c371009651a14eef3d806d8b1b3 1
* Sentinel new configuration saved on disk
# +new-epoch 2
* Sentinel new configuration saved on disk
# +vote-for-leader ef9a8ead3e48f38610d4e6a417f6c46872b52c4e 2
# +config-update-from sentinel ef9a8ead3e48f38610d4e6a417f6c46872b52c4e 127.0.0.1 26381 @ mymaster 127.0.0.1 6380
# +switch-master mymaster 127.0.0.1 6380 127.0.0.1 6390
* +slave slave 127.0.0.1:6391 127.0.0.1 6391 @ mymaster 127.0.0.1 6390
* +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6390
* Sentinel new configuration saved on disk
# +sdown slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6390

重启 6380 ,可以看到日志又追加:

* +convert-to-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ mymaster 127.0.0.1 6390

6380 重启后是降级变成从机角色了:不能写入数据,能查询

> 127.0.0.1@6380 connected!
> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6390
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:510388
slave_repl_offset:510388
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:da5603777a8fcbeff561d8bcb79054c85bbb13fb
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:510388
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:491354
repl_backlog_histlen:19035
> set k11 v11
READONLY You can't write against a read only replica.
> get k10
v10

🅰️ 6、 经过上边的过程,现在查看配置文件的变化

查看旧的 master 6380 的配置文件。在文件最后追加了配置,挂载到 6390 当从机

# Generated by CONFIG REWRITE
replicaof 127.0.0.1 6390
latency-tracking-info-percentiles 50 99 99.9
save 300 100
save 60 10000
user default on sanitize-payload #8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 ~* &* +@all

查看新的 master 6390 的配置文件。最初的配置项 replicaof 被删除,并在文件追加:

# Generated by CONFIG REWRITE
save 300 100
save 60 10000
latency-tracking-info-percentiles 50 99 99.9
user default on sanitize-payload #8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92 ~* &* +@all

查看从机 6391 的配置文件。最初的配置项 replicaof 被修改为 replicaof 127.0.0.1 6390

结论:

文件的内容,在运行期间会被 sentinel 动态进行更改。

master - slave 切换后,master 的配置、slave 的配置和 sentinel 的配置内容都会发生改变,即 master 配置中会多了 replicaof 的配置,sentinel.conf 的监控目标会随之调换。

sentinel known-replica mymaster 127.0.0.1 6380

sentinel known-replica mymaster 127.0.0.1 6391

02、哨兵运行流程和选举原理

当一个主从配置中的 master 失效之后,sentinel 可以选举出一个新的 master 用于自动接替原 master 的工作,主从配置中的其他 Redis 服务器自动指向新的 master 同步数据。一般建议 sentinel 采取奇数台,防止某一台 sentinel 无法连接到 master 导致误切换。

🅰️ 1、SDown 主观下线(Subjectively Down)

SDown(主观不可用)是单个 sentinel 自己主观上检测到的关于 master 的状态,从 sentinel 的角度来看,如果发送了 PING 心跳后,在一定时间内没有收到合法的回复,就达到了 SDown 的条件。

sentinel 配置文件中的 down-after-milliseconds 设置了判断主观下线的时间长度。

# Number of milliseconds the master (or any attached replica or sentinel) should
# be unreachable (as in, not acceptable reply to PING, continuously, for the
# specified period) in order to consider it in S_DOWN state (Subjectively
# Down).
#
# Default is 30 seconds.
sentinel down-after-milliseconds mymaster 30000

sentinel down-after-milliseconds 表示 master 被当前 sentinel 实例认定为失效的间隔时间,这个配置其实就是进行主观下线的一个依据。

master 在多长时间内一直没有给 sentine 返回有效信息,则认定该 master 主观下线。也就是说如果多久没联系上redis-servevr,认为这个 redis-server 进入到失效(SDOWN)状态。

🅰️ 2、ODown 客观下线(Objectively Down)

ODown 需要一定数量的 sentinel,多个哨兵达成一致意见才能认为一个 master 客观上已经挂了。

# sentinel monitor <master-name> <ip> <redis-port> <quorum>
#
# Tells Sentinel to monitor this master, and to consider it in O_DOWN
# (Objectively Down) state only if at least <quorum> sentinels agree.
#
# Note that whatever is the ODOWN quorum, a Sentinel will require to
# be elected by the majority of the known Sentinels in order to
# start a failover, so no failover can be performed in minority.
#
# Replicas are auto-discovered, so you don't need to specify replicas in
# any way. Sentinel itself will rewrite this configuration file adding
# the replicas using additional configuration options.
# Also note that the configuration file is rewritten when a
# replica is promoted to master.
#
# Note: master name should not include special characters or spaces.
# The valid charset is A-z 0-9 and the three characters ".-_".
sentinel monitor mymaster 127.0.0.1 6379 2
  • <master-name> 是对某个 master+slave 组合的一个区分标识(一套 sentinel 可以监听多组 master+slave 这样的组合)
  • <quorum> 这个参数是进行客观下线的一个依据,法定人数/法定票数

意思是至少有 quorum 个 sentinel 认为这个 master 有故障才会对这个 master 进行下线以及故障转移。因为有的时候,某个 sentinel 节点可能因为自身网络原因导致无法连接 master,而此时 master 并没有出现故障,所以这就需要多个 sentinel 都一致认为该 master 有问题,才可以进行下一步操作,这就保证了公平性和高可用。

🅰️ 3、选举出领导者哨兵(哨兵中选出兵王)

当主节点被判断客观下线以后,各个哨兵节点会进行协商,先选举出一个领导者哨兵节点(leader)并由该领导者节点,也即被选举出的兵王进行 failover(故障迁移)。

1)哨兵日志文件解读分析

在这里插入图片描述

在这里插入图片描述

2)哨兵领导者,兵王如何选出来的?

Raft 算法

在这里插入图片描述

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

🅰️ 4、由哨兵 leader 开始推动故障切换流程并选出一个新 master

3 步骤:

1)选出新主

选出新 master 的规则,剩余 slave 节点健康前提下。

在这里插入图片描述

redis.conf 文件中,优先级 slave-priority 或者 replica-priority 最高的从节点(数字越小优先级越高)

# The replica priority is an integer number published by Redis in the INFO
# output. It is used by Redis Sentinel in order to select a replica to promote
# into a master if the master is no longer working correctly.
#
# A replica with a low priority number is considered better for promotion, so
# for instance if there are three replicas with priority 10, 100, 25 Sentinel
# will pick the one with priority 10, that is the lowest.
#
# However a special priority of 0 marks the replica as not able to perform the
# role of master, so a replica with priority of 0 will never be selected by
# Redis Sentinel for promotion.
#
# By default the priority is 100.
replica-priority 100

复制偏移位置 offset 最大的从节点。(选择从节点中数据完整度高的)

最小 Run ID 的从节点。(每个 Redis 实例启动后都会随机生成一个 40 位的 Run ID)

2)重新绑定关系

会执行 slaveof no one 命令让选出来的从节点成为新的主节点,并通过 slaveof 命令让其他节点成为其从节点。

  1. Sentinel leader 会对选举出的新 master 执行 slaveof no one 操作,将其提升为 master 节点。
  2. Sentinel leader 向其它 slave 发送 slaveof 命令,让剩余的 slave 成为新的 master 节点的 slave。

3)旧主回来当 slave

将之前已下线的旧 master 设置为新选出的新 master 的从节点,当旧 master 重新上线后,它会成为新 master 的从节点。

Sentinel leader 会让原来的 master 降级为 slave 并恢复正常工作。

总结:

上述的 failover 操作均由 sentinel 自己独自完成,完全无需人工干预。

在这里插入图片描述

🅰️ 3、哨兵使用建议

  • 哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用
  • 哨兵节点的数量应该是奇数
  • 各个哨兵节点的配置应一致
  • 如果哨兵节点部署在 Docker 等容器里面,尤其要注意端口的正确映射
  • 哨兵集群+主从复制,并不能保证数据零丢失 ❗️(见 Redis 集群篇)

🌈 Redis 集群(cluster)

使用 Redis 集群进行扩展:https://redis.io/docs/management/scaling

🍉 Redis 集群规范(算法和设计原理):https://redis.io/docs/reference/cluster-spec

01、Redis Cluster 介绍

编辑中。。。

🌈 Spring Boot 集成 Redis

🅰️ 1、使用 Jedis 客户端集成

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

文档:https://github.com/redis/jedis

1)引入依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.4.3</version>
</dependency>

2)基础操作

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.resps.Tuple;

import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Jedis 客户端测试
 *
 * @author Don
 */
public class JedisClientTest {

    public static void main(String[] args) {
        JedisPool pool = new JedisPool("127.0.0.1", 6379);
        Jedis jedis = pool.getResource();

        System.out.println(jedis.ping());

        // key
        Set<String> keys = jedis.keys("*");
        keys.forEach(System.out::println);
        System.out.println();

        // String
        String setString = jedis.set("jedis", "Hello Jedis");
        String getString = jedis.get("jedis");
        System.out.println(setString + " \t" + getString);
        System.out.println();

        // Hash
        long hashSet = jedis.hset("jedis-hash", "name", "Don");
        long hashSet2 = jedis.hset("jedis-hash", "age", "18");
        Map<String, String> hashGetAll = jedis.hgetAll("jedis-hash");
        System.out.println(hashSet + " \t" + hashSet2 + " \t" + hashGetAll);
        System.out.println();

        // List
        long listPush = jedis.lpush("jedis-list", "Hello", "Redis", "Jedis");
        List<String> listRange = jedis.lrange("jedis-list", 0, -1);
        System.out.println(listPush + " \t" + listRange);
        System.out.println();

        // Set
        long setAdd = jedis.sadd("jedis-set-game", "u1", "u2", "u3");
        System.out.println(setAdd + " \t" + jedis.scard("jedis-set-game"));
        System.out.println(jedis.srandmember("jedis-set-game", 2));
        System.out.println(jedis.srandmember("jedis-set-game", 2));
        System.out.println(jedis.srandmember("jedis-set-game", 2));
        System.out.println();

        // ZSet
        jedis.zadd("jedis-set-rank", 60, "m1");
        jedis.zadd("jedis-set-rank", 80, "m2");
        jedis.zadd("jedis-set-rank", 90, "m3");
        List<Tuple> zSetRange = jedis.zrangeWithScores("jedis-set-rank", 0, -1);
        System.out.println(zSetRange);
    }

}

🅰️ 2、使用 Lettuce 客户端集成

Lettuce 是一个线程安全的 Redis Java 客户端。Lettuce 是一个完全非阻塞的 Redis 客户端,使用 Netty 构建,提供反应式、异步和同步数据访问。

文档:https://lettuce.io

1)Lettuce VS Jedis

都是 Redis 的客户端,在 Spring Boot 2 之后默认使用的 Letuce 客户端连接 Redis 服务器。因为当使用 Jedis 客户端连接 Redis 服务器的时候,每个线程都要拿自己创建的 Jedis 实例去连接 Redis 客户端,当有很多个线程的时候,不仅开销大需要反复的创建关闭一个 Jedis 连接,而且也是线程不安全的,一个线程通过 Jedis 实例更改 Redis 服务器中的数据之后会影响另一个线程。

使用 Lettuce 这个客户端连接 Redis 服务器的时候,就不会出现上面的情况,Lettuce 底层使用的是 Netty,多个线程都需要连接 Redis 服务器的时候,可以保证只创建一个 Letluce 连接,使所有的线程共享这一个 Lettuce 连接,这样可以减少创建关闭一个 Letuce 连接时候的开销。而这种方式也是线程安全的,不会出现一个线程通过 Lettuce 更改 Redis 服务器中的数据之后而影响另一个线程的情况。

2)引入依赖

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>6.2.5.RELEASE</version>
</dependency>

3)基础操作

import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisURI;
import io.lettuce.core.ScoredValue;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;

import java.util.List;
import java.util.Map;

/**
 * Lettuce 客户端测试
 *
 * @author Don
 */
public class LettuceClientTest {
    public static void main(String[] args) {

        RedisURI redisURI = RedisURI.create("127.0.0.1", 6379);
        RedisClient redisClient = RedisClient.create(redisURI);
        StatefulRedisConnection<String, String> connection = redisClient.connect();
        RedisCommands<String, String> stringRedisCommand = connection.sync();

        // key
        List<String> keys = stringRedisCommand.keys("*");
        keys.forEach(System.out::println);

        // String
        String setString = stringRedisCommand.set("lettuce", "Hello Lettuce");
        String getString = stringRedisCommand.get("lettuce");
        System.out.println(setString + " \t" + getString + " \n");

        // Hash
        Boolean hashSet = stringRedisCommand.hset("lettuce-hash", "name", "Don");
        Boolean hashSet2 = stringRedisCommand.hset("lettuce-hash", "age", "18");
        Map<String, String> hashGetAll = stringRedisCommand.hgetall("lettuce-hash");
        System.out.println(hashSet + " \t" + hashSet2 + " \t" + hashGetAll + " \n");

        // List
        long listPush = stringRedisCommand.lpush("lettuce-list", "Hello", "Redis", "Lettuce");
        List<String> listRange = stringRedisCommand.lrange("lettuce-list", 0, -1);
        System.out.println(listPush + " \t" + listRange + " \n");

        // Set
        long setAdd = stringRedisCommand.sadd("lettuce-set-game", "u1", "u2", "u3");
        System.out.println(setAdd + " \t" + stringRedisCommand.scard("lettuce-set-game"));
        System.out.println(stringRedisCommand.srandmember("lettuce-set-game", 2));
        System.out.println(stringRedisCommand.srandmember("lettuce-set-game", 2));
        System.out.println(stringRedisCommand.srandmember("lettuce-set-game", 2));
        System.out.println();

        // ZSet
        stringRedisCommand.zadd("lettuce-set-rank", 60, "m1");
        stringRedisCommand.zadd("lettuce-set-rank", 80, "m2");
        stringRedisCommand.zadd("lettuce-set-rank", 90, "m3");
        List<ScoredValue<String>> zRangeWithScores = stringRedisCommand.zrangeWithScores("lettuce-set-rank", 0, -1);
        System.out.println(zRangeWithScores);

        // GEO
        Long geoAdd = stringRedisCommand.geoadd("lettuce-geo", 116.403963, 39.915119, "天安门");
        System.out.println(geoAdd + " \t" + stringRedisCommand.geopos("lettuce-geo", "天安门"));

        connection.close();
        redisClient.shutdown();
    }
}

🅰️ 3、集成 RedisTemplate

文档:https://docs.spring.io/spring-boot/docs/2.7.14/reference/html/data.html#data.nosql.redis

文档:https://spring.io/projects/spring-data-redis

1)引入依赖

<!-- Spring Data Redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lettuce 需要使用的连接池 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.11.1</version>
</dependency>

spring-boot-starter-data-redis 默认使用 lettuce 作为连接 Redis 的客户端。

2)配置 YML

spring:
  application:
    name: springboot-redis
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    client-name: springboot-redis
    client-type: lettuce
    lettuce:
      pool:
        enabled: true
        # 连接池最大连接数(默认为8, 使用负值表示没有限制)
        max-active: 8
        # 连接池中的最大空闲连接(默认为8)
        max-idle: 8
        # 连接池中的最小空闲连接(默认为0)
        min-idle: 0
        # 连接池最大阻塞等待时间(默认为-1, 使用负值表示没有限制)
        max-wait: 3000ms
        # 空闲对象逐出器线程的运行间隔时间. 空闲连接线程释放周期时间.
        time-between-eviction-runs: 3000ms

3)编写业务

public class OrderService {

    public static final String ORDER_KEY = "order:";

    @Resource
    private RedisTemplate<String, Object> 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 getOrder(Integer keyId) {
        return (String) redisTemplate.opsForValue().get(ORDER_KEY + keyId);
    }
}
public class OrderController {

    @Resource
    private OrderService orderService;

    @PostMapping
    public void addOrder() {
        orderService.addOrder();
    }

    @GetMapping(value = "/{id}")
    public String findUserById(@PathVariable Integer id) {
        return orderService.getOrder(id);
    }
}

4)测试

OrderService    : 编号: 548 的订单流水: f36dd085-ba21-4520-bd8c-fde5f75172fa

成功存储与查看。

现在通过客户端查看 Redis 数据:

在这里插入图片描述

发现出现乱码了。。。

5)序列化问题

键(key)和值(value)都是通过 Spring 提供的 Serializer 序列化到数据库的。

RedisTemplate 默认使用的是 JDK 序列化方式 (JdkSerializationRedisSerializer)。

org.springframework.data.redis.serializer.JdkSerializationRedisSerializer

默认情况下,RedisTemplate 使用该数据列化方式,可以看下源码 RedisTemplate#afterPropertiesSet()

    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        boolean defaultUsed = false;
        if (this.defaultSerializer == null) {
            this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
        }

        // ....
    }

StringRedisTemplate 默认使用的是 StringRedisSerializer。

public class StringRedisTemplate extends RedisTemplate<String, String> {
    public StringRedisTemplate() {
        this.setKeySerializer(RedisSerializer.string());
        this.setValueSerializer(RedisSerializer.string());
        this.setHashKeySerializer(RedisSerializer.string());
        this.setHashValueSerializer(RedisSerializer.string());
    }

    public StringRedisTemplate(RedisConnectionFactory connectionFactory) {
        this();
        this.setConnectionFactory(connectionFactory);
        this.afterPropertiesSet();
    }

    protected RedisConnection preProcessConnection(RedisConnection connection, boolean existingConnection) {
        return new DefaultStringRedisConnection(connection);
    }
}

6)自定义序列化

@Configuration
public class RedisConfig {

    /**
     * 自定义序列化规则
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {

        // 创建 RedisTemplate 对象
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

        // 设置 RedisConnection 工厂(实现多种 Java Redis 客户端接入的工厂)
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        // Key 的序列化
        redisTemplate.setKeySerializer(StringRedisSerializer.UTF_8);
        redisTemplate.setHashKeySerializer(StringRedisSerializer.UTF_8);

        // Value 的序列化

        // 使用 Jackson2JsonRedisSerialize 替换默认序列化
        Jackson2JsonRedisSerializer<?> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);

        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);

        jackson2JsonRedisSerializer.setObjectMapper(mapper);

        redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);

        return redisTemplate;
    }
    
}

再次测试,成功解决。

在这里插入图片描述

🅰️ 4、集成 Redis 集群

1)启动 Redis 集群 6 个实例

127.0.0.1:6381> cluster nodes
f3f9e80fe1186a3a85f54052a9b8a1546283b377 127.0.0.1:6381@16381,,shard-id=3a9b035f1594f61360e9003daa61ff05e78c44fe myself,master - 0 1690183401000 10 connected 0-6826 10923-12287
481f2585f911cbe0be70f038df13b771a1d7066a 127.0.0.1:6382@16382,,shard-id=ce67000eb0122d4015af9ea99a32c25f37a8b5e3 master - 0 1690183401846 2 connected 6827-10922
87611964f51a161e1c2d21da3b52f3397344fcbb 127.0.0.1:6383@16383,,shard-id=d70b6fd1b6296a17040bff38e93b31bfd186835f master - 0 1690183402000 3 connected 12288-16383
186eeae11f01c64266e28bb35aa7f8dec4662c0e 127.0.0.1:6384@16384,,shard-id=3a9b035f1594f61360e9003daa61ff05e78c44fe slave f3f9e80fe1186a3a85f54052a9b8a1546283b377 0 1690183402506 10 connected
3f966ff72fea5dcb091bb584f1c0987f080502c8 127.0.0.1:6385@16385,,shard-id=ce67000eb0122d4015af9ea99a32c25f37a8b5e3 slave 481f2585f911cbe0be70f038df13b771a1d7066a 0 1690183403057 2 connected
329ef5e839a9a1ecc697812fdaad0ddd658ba12f 127.0.0.1:6386@16386,,shard-id=d70b6fd1b6296a17040bff38e93b31bfd186835f slave 87611964f51a161e1c2d21da3b52f3397344fcbb 0 1690183402506 3 connected
127.0.0.1:6381>

2)修改 YML 配置

spring:
  application:
    name: springboot-redis
  redis:
    database: 0
    password: 123456
    client-name: springboot-redis
    client-type: lettuce
    lettuce:
      pool:
        enabled: true
        # 连接池最大连接数(默认为8, 使用负值表示没有限制)
        max-active: 8
        # 连接池中的最大空闲连接(默认为8)
        max-idle: 8
        # 连接池中的最小空闲连接(默认为0)
        min-idle: 0
        # 连接池最大阻塞等待时间(默认为-1, 使用负值表示没有限制)
        max-wait: 3000ms
        # 空闲对象逐出器线程的运行间隔时间. 空闲连接线程释放周期时间.
        time-between-eviction-runs: 3000ms
    cluster:
      # 集群节点的“初始”列表
      nodes: 127.0.0.1:6381,127.0.0.1:6382,127.0.0.1:6383,127.0.0.1:6384,127.0.0.1:6385,127.0.0.1:6386
      # 在集群中执行命令时要遵循的最大重定向数
      max-redirects: 3

3)直接通过微服务访问 Redis 集群操作数据,成功存取数据

4)手动模拟宕机

手动模拟宕机,master-6381 机器意外宕机,手动 shutdown。

先对 Redis 集群以命令方式,手动验证各种读写命令,看看 6384 从机是否正常切换到 master。

一切正常,Redis Cluster 集群能自动感知并自动完成主备切换,对应的 slave-6384 会被选举为新的 master 节点。

>redis-cli -a 123456 -p 6384
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6384> info replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:4a8cfe2459f8617c34b01b7bb16f790a6bd48d2c
master_replid2:12e53068f3fd7c908020f437e8532ce98864b1d6
master_repl_offset:1292
second_repl_offset:1293
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1292
127.0.0.1:6384> set k3 v3
OK
127.0.0.1:6384> set k4 v5
-> Redirected to slot [8455] located at 127.0.0.1:6382
OK
127.0.0.1:6382>

5)微服务客户端再次读写访问试试

此时出现问题了,故障现象:SpringBoot 客户端没有动态感知到 RedisCluster 的最新集群信息

❌ 经典 2 个故障:

io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s)
Reconnecting, last destination was 127.0.0.1:6381
Cannot reconnect to [127.0.0.1:6381]: Connection refused: no further information: /127.0.0.1:6381

导致原因:

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

♻️ 解决方案:

1、排除 lettuce 采用 jedis 客户端(不推荐)

2、重写连接工厂实例(极度不推荐)

    @Bean
    public DefaultClientResources lettuceClientResources() {
        return DefaultClientResources.create();
    }

    @Bean
    public LettuceConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties, ClientResources clientResources) {

        ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                .enablePeriodicRefresh(Duration.ofSeconds(30)) //按照周期刷新拓扑
                .enableAllAdaptiveRefreshTriggers() //根据事件刷新拓扑
                .build();

        ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
                //Redis命令超时时间,超时后才会使用新的拓扑信息重新建立连接
                .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(10)))
                .topologyRefreshOptions(topologyRefreshOptions)
                .build();

        LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
                .clientResources(clientResources)
                .clientOptions(clusterClientOptions)
                .build();

        RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
        clusterConfig.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
        clusterConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));

        return new LettuceConnectionFactory(clusterConfig, clientConfiguration);
    }

3、刷新节点集群拓扑动态感应

文档:https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#refreshing-the-cluster-topology-view

添加 YML 配置:

spring:
  redis:
    lettuce:
      cluster:
        refresh:
          dynamic-refresh-sources: true
          # 支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
          adaptive: true
          # 群集拓扑刷新周期
          period: 2000ms

再次启动测试,发现第一次访问会出现 1s 左右成功返回,之后完全正常使用。

Redis 高阶篇 生产落地与面试解析

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Redis是一个开源的、使用C语言编写的高性能键值对存储数据库,其源码注释主要包含了对Redis的各个组件、模块、函数等的详细说明,方便开发者理解和使用Redis。 在Redis 7.0的源码注释中,包含了对Redis的基本结构、数据类型、内存管理、数据库操作、网络通信等方面的注释。 在基本结构方面,源码注释详细介绍了Redis的整体架构和模块之间的关系,如服务器结构、客户端结构、数据库结构等。这些注释帮助开发者了解Redis的组成部分,有助于对Redis进行二次开发和定制化。 在数据类型方面,可以找到对Redis支持的各种数据类型(如字符串、列表、哈希表、有序集合等)的源码注释。这些注释详细说明了数据类型的实现原理和内部数据结构,以及对应操作的时间复杂度等信息,有助于开发者在使用Redis时了解其内部实现机制和使用方法。 在内存管理方面,Redis源码注释解释了Redis对内存的分配和释放,并介绍了部分内存管理的原理和策略,如对象引用计数、内存回收等。这些注释可以帮助开发者理解Redis在内存管理方面的设计和优化。 在数据库操作方面,Redis源码注释提供了对数据库的增删改查操作的详细注释,包括对集合操作、哈希表操作、有序集合操作等。这些注释可以帮助开发者了解Redis对数据的存储和操作方式,以及相应的数据结构和算法。 在网络通信方面,Redis源码注释包含了对网络连接、通信协议、命令解析等的详细说明。这些注释帮助开发者了解Redis与客户端之间的通信机制,以及如何解析和处理客户端发送的命令。 总之,Redis 7.0源码注释提供了对Redis各个方面的详细解释,帮助开发者理解Redis的内部结构、实现原理和使用方式,是开发者学习和使用Redis的重要参考资料。 ### 回答2: Redis 7.0是一款开源的、高性能的、支持多种数据结构的内存数据库。在Redis 7.0的源码注释中,涉及了许多关键的实现细节、数据结构定义和算法逻辑。 首先,Redis 7.0的源码注释提供了对各个函数和模块的详细解释。这些注释能够帮助开发人员更好地理解代码的功能和实现方式,提供了方便的参考和指导。例如,对于关键数据结构如字符串、哈希表、链表等的定义和使用都有详细的注释说明,帮助开发人员了解其内部实现原理和使用方式。 其次,Redis 7.0的源码注释还包含了算法的解释和优化思路。对于一些关键算法,如缓存淘汰算法、订阅与发布算法等,注释中详细解释了其实现原理和性能优化的思路。这对于开发人员来说是非常有价值的,可以帮助他们更好地理解和改进Redis的性能和功能。 此外,Redis 7.0的源码注释还包括了对一些重要模块和功能的解释。例如,对于多线程支持的相关代码和模块,注释中详细解释了其原理和使用方式,以及相关的并发控制策略。这可以帮助开发人员更好地理解和使用Redis的多线程功能,提高系统的并发处理能力。 总而言之,Redis 7.0源码注释对于开发人员来说是一份非常宝贵的文档。它提供了对代码的详细解释和说明,包括数据结构定义、算法实现和模块功能等方面。这些注释可以帮助开发人员更加深入地理解和使用Redis,并为他们在解决问题和优化性能时提供有价值的参考和指导。 ### 回答3: Redis 7.0Redis 数据库的一个版本,以下是对该版本源码注释的简要解释: Redis 7.0 的源码注释主要用于解释 Redis 数据库中的各个函数、数据结构和算法的功能和实现原理。 在 Redis 7.0 的源码中,注释被用来解释函数的输入参数和返回值、函数的作用、关键算法的实现细节以及代码的逻辑结构等等。注释帮助开发者理解和使用源码,对于对 Redis 进行二次开发或者进行排错、优化也起到了重要作用。 源码注释提供了对 Redis 各个模块和功能的清晰解释,例如底层的数据类型实现、线程模型、COW(copy-on-write)策略等等。同时,注释还覆盖了 Redis 7.0 引入的新功能和改进。例如,Redis 7.0 可能引入了新的数据结构或者命令,这些新功能的实现细节可能在注释中有所描述。 此外,Redis 7.0 的源码注释还可能包含一些重要的注释块,其中包括一些算法和数据结构的详细解释,这有助于开发者深入了解 Redis 内部的运作方式。 需要注意的是,Redis 7.0 的源码注释是用来帮助开发者理解源码的,因此在实际运行时会被编译器忽略掉。因此,在阅读和使用源码时,我们需要同时参考源码注释和实际代码实现来全面理解 Redis 的工作机制。 总之,Redis 7.0 的源码注释对于开发者来说是一份重要的参考资料,能够帮助他们理解 Redis 的工作原理和实现方式,从而更好地使用和扩展 Redis 数据库

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值