Chiriri's blog Chiriri's blog
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)

Iekr

苦逼后端开发
首页
  • Java

    • JavaSE
    • JavaEE
    • 设计模式
  • Python

    • Python
    • Python模块
    • 机器学习
  • Golang

    • Golang
    • gRPC
  • 服务器

    • Linux
    • MySQL
    • NoSQL
    • Kubernetes
  • 项目

    • 传智健康
    • 畅购商城
  • Hadoop生态

    • Hadoop
    • Zookeeper
    • Hive
    • Flume
    • Kafka
    • Azkaban
    • Hbase
    • Scala
    • Spark
    • Flink
  • 大数据项目

    • 离线数仓
  • 青训营

    • 第四届青训营
  • HTML

    • HTML
    • JavaScript
  • Vue

    • Vue2
    • TypeScript
    • Vue3
    • Uni-APP
  • 数据结构与算法
  • C语言
  • 考研数据结构
  • 计算机组成原理
  • 计算机操作系统
  • Java基础

    • Java基础
    • Java集合
    • JUC
    • JVM
  • 框架

    • Spring
    • Dubbo
    • Spring Cloud
  • 数据库

    • MySQL
    • Redis
    • Elasticesearch
  • 消息队列

    • RabbitMQ
    • RocketMQ
  • 408

    • 计算机网络
    • 操作系统
    • 算法
  • 分类
  • 标签
  • 归档
  • 导航站
GitHub (opens new window)
  • JavaSE

  • JavaEE

  • Linux

  • MySQL

  • NoSQL

    • Nosql
    • Redis
    • Redis高级
      • 持久化
        • RDB(Redis DataBase)
        • 自动触发
        • 手动触发
        • 总结
        • AOF(Append Only File)
        • AOF缓冲区三种写回策略
        • 配置文件
        • 与RDB对比
        • AOF重写机制
        • 重写原理
        • 总结
        • RDB-AOF混合持久化
        • 纯缓存模式
      • 事务
        • watch监控
      • 管道
      • 发布订阅
        • 订阅频道
        • 发布消息
        • 取消订阅
      • 过期数据
        • 定时删除
        • 惰性删除
        • 定期删除
        • 淘汰策略
      • 主从复制
        • 互联网 "三高" 架构
        • 阶段一:建立连接阶段
        • 主从连接 (slave连接master)
        • 主从断开连接
        • 授权访问
        • 阶段二:数据同步阶段工作流程
        • 阶段三:命令传播阶段
        • 流程更新
        • 心跳机制
        • 频繁的全量复制
        • 频繁的网络中断
        • 数据不一致
        • 复制原理和工作流程
        • 复制的缺点
      • 哨兵模式
        • 配置哨兵
        • 阶段一:监控阶段
        • 阶段二:通知阶段
        • 阶段三:故障转移
        • 判断master宕机
        • 选举master
        • 哨兵运行流程和选举原理
        • SDown主观下线(Subjectively Down)
        • ODown客观下线(Objectively Down)
        • 选举出领导者哨兵(哨兵中选出兵王)
        • Raft算法
        • 由兵王开始推动故障切换流程并选出一个新master
        • 新主登基
        • 群臣俯首
        • 旧主拜服
      • Cluster集群
        • 集群算法-分片-槽位slot
        • slot槽位映射
        • 哈希取余分区
        • 一致性哈希算法分区
        • 一致性哈希环
        • 节点映射
        • 落键规则
        • 优点
        • 缺点
        • 哈希槽分区
        • 为什么redis集群的最大槽数是16384个?
        • 不保证强一致性
        • Cluster集群结构设计
        • 配置
        • 启动Culster
        • 节点增删
        • 占位符和CRC16算法
        • RedisTemplate连接集群
      • 缓存预热
      • 缓存雪崩
      • 缓存击穿
      • 缓存穿透
      • 性能指标监控
      • 性能指标工具
    • MongoDB
  • Python

  • Python模块

  • 机器学习

  • 设计模式

  • 传智健康

  • 畅购商城

  • 博客项目

  • JVM

  • JUC

  • Golang

  • Kubernetes

  • 硅谷课堂

  • C

  • 源码

  • 神领物流

  • RocketMQ

  • 短链平台

  • 后端
  • NoSQL
Iekr
2021-09-24
目录

Redis高级

# Redis 高级

# 持久化

image-20231122152154740

# RDB(Redis DataBase)

在指定的时间间隔,执行数据集的时间点快照,实现类似照片记录效果的方式,就是把某一时刻的数据和状态以文件的形式写到磁盘上,也就是快照。这样一来即使故障宕机,快照文件也不会丢失,数据的可靠性也就得到了保证。这个快照文件就称为 RDB 文件 (dump.rdb),其中,RDB 就是 Redis DataBase 的缩写。

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

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

image-20231122152344384

# 自动触发

Redis 6.0.16 以下

在 Redis.conf 添加 save 关键字,以多行配置来设置多个间隔

save 900 1 # 每隔900s 如果有超过1个key发生了变化,就写一份新的DB文件
save 300 10 # 每隔300s 如果有超过10个key发生了变化,就写一份新的DB文件
save 60 10000 # 每隔60s 如果有超过1000个key发生了变化,就写一份新的DB文件
1
2
3

Redis 6.2 以及 Redis-7.0.0

在 Redis.conf 添加 save 关键字,可以以单行配置多个间隔

save 3600 1 300 100 60 10000
1

如果更改了工作目录则快照文件也会改变,并可以更改 dump 文件名

dir ./
dbfilename dump6379.rdb # 修改dump文件名
1
2

自定义修改的路径且可以进入客户端使用命令查看目录

config get dir
1

如何恢复快照?

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

注意:执行 flushall/flushdb 命令也会产生 dump.rdb 文件,但里面是空的,无意义

# 手动触发

上面我们通过修改配置文件,让 redis 自动触发快照生成,我们同样可以使用命令来手动触发快照生成

Redis 提供了两个命令来⽣成 RDB ⽂件,分别是 save 和 bgsave

save
1

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

bgsave
1

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

Redis 会使用 bgsave 对当前内存中的所有数据做快照,这个操作是子进程在后台完成的,这就允许主进程同时可以修改数据。在 Linux 程序中,fork () 会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,尽量避免膨胀。

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

lastsave
1

# 总结

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

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

检查修复 dump.rdb 文件

redis-check-rdb dump6379.rdb
1

动态所有停止 RDB 保存规则的方法:

redis-cli config set save ""
1

或者在 redis.config

save ""
1

配置中 rdb 优化手段

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

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

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

rdb-del-sync-files no # 在没有持久性的情况下删除复制中使用的RDB文件启用。默认情况下no,此选项是禁用的。 
1
2
3
4
5
6
7
  • RDB 是一个非常紧凑的文件
  • RDB 在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以 RDB 持久化方式可以最大化 redis 的性能。
  • 与 AOF 相比,在恢复大的数据集的时候,RDB 方式会更快一些。
  • 数据丢失风险大
  • RDB 需要经常 fork 子进程来保存数据集到硬盘上,当数据集比较大的时候,fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级不能相应客户端请求

# AOF(Append Only File)

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

image-20231122162325415

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

# AOF 缓冲区三种写回策略

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

# 配置文件

默认情况下,redis 是没有开启 AOF (append only file) 的。开启 AOF 功能需要设置配置:

appendonly yes # 开启aof
appendfsync everysec # aof写回策略
1
2

Aof 保存的是 appendonly.aof 文件,在 redis6 之前,AOF 保存文件的位置和 RDB 保存文件的位置一样, 都是通过 redis.conf 配置文件的 dir 配置。redis7 之后是 dir + appenddirname

dir /myredis
appenddirname "appendonlydir" # redis7新增的配置项,可以修改aof文件保存路径
appendfilename "appendonly.aof" # aof文件名称
1
2
3

redis7 新增了 Redis7.0 Multi Part AOF 的设计,顾名思义,MP-AOF 就是将原来的单个 AOF 文件拆分成多个 AOF 文件。在 MP-AOF 中,我们将 AOF 分为三种类型分别为:

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

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

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

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

image-20231122164342540

# 与 RDB 对比

AOF 更好的保护数据不丢失 、性能高、可做紧急恢复。但相同数据集的数据而言 aof 文件要远大于 rdb 文件,恢复速度慢于 rdb,aof 运行效率要慢于 rdb, 每秒同步策略效率较好,不同步效率和 rdb 相同。

AOF 异常修复命令:

redis-check-aof --fix
1

# AOF 重写机制

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

开启 AOF 文件的内容压缩,在 Redis.conf 进行配置

# 同时满足,且的关系才会触发重写机制
auto-aof-rewrite-percentage 100 # 根据上次重写后的aof大小,判断当前aof大小是不是增长了1倍
auto-aof-rewrite-min-size 64mb # 重写时满足的文件大小
1
2
3

满足配置文件中的选项后,Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时。

当然我们可以通过命令的方式手动触发重写机制

bgrewriteaof
1

重写机制效果举个例子:比如有个 key,执行了以下命令

set k1 v1
set k1 v2
set k1 v3
1
2
3

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

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

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

# 重写原理

  1. 在重写开始前,redis 会创建一个 “重写子进程”,这个子进程会读取现有的 AOF 文件,并将其包含的指令进行分析压缩并写入到一个临时文件中。
  2. 与此同时,主进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的 AOF 文件中,这样做是保证原有的 AOF 文件的可用性,避免在重写过程中出现意外。
  3. 当 “重写子进程” 完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新 AOF 文件中
  4. 当追加结束后,redis 就会用新 AOF 文件来代替旧 AOF 文件,之后再有新的写指令,就都会追加到新的 AOF 文件中
  5. 重写 aof 文件的操作,并没有读取旧的 aof 文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的 aof 文件,这点和快照有点类似

# 总结

image-20231122170308026

image-20231122170332440

  • AOF 文件时一个只进行追加的日志文件

  • Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写

  • AOF 文件有序地保存了对数据库执行的所有写入操作,这些写入操作以 Redis 协议的格式保存,因此

    AOF 文件的内容非常容易被人读懂对文件进行分析也很轻

  • 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积

  • 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB

# RDB-AOF 混合持久化

在同时开启 rdb 和 aof 持久化时,重启时只会加载 aof 文件,不会加载 rdb 文件

image-20231122170642759

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

RDB 的数据不实时,同时使用两者时服务器重启也只会找 AOF 文件。那要不要只使用 AOF 呢?

作者建议不要,因为 RDB 更适合用于备份数据库 (AOF 在不断变化不好备份),留着 rdb 作为一个万一的手段。

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

开启混合方式设置

aof-use-rdb-preamble yes
1

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

先使用 RDB 进行快照存储,然后使用 AOF 持久化记录所有的写操作,当重写策略满足或手动触发重写的时候,将最新的数据存储为新的 RDB 记录。这样的话,重启服务的时候会从 RDB 和 AOF 两部分恢复数据,既保证了数据完整性,又提高了恢复数据的性能。

简单来说:混合持久化方式产生的文件一部分是 RDB 格式,一部分是 AOF 格式。----》AOF 包括了 RDB 头部 + AOF 混写

image-20231122171103259

# 纯缓存模式

同时关闭 RDB+AOF

save "" # 禁用 RDB
appendonly no # 禁用 AOF
1
2
  • 禁用 rdb 持久化模式下,我们仍然可以使用命令 save、bgsave 生成 rdb 文件
  • 禁用 aof 持久化模式下,我们仍然可以使用命令 bgrewriteaof 生成 aof 文件

# 事务

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

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

Redis 事务 VS 数据库事务

  • 单独的隔离操作:Redis 的事务仅仅是保证事务里的操作会被连续独占的执行,redis 命令执行是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的
  • 没有隔离级别的概念:因为事务提交前任何指令都不会被实际执行,也就不存在” 事务内的查询要看到事务里的更新,在事务外查询不能看到” 这种问题了
  • 不保证原子性:Redis 的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力
  • 排它性:Redis 会保证一个事务内的命令依次执行,而不会被其它命令插入
MULTI # 标记一个事务块的开始
EXEC # 执行所有事务块内的命令
WATCH key [key ...] # 监控一个或者多个key,如果这些key在事务之前被其他命令变动则事务被打断
UNWATCH # 取消监控所有key
1
2
3
4

如在事务块有任何的语法出错或执行时出错,都会导致事务的打断。

# watch 监控

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

  • 悲观锁 (Pessimistic Lock):顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 block 直到它拿到锁。
  • 乐观锁 (Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
    • 乐观锁策略:提交版本必须 大于 记录当前版本才能执行更新
  • CAS

# 管道

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

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

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

image-20231122191304484

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

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

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

cat cmd.txt
1
set k100 v100
set k200 v200
hset k300 name 123
hset k300 age 20
1
2
3
4

执行 redis 命令

cat cmd.txt | redis-cli -a 123456 --pipe
1

Pipeline 与原生批量命令对比

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

Pipeline 与事务对比

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

使用 Pipeline 注意事项

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

# 发布订阅

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

Redis 可以实现消息中间件 MQ 的功能,通过发布订阅实现消息的引导和分流。

Redis 客户端可以订阅任意数量的频道

image-20231122193749801

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

image-20231122193802066

# 订阅频道

订阅给定的一个或多个频道的信息

SUBSCRIBE channel [channel ...]
1

推荐先执行订阅后再发布,订阅成功之前发布的消息是收不到的,订阅的客户端每次可以收到一个 3 个参数的消息,消息的种类、始发频道的名称、实际的消息内容

# 发布消息

PUBLISH channel message # 发布消息到指定的频道
PSUBSCRIBE pattern [pattern ...] # 按照模式批量订阅,订阅一个或多个符合给定模式(支持*号?号之类的)的频道

PUBSUB subcommand [argument [argument ...]] # 查看订阅与发布系统状态
PUBSUB CHANNELS # 由活跃频道组成的列表
PUBSUB NUMSUB  [channel [channel ...]] # 某个频道有几个订阅者
PUBSUB NUMPAT # 只统计使用PSUBSCRIBE命令执行的,返回客户端订阅的唯一模式的数量

1
2
3
4
5
6
7
8

# 取消订阅

UNSUBSCRIBE [channel [channel ...]] # 取消订阅
PUNSUBSCRIBE [pattern [pattern ...]] # 退订所有给定模式的频道
1
2

Pub/Sub 缺点:

  • 发布的消息在 Redis 系统中不能持久化,因此,必须先执行订阅,再等待消息发布。如果先发布了消息,那么该消息由于没有订阅者,消息将被直接丢弃
  • 消息只管发送对于发布者而言消息是即发即失的,不管接收,也没有 ACK 机制,无法保证消息的消费成功。
  • 以上的缺点导致 Redis 的 Pub/Sub 模式就像个小玩具,在生产环境中几乎无用武之地,为此 Redis5.0 版本新增了 Stream 数据结构,不但支持多播,还支持数据持久化,相比 Pub/Sub 更加的强大

# 过期数据

Redis 是一个内存级别的数据库 所有数据都存放在内存中 我们可以通过指令 TTL 获取指定数据状态

  • XX 具有时效性的数据
  • -1 永久有效的数据
  • -2 已经过期的数据 或被 已删除 或 未定义的数据

image-20210924090500046

我们通过 SETEX 设置的有效时间 redis 会新开辟一个键值对 用来存储 key 对应的 value 地址值 以及存活时间

# 定时删除

创建一个定时器,当 key 设置的过期时间到达时,由定时器执行的对键删除操作

优化:节约内存 快速释放不必要的内容

缺点:cpu 负载量大 会影响 redis 服务器响应时间和指令吞吐量

使用处理性能换取存储空间

# 惰性删除

数据到达过期时间,不做处理,等下次访问该数据时删除

  • 如果未过期 返回数据
  • 如果已过期 删除 返回不存在

通过 get 方法查询时,get 绑定 expirelfNeeded () 方法 此方法会检测数据是否过期

优点:节约 cpu 性能 只有访问时才进行判别删除

缺点:内存压力大 出现长期占用内容

用存储空间换取处理器性能

# 定期删除

通过设置 redis.config 文件中的 hz 属性 默认为 10

为每秒执行 hz 的次数

设置后服务器每秒执行 hz 次 serverCron () --> databasesCron () --> activeExpireCycle ()

activeExpireCycle () 对每个 expires [*](分成若干区) 逐一进行检测 每次执行 250ms/hz

对某个 expires [*] 检测时,随机挑选 W 个 key 检测

  • 如果 key 超时,删除 key
  • 如果一轮中删除 key 的数据 > W*25%, 循环该过程
  • 如果一轮中删除 key 的数据 <=W*25%, 则检查下一个 expires [*]
  • W 取值 = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 属性值

参数 current_db 用于记录 activeExpireCycle () 进入哪个 expires [*] 执行

如果 activeExpireCyle () 执行时间到期,下次从 current_db 继续向下执行

优点:cpu 性能占用设置峰值 检测频度可自定义

内容压力不是很大 长期占用内存的冷数据会被清理掉

# 淘汰策略

当 redis 使用内容存储数据,执行每一个命令前,会调用 freeMemoryIfNeeded () 检测内存是否充足。如果不满足加入数据的最低存储要求,则 redis 会为当前指令清理存储空间。清理数据的策略又称为逐出算法

逐出数据的过程不是百分百能够清理出足够的可使用空间,如果不成功则反复执行。当对所有数据尝试完毕,如果仍然不能到达内存清理要求,将出现错误信息

而 redis 中内存空间是根据物理内存和配置信息来指定

  • maxmenory ?mb 默认值为 0 不做限制 通常设置在 50% 以上

  • maxmemory-samples count 每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据

  • maxmemory-policy policy 对数据进行删除的选择策略

    • 检测易失数据 (可能会过期的数据集 server.db [i].expires)
      • volatile-lru 挑选最近最少使用的数据淘汰
      • valatile-lfu 挑选最近使用次数最少的数据淘汰
      • valatile-tfl 挑选将要过期的数据淘汰
      • volatile-random 任意选择数据淘汰
      • image-20210924142017817
    • 检测全库数据 (所有数据集 server.db [i].dict)
      • allkeys-lru 挑选最近最少使用的数据淘汰
      • allkeys-lfu 挑选最近使用次数最少的数据淘汰
      • allkeys-random 任意选择数据淘汰
    • 放弃数据驱逐
      • no-enviction (驱逐) 禁止驱逐数据 (redis4.0 中默认策略) 会引发错误 OOM (Out Of Memory)

    使用 INFO 命令输出监控信息,查询缓存 hit 和 miss 的次数,根据业务需求调用 Redis 配置

# 主从复制

image-20210924144342323

就是主从复制,master 以写为主,Slave 以读为主。当 master 数据变化的时候,自动将新的数据异步同步到其它 slave 数据库。

# 互联网 "三高" 架构

  • 高并发
  • 高性能
  • 高可用
    • 可用性 = (1 年的总时间 - 1 年宕机的总时间) / 1 年的总时间 * 100% : 业界可用性目标 5 个 9 即 99.999% 即服务器年宕机时长低于 315 秒 约 5.25 分钟

# 阶段一:建立连接阶段

建立 slave 到 master 的连接,使用 master 能够识别 slave, 并保存 slave 端口号

  1. saveof ip port : salve 发送指令给 master
  2. master 接受到指令,响应对方
  3. 保存 master 的 ip 与端口 masterhost 和 masterport
  4. 根据保存的信息创建连接 master 的 socket
  5. 周期性发送 ping 指令
  6. 响应 pong
  7. 发送指令 auth password (如无密码则忽略)
  8. 验证授权
  9. replconf listening-port <port-number>
  10. 保存 slave 的端口号

查看复制节点的主从关系和配置信息

info replication
1

# 主从连接 (slave 连接 master)

每次与 master 断开之后,都需要重新连接,除非你配置进 redis.conf 文件。

在运行期间修改 slave 节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系转而和新的主数据库同步,重新拜码头。

  • 方式一 从机客户端发送命令 slaveof masterip masterport
  • 方式二 启动服务器时连接 redis-server -slaveof masterip masterport
  • 方式三 服务器配置 在从机服务器的 redis 服务器配置文件 加上 slaveof masterip masterport

# 主从断开连接

断开 slave 与 master 的连接 不会删除已有数据 只是不再接受 master 发送的数据,使当前数据库停止与其他数据库的同步,转成主数据库,自立为王

salveof no one  # 在从机上执行,转成主数据库,自立为王
1

# 授权访问

image-20210924150746772

# 阶段二:数据同步阶段工作流程

image-20210924151257353

  1. 如果 master 数据量巨大,数据同步阶段应该避开流量高峰期,避免造成阻塞

  2. 如果复制缓冲区设置不合理,会导致数据溢出。如果全量复制周期太长,进行部分复制时发现数据已经存在丢失,必须进行第二次全量复制 导致 slave 陷入死循环

    #在主服务器配置文件配置 配置缓存区大小
    repl-backlog-size ?mb
    
    1
    2
  3. master 单机内存占用主机内存比例应在 50%-70%, 留下 30-50 作为 bgsave 命令和创建复制缓冲区

  4. 在 slave 进行全量复制 / 部分复制时 建议关闭此时对外服务

    #在从服务器配置文件中配置  关闭和开启
    slave-serve-stale-data yes|no
    
    1
    2
  5. 数据同步阶段 master 发送给 slave 信息可以理解为 master 是 slave 的一个客户端 主动向 slave 发送命令

  6. 多个 slave 同时对 master 请求数据同步 master 发送的 rdb 文件增多 如果 master 带宽不足 会对带宽造成巨大冲击 建议适量错峰请求同步

  7. salve 过多时 建议调整拓扑结构 由一主多从结构变为树状结构 中间的节点即使 master 也 salve 使用树状结构时 由于层级深度 导致深度越高的 slave 与最顶层 master 间数据同步延迟较大 数据一致性变差 应谨慎选择

# 阶段三:命令传播阶段

命令传播阶段出现了断网现象:

网络闪断闪连:忽略

短时间网络中断:部分复制

长时间网络中断:全量复制

工作原理

  • 通过 offset 区分不同的 slave 当前数据传播的差异
  • master 记录已发送的信息对应的 offset
  • slave 记录已接收的信息对应的 offset

image-20210924160149097

# 流程更新

image-20210924160451925

# 心跳机制

进入命令传播阶段候,master 与 slave 间需要进行信息交换,使用心跳机制进行维护,实现双方连接保持在线

master 心跳:

  • 内部指令:PING
  • 周期:由 repl-ping-slave-period 决定,默认 10 秒
  • 作用:判断 slave 是否在线
  • 查询:INFO replication 获取 slave 最后一次连接时间间隔,lag 项维持在 0 或 1 视为正常

slave 心跳任务

  • 内部指令:REPLCONF ACK {offset}
  • 周期:1 秒
  • 作用 1:汇报 slave 自己的复制偏移量,获取最新的数据变更指令
  • 作用 2:判断 master 是否在线

心跳阶段注意事项:

  • 当 slave 多数掉线,或延迟过高时,master 为保障数据稳定性,将拒绝所有信息同步
#在主服务器配置文件中配置
min-slaves-to-write 2
min-slaves-max-lag 8
1
2
3

slave 数量少于 2 个,或者所有 slave 的延迟都大于等于 8 秒时,强制关闭 master 写功能,停止数据同步

  • slave 数量由 slave 发送 REPLCONF ACK 命令做确认

  • slave 延迟由 slave 发送 REPLCONF ACK 命令做确认

image-20210924161711708

# 频繁的全量复制

  • 伴随着系统的运行,master 的数据量会越来越大,一旦 master 重启,runid 将发生变化,会导致全部 slave 的全量复制操作

内部优化调整方案:

1:master 内部创建 master_replid 变量,使用 runid 相同的策略生成,长度 41 位,并发送给所有 slave

2:在 master 关闭时执行命令 shutdown save,进行 RDB 持久化,将 runid 与 offset 保存到 RDB 文件中

  • repl-id repl-offset

  • 通过 redis-check-rdb 命令可以查看该信息

3:master 重启后加载 RDB 文件,恢复数据,重启后,将 RDB 文件中保存的 repl-id 与 repl-offset 加载到内存中

  • master_repl_id=repl master_repl_offset =repl-offset

  • 通过 info 命令可以查看该信息

作用:本机保存上次 runid,重启后恢复该值,使所有 slave 认为还是之前的 master

  • 第二种出现频繁全量复制的问题现象:网络环境不佳,出现网络中断,slave 不提供服务

问题原因:复制缓冲区过小,断网后 slave 的 offset 越界,触发全量复制

最终结果:slave 反复进行全量复制

解决方案:修改复制缓冲区大小

repl-backlog-size ?mb
1

建议设置如下:

1. 测算从 master 到 slave 的重连平均时长 second

2. 获取 master 平均每秒产生写命令数据总量 write_size_per_second

3. 最优复制缓冲区空间 = 2 * second * write_size_per_second

# 频繁的网络中断

  • 问题现象:master 的 CPU 占用过高 或 slave 频繁断开连接

问题原因

  • slave 每 1 秒发送 REPLCONFACK 命令到 master
  • 当 slave 接到了慢查询时(keys * ,hgetall 等),会大量占用 CPU 性能
  • master 每 1 秒调用复制定时函数 replicationCron (),比对 slave 发现长时间没有进行响应

最终结果:master 各种资源(输出缓冲区、带宽、连接等)被严重占用

解决方案:通过设置合理的超时时间,确认是否释放 slave

repl-timeout seconds
1

该参数定义了超时时间的阈值(默认 60 秒),超过该值,释放 slave

# 数据不一致

问题现象:多个 slave 获取相同数据不同步

问题原因:网络信息不同步,数据发送有延迟

解决方案

  • 优化主从间的网络环境,通常放置在同一个机房部署,如使用阿里云等云服务器时要注意此现象
  • 监控主从节点延迟(通过 offset)判断,如果 slave 延迟过大,暂时屏蔽程序对该 slave 的数据访问
slave-serve-stale-data	yes|no
1

开启后仅响应 info、slaveof 等少数命令(慎用,除非对数据一致性要求很高)

# 复制原理和工作流程

  • slave 启动,同步初请,slave 启动成功连接到 master 后会发送一个 sync 命令,slave 首次全新连接 master, 一次完全同步(全量复制) 将被自动执行,slave 自身原有数据会被 master 数据覆盖清除
  • 首次连接,全量复制,master 节点收到 sync 命令后会开始在后台保存快照 (即 RDB 持久化,主从复制时会触发 RDB),同时收集所有接收到的用于修改数据集命令缓存起来,master 节点执行 RDB 持久化完后,master 将 rdb 快照文件和所有缓存的命令发送到所有 slave, 以完成一次完全同步。而 slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化
  • 心跳持续,保持通信, repl-ping-replica-period 10 master 发出 PING 包的周期,默认是 10 秒
  • 进入平稳,增量复制,Master 继续将新的所有收集到的修改命令自动依次传给 slave, 完成同步
  • 从机下线,重连续传,master 会检查 backlog 里面的 offset,master 和 slave 都会保存一个复制的 offset 还有一个 masterId,offset 是保存在 backlog 中的。Master 只会把已经复制的 offset 后面的数据复制给 Slave,类似断点续传

# 复制的缺点

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

image-20231122202036520

# 哨兵模式

哨兵 (sentinel) 是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的 master 并将所有 slave 连接到新的 master

哨兵也是一台 redis 服务器 只是不提供数据相关服务 通常哨兵的数量配置为单数

# 配置哨兵

通过 sentinel.conf 配置

sentinel monitor master_name  master_host master_port quorum  # 设置哨兵监听的主服务器信息 quorum表示参与投票的哨兵数量  设置超过哨兵半数即可
sentinel down-after-milliseconds master_name million_seconds  # 设置判定服务器宕机时长 该设置控制是否进行主从切换
sentinel failover-timeout master_name million_seconds  # 设置故障切换的最大超时时长,如果超过设置的毫秒,表示故障转移失败
sentinel parallel-syncs master_name sync_slave_number  # 设置主从切换后,同时进行数据的slave数量 值越大 要求网络资源越高 值越小 同步时间越长
sentinel auth-pass <master-name> <password> # master设置了密码,连接master服务的密码
sentinel notification-script <master-name> <script-path> # 配置当某一事件发生时所需要执行的脚本
sentinel client-reconfig-script <master-name> <script-path> # 客户端重新配置主节点参数脚本
1
2
3
4
5
6
7

启动哨兵

redis-sentinel filename --sentinel # filename为sentinel.conf配置文件路径  启动哨兵
1

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

# 阶段一:监控阶段

  • 获取各个 sentinel 的状态(是否在线)

  • 获取 master 的状态

  • 获取所有 slave 的状态(根据 master 中的 slave 信息)

image-20210924164930119

# 阶段二:通知阶段

sentinel 在通知阶段要不断的去获取 master/slave 的信息,然后在各个 sentinel 之间进行共享,具体的流程如下:

image-20210924165124855

# 阶段三:故障转移

# 判断 master 宕机

image-20210924165317577

主观下线:任意一个 sentine 认为 master 已经下线

客观下线:半数以上的 sentine 认为 master 已经下线

# 选举 master

每个 sentine 都有一票 最终票最多的称为处理事故的哨兵服务

image-20210924165652150

首先它有一个在服务器列表中挑选备选 master 的原则

  • 不在线的 OUT

  • 响应慢的 OUT

  • 与原 master 断开时间久的 OUT

  • 优先原则

    ​ 优先级 ​ offset ​ runid

总结:故障转移阶段

  1. 发现问题,主观下线与客观下线
  2. 竞选负责人
  3. 优选新 master
  4. 新 master 上任,其他 slave 切换 master,原 master 作为 slave 故障恢复后连接

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

当一个主从配置中的 master 失效之后,sentinel 可以选举出一个新的 master 用于自动接替原 master 的工作,主从配置中的其他 redis 服务器自动指向新的 master 同步数据。一般建议 sentinel 采取奇数台,防止某一台 sentinel 无法连接到 master 导致误切换

# SDown 主观下线 (Subjectively Down)

SDOWN (主观不可用) 是单个 sentinel 自己主观上检测到的关于 master 的状态,从 sentinel 的角度来看, 如果发送了 PING 心跳后,在一定时间内没有收到合法的回复,就达到了 SDOWN 的条件。

sentinel 配置文件中的 down-after-milliseconds 设置了判断主观下线的时间长度

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

# ODown 客观下线 (Objectively Down)

ODOWN 需要一定数量的 sentinel,多个哨兵达成一致意见才能认为一个 master 客观上已经宕掉

sentinel monitor master_name  master_host master_port quorum  # 设置哨兵监听的主服务器信息 quorum表示参与投票的哨兵数量  设置超过哨兵半数即可
1

quorum 这个参数是进行客观下线的一个依据,法定人数 / 法定票数意思是至少有 quorum 个 sentinel 认为这个 master 有故障才会对这个 master 进行下线以及故障转移。因为有的时候,某个 sentinel 节点可能因为自身网络原因导致无法连接 master,而此时 master 并没有出现故障,所以这就需要多个 sentinel 都一致认为该 master 有问题,才可以进行下一步操作,这就保证了公平性和高可用

# 选举出领导者哨兵 (哨兵中选出兵王)

当主节点被判断客观下线以后,各个哨兵节点会进行协商,先选举出一个 ** 领导者哨兵节点(兵王)** 并由该领导者节点,也即被选举出的兵王进行 failover(故障迁移)

哨兵领导者,兵王如何选出来的?

# Raft 算法

image-20231122203738199

监视该主节点的所有哨兵都有可能被选为领导者,选举使用的算法是 Raft 算法;

Raft 算法的基本思路是先到先得:即在一轮选举中,哨兵 A 向 B 发送成为领导者的申请,如果 B 没有同意过其他哨兵,则会同意 A 成为领导者

# 由兵王开始推动故障切换流程并选出一个新 master

# 新主登基

某个 Slave 被选中成为新 Master,选出新 master 的规则,剩余 slave 节点健康前提下

image-20231122203916615

  • redis.conf 文件中,优先级 slave-priority 或者 replica-priority 最高的从节点 (数字越小优先级越高)
  • 复制偏移位置 offset 最大的从节点
  • 最小 Run ID 的从节点,字典顺序,ASCII 码
# 群臣俯首

一朝天子一朝臣,换个码头重新拜

执行 slaveof no one 命令让选出来的从节点成为新的主节点,并通过 slaveof 命令让其他节点成为其从节点,Sentinel leader 会对选举出的新 master 执行 slaveof no one 操作,将其提升为 master 节点,Sentinel leader 向其它 slave 发送命令,让剩余的 slave 成为新的 master 节点的 slave。

# 旧主拜服

老 master 回来也认怂

将之前已下线的老 master 设置为新选出的新 master 的从节点,当老 master 重新上线后,它会成为新 master 的从节点,Sentinel leader 会让原来的 master 降级为 slave 并恢复正常工作。

# Cluster 集群

集群就是使用网络将若干台计算机联通起来,并提供统一的管理方式,使其对外呈现单机的服务效果

Redis 集群是一个提供在多个 Redis 节点间共享数据的程序集,Redis 集群可以支持多个 Master。

由于 Cluster 自带 Sentinel 的故障转移机制,内置了高可用的支持,无需再去使用哨兵功能。客户端与 Redis 的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可。槽位 slot 负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系

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

Redis 集群的数据分片,Redis 集群没有使用一致性 hash 而是引入哈希槽的概念。

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

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

image-20231123022856928

使用 Redis 集群时我们会将存储的数据分散到多台 redis 机器上,这称为分片。简言之,集群中的每个 Redis 实例都被认为是整个数据的一个分片。

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

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

# slot 槽位映射

# 哈希取余分区

1

2 亿条记录就是 2 亿个 k,v,我们单机不行必须要分布式多机,假设有 3 台机器构成一个集群,用户每次读写操作都是根据公式: $hash (key) % N 个机器台数,计算出哈希值,用来决定数据映射到哪一个节点上。

优点:

简单粗暴,直接有效,只需要预估好数据规划好节点,例如 3 台、8 台、10 台,就能保证一段时间的数据支撑。使用 Hash 算法让固定的一部分请求落到同一台服务器上,这样每台服务器固定处理一部分请求(并维护这些请求的信息),起到负载均衡 + 分而治之的作用。

缺点:

原来规划好的节点,进行扩容或者缩容就比较麻烦了额,不管扩缩,每次数据变动导致节点有变动,映射关系需要重新进行计算,在服务器个数固定不变时没有问题,如果需要弹性扩容或故障停机的情况下,原来的取模公式就会发生变化:Hash (key)/3 会变成 Hash (key) /?。此时地址经过取余运算的结果将发生很大变化,根据公式获取的服务器也会变得不可控。 某个 redis 机器宕机了,由于台数数量变化,会导致 hash 取余全部数据重新洗牌。

# 一致性哈希算法分区

一致性 Hash 算法背景 一致性哈希算法在 1997 年由麻省理工学院中提出的,设计目标是为了解决分布式缓存数据变动和映射问题,某个机器宕机了,分母数量改变了,自然取余数不 OK 了。

提出一致性 Hash 解决方案。目的是当服务器个数发生变动时,尽量减少影响客户端到服务器的映射关系。

# 一致性哈希环

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

它也是按照使用取模的方法,前面介绍的节点取模法是对节点(服务器)的数量进行取模。而一致性 Hash 算法是对 2^32 取模,简单来说,一致性 Hash 算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数 H 的值空间为 0-2^32-1(即哈希值是一个 32 位无符号整形),整个哈希环如下图:整个空间按顺时针方向组织,圆环的正上方的点代表 0,0 点右侧的第一个点代表 1,以此类推,2、3、4、…… 直到 2^32-1,也就是说 0 点左侧的第一个点代表 2^32-1, 0 和 2^32-1 在零点中方向重合,我们把这个由 2^32 个点组成的圆环称为 Hash 环。

image-20231123023610028

# 节点映射

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

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

image-20231123023635625

# 落键规则

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

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

image-20231123023711575

# 优点

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

image-20231123023749586

扩展性

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

image-20231123023811860

# 缺点

一致性哈希算法的数据倾斜问题

Hash 环的数据倾斜问题 一致性 Hash 算法在服务节点太少时,容易因为节点分布不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题, 例如系统中只有两台服务器:

image-20231123023914358

# 哈希槽分区

哈希槽实质就是一个数组,数组 [0,2^14 -1] 形成 hash slot 空间。

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

image-20231123024008528

槽解决的是粒度问题,相当于把粒度变大了,这样便于数据移动。哈希解决的是映射问题,使用 key 的哈希值来计算所在的槽,便于数据分配

一个集群只能有 16384 个槽,编号 0-16383(0-214−1)。这些槽会分配给集群中的所有主节点,分配策略没有要求。 集群会记录节点和槽的对应关系,解决了节点和槽的关系后,接下来就需要对 key 求哈希值,然后对 16384 取模,余数是几 key 就落入对应的槽里。HASH_SLOT = CRC16 (key) mod 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。

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

System.out.println(SlotHash.getSlot("A")); //6373
System.out.println(SlotHash.getSlot("B")); //10374
System.out.println(SlotHash.getSlot("C")); //14503
System.out.println(SlotHash.getSlot("hello")); //866
1
2
3
4

# 为什么 redis 集群的最大槽数是 16384 个?

Redis 集群并没有使用一致性 hash 而是引入了哈希槽的概念。Redis 集群有 16384 个哈希槽,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。但为什么哈希槽的数量是 16384(214)个呢?

CRC16 算法产生的 hash 值有 16bit,该算法可以产生216=65536 个值。 换句话说值是分布在 0~65535 之间,有更大的 65536 不用为什么只用 16384 就够? 作者在做 mod 运算的时候,为什么不 mod65536,而选择 mod16384? HASH_SLOT = CRC16 (key) mod 65536 为什么没启用

正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。 这意味着它们包含原始节点的插槽配置,该节点使用 2k 的空间和 16k 的插槽,但是会使用 8k 的空间(使用 65k 的插槽)。 同时,由于其他设计折衷,Redis 集群不太可能扩展到 1000 个以上的主节点。

因此 16k 处于正确的范围内,以确保每个主机具有足够的插槽,最多可容纳 1000 个矩阵,但数量足够少,可以轻松地将插槽配置作为原始位图传播。请注意,在小型群集中,位图将难以压缩,因为当 N 较小时,位图将设置的 slot / N 位占设置位的很大百分比。

image-20231123024451264

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

在消息头中最占空间的是 myslots [CLUSTER_SLOTS/8]。 当槽位为 65536 时,这块的大小是: $65536÷8÷1024=8kb $ 在消息头中最占空间的是 myslots [CLUSTER_SLOTS/8]。 当槽位为 16384 时,这块的大小是: $16384÷8÷1024=2kb $ 因为每秒钟,redis 节点需要发送一定数量的 ping 消息作为心跳包,如果槽位为 65536,这个 ping 消息的消息头太大了,浪费带宽。

redis 的集群主节点数量基本不可能超过 1000 个。

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

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

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

# 不保证强一致性

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

# Cluster 集群结构设计

数据存储设计:

  1. 通过算法设计,计算出 key 应该保存的位置

  2. 将所有的存储空间计划切割成 16384 份,每台主机保存一部分

    注意:每份代表的是一个存储空间,不是一个 key 的保存空间

  3. 将 key 按照计算出的结果放到对应的存储空间

image-20210924171112372 那 redis 的集群是如何增强可扩展性的呢?譬如我们要增加一个集群节点

image-20210924171123991

当我们查找数据时,集群是如何操作的呢?

  • 各个数据库相互通信,保存各个库中槽的编号数据
  • 一次命中,直接返回
  • 一次未命中,告知具体位置

image-20210924171145504

# 配置

在 redis-6401.conf 中配置

cluster-enabled yes # 是否启用cluster,加入cluster节点
cluster-config-file cluster-6503.conf # cluster配置文件名,该文件属于自动生成,仅用于快速查找文件并查询文件内容
cluster-node-timeout 5000 # 节点服务响应超时时间,用于判定该节点是否下线或切换为从节点 毫秒数
cluster-migration-barrier 2 # master连接的slave最小数量
1
2
3
4

# 启动 Culster

redis-cli –-cluster create masterhost1:masterport1 masterhost2:masterport2  masterhost3:masterport3 [masterhostn:masterportn …] slavehost1:slaveport1  slavehost2:slaveport2 slavehost3:slaveport3 -–cluster-replicas n

# 案例
redis-cli -a 111111 --cluster create 192.168.111.175:6381 192.168.111.175:6382 192.168.111.172:6383 192.168.111.172:6384 192.168.111.174:6385 192.168.111.174:6386 --cluster-replicas 1
1
2
3
4

n 为多少个主服务器 前 n 个 ip 为主服务器 后面为 slave 服务器

  • cluster nodes 查询集群节点信息
info replication # 查看集群状态信息
cluster info # 当前客户端状态
cluster nodes # 节点状态
1
2
3

客户端连接集群需要添加 -c 参数,防止路由失效

redis-cli -a 123456 -p 6381 -c
1

如果节点主从发生变化,可以手动更新主从关系

cluster failover
1

# 节点增删

  • 添加 master 到当前集群中,连接时可以指定任意现有节点地址与端口

    redis-cli --cluster add-node new-master-host:new-master-port now-host:now-port
    
    1
  • 添加 slave

    redis-cli --cluster add-node new-slave-host:new-slave-port master-host:master-port --cluster-slave --cluster-master-id masterid
    
    1
  • 删除节点,如果删除的节点是 master,必须保障其中没有槽 slot

    redis-cli --cluster del-node del-slave-host:del-slave-port del-slave-id
    
    1
  • 重新分槽,分槽是从具有槽的 master 中划分一部分给其他 master,过程中不创建新的槽

    redis-cli --cluster reshard new-master-host:new-master:port --cluster-from src-  master-id1, src-master-id2, src-master-idn --cluster-to target-master-id --  cluster-slots slots
    #将需要参与分槽的所有masterid不分先后顺序添加到参数中,使用,分隔
    #指定目标得到的槽的数量,所有的槽将平均从每个来源的master处获取
    
    1
    2
    3
  • 重新分配槽,从具有槽的 master 中分配指定数量的槽到另一个 master 中,常用于清空指定 master 中的槽

    redis-cli --cluster reshard src-master-host:src-master-port --cluster-from src-  master-id --cluster-to target-master-id --cluster-slots slots --cluster-yes
    
    1

检查集群情况

redis-cli --cluster check 真实ip地址:6381
1

# 占位符和 CRC16 算法

不在同一个 slot 槽位下的多键操作支持不好,通识占位符登场

mset k1 11 k2 12 k3 13 # (error) CROSSSLOT Keys in request don't hash to the same slot
1

不在同一个 slot 槽位下的键值无法使用 mset、mget 等多键操作,可以通过 {} 来定义同一个组的概念,使 key 中 {} 内相同内容的键值对放到一个 slot 槽位去,对照下面类似 k1k2k3 都映射为 x,自然槽位一样

mset k1 {x} k2 {x} k3 {x}
mget k1 {x} k2 {x} k3 {x}
1
2

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

查看某个 key 该属于对应的槽位值

CLUSTER KEYSLOT 键名称 # 查看某个key该属于对应的槽位值
CLUSTER COUNTKEYSINSLOT 槽位数字编号 # 查询指定槽位是否被占用 1为被占用 0为未被占用
1
2

cluster.c

image-20231123030502289

# RedisTemplate 连接集群

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

server.port=7777

spring.application.name=redis7_study

# ========================logging=====================
logging.level.root=info
logging.level.com.atguigu.redis7=info
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n 

logging.file.name=D:/mylogs2023/redis7_study.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n

# ========================swagger=====================
spring.swagger2.enabled=true
#在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常,
#原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser,
# 导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
 
# ========================redis集群=====================
spring.redis.password=111111
# 获取失败 最大重定向次数
spring.redis.cluster.max-redirects=3
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
#支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
spring.redis.lettuce.cluster.refresh.adaptive=true
#定时刷新
spring.redis.lettuce.cluster.refresh.period=2000
# 集群节点地址
spring.redis.cluster.nodes=192.168.111.175:6381,192.168.111.175:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.174:6385,192.168.111.174:6386
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 缓存预热

1. 请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存,造成了短期内对 redis 的高强度操作从而导致问题

2. 主从之间数据吞吐量较大,数据同步操作频度较高

  • 前置准备工作:

1. 日常例行统计数据访问记录,统计访问频度较高的热点数据

2. 利用 LRU 数据删除策略,构建数据留存队列例如:storm 与 kafka 配合

  • 准备工作:

1. 将统计结果中的数据分类,根据级别,redis 优先加载级别较高的热点数据

2. 利用分布式多服务器同时进行数据读取,提速数据加载过程

3. 热点数据主从同时预热

  • 实施:

4. 使用脚本程序固定触发数据预热过程

5. 如果条件允许,使用了 CDN(内容分发网络),效果会更好

总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!

# 缓存雪崩

1. 系统平稳运行过程中,忽然数据库连接量激增

2. 应用服务器无法及时处理请求

3. 大量 408,500 错误页面出现

4. 客户反复刷新页面获取数据

5. 数据库崩溃

6. 应用服务器崩溃

7. 重启应用服务器无效

8.Redis 服务器崩溃

9.Redis 集群崩溃

10. 重启数据库后再次被瞬间流量放倒

解决方案

  • 思路:

1. 更多的页面静态化处理

2. 构建多级缓存架构

​ Nginx 缓存 + redis 缓存 + ehcache 缓存

3. 检测 Mysql 严重耗时业务进行优化

​ 对数据库的瓶颈排查:例如超时查询、耗时较高事务等

4. 灾难预警机制

​ 监控 redis 服务器性能指标

​ CPU 占用、CPU 使用率

​ 内存容量

​ 查询平均响应时间

​ 线程数

5. 限流、降级

短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问

  • 落地实践:

1.LRU 与 LFU 切换

2. 数据有效期策略调整

​ 根据业务数据有效期进行分类错峰,A 类 90 分钟,B 类 80 分钟,C 类 70 分钟

​ 过期时间使用固定时间 + 随机值的形式,稀释集中到期的 key 的数量

3. 超热数据使用永久 key

4. 定期维护(自动 + 人工)

​ 对即将过期数据做访问量分析,确认是否延时,配合访问量统计,做热点数据的延时

5. 加锁:慎用!

总的来说:缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的 出现(约 40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。

# 缓存击穿

1. 系统平稳运行过程中

2. 数据库连接量瞬间激增

3.Redis 服务器无大量 key 过期

4.Redis 内存平稳,无波动

5.Redis 服务器 CPU 正常

6. 数据库崩溃

问题排查:

1.Redis 中某个 key 过期,该 key 访问量巨大

2. 多个数据请求从服务器直接压到 Redis 后,均未命中

3.Redis 在短时间内发起了大量对数据库中同一数据的访问

总而言之就两点:单个 key 高热数据,key 过期

解决方案:

1. 预先设定

​ 以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息 key 的过期时长 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势

2. 现场调整

​ 监控访问量,对自然流量激增的数据延长过期时间或设置为永久性 key

3. 后台刷新数据

​ 启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失

4. 二级缓存

​ 设置不同的失效时间,保障不会被同时淘汰就行

5. 加锁

​ 分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重!

总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 redis 后,发起了大量对同一数据的数据库访问,导致对数 据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过 期监控难度较高,配合雪崩处理策略即可。

# 缓存穿透

1. 系统平稳运行过程中

2. 应用服务器流量随时间增量较大

3.Redis 服务器命中率随时间逐步降低

4.Redis 内存平稳,内存无压力

5.Redis 服务器 CPU 占用激增

6. 数据库服务器压力激增

7. 数据库崩溃

问题排查:

1.Redis 中大面积出现未命中

2. 出现非正常 URL 访问

问题分析:

  • 获取的数据在数据库中也不存在,数据库查询未得到对应数据
  • Redis 获取到 null 数据未进行持久化,直接返回
  • 下次此类数据到达重复上述过程
  • 出现黑客攻击服务器

解决方案:

1. 缓存 null

​ 对查询结果为 null 的数据进行缓存(长期使用,定期清理),设定短时限,例如 30-60 秒,最高 5 分钟

2. 白名单策略

​ 提前预热各种分类数据 id 对应的 bitmaps,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低)

​ 使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略)

2. 实施监控

​ 实时监控 redis 命中率(业务正常范围时,通常会有一个波动值)与 null 数据的占比

​ 非活动时段波动:通常检测 3-5 倍,超过 5 倍纳入重点排查对象

​ 活动时段波动:通常检测 10-50 倍,超过 50 倍纳入重点排查对象

​ 根据倍数不同,启动不同的排查流程。然后使用黑名单进行防控(运营)

4.key 加密

​ 问题出现后,临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验

​ 例如每天随机分配 60 个加密串,挑选 2 到 3 个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问

总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 redis 数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。应对策略应该在临时预案防范方面多做文章。

无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除。

# 性能指标监控

性能指标

  • latency 响应请求的平均时间
  • instantaneous_ops_per_sec 平均每秒处理请求总数
  • hit_rate (calculated) 缓存查询命中率(通过查询总次数与查询得到非 nil 数据总次数计算而来)

内存指标

  • used_memory 当前内存使用量
  • mem_fragmentation_ratio 内存碎片率(关系到是否进行碎片整理)
  • evicted_keys 为避免内存溢出删除的 key 的总数量
  • blocked_clients 基于阻塞操作(BLPOP 等)影响的客户端数量

活动指标

  • connected_clients 当前客户端连接总数
  • connected_slaves 当前连接 slave 总数
  • master_last_io_seconds_ago 最后一次主从信息交换距现在的秒
  • keyspace key 的总数

持久性指标

  • rdb_last_save_time 当前服务器最后一次 RDB 持久化的时间
  • rdb_changes_since_last_save 当前服务器最后一次 RDB 持久化后数据变化总量

错误指标

  • rejected_connections 被拒绝连接的客户端总数(基于达到最大连接值的因素)
  • keyspace_misses key 未命中的总次数
  • master_link_down_since_seconds 主从断开的秒数

# 性能指标工具

测试当前服务器的并发性能

redis-benchmark [-h ] [-p ] [-c ] [-n <requests]> [-k ]
1

image-20210924175043616

启动服务器调试信息

monitor
1

慢日志 记录查询慢的日志

  • slowlog [operator]
    • get 获取慢日志信息
    • len 获取慢日志条目数
    • reset 重置慢查询日志

慢日志通过 redis-xxx.conf 配置

slowlog-log-slower-than 1000 #设置慢查询的时间下线,单位:微妙
slowlog-max-len 100	#设置慢查询命令对应的日志显示长度,单位:命令数
1
2
编辑 (opens new window)
上次更新: 2023/12/13, 06:06:02
Redis
MongoDB

← Redis MongoDB→

最近更新
01
k8s
06-06
02
进程与线程
03-04
03
计算机操作系统概述
02-26
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Iekr | Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式