Redis基础篇

文章目录


image-20240508214114945

2 Redis入门概述

Redis是基于内存的KV键值对缓存数据库,常用作数据库、缓存和消息代理。一秒可以高达8w次的写入和10w次的读操作

Redis的特性:

  1. 高性能:因为数据存储在内存中,Redis 读写操作非常快速,适用于需要高吞吐量和低延迟的应用场景。
  2. 丰富的数据类型:Redis 支持多种数据结构,不仅仅是简单的键值对,还包括列表、集合、哈希、位图等。这使得 Redis 能够处理多种复杂的数据操作。
  3. 持久化:虽然 Redis 是基于内存的,但它支持数据持久化,可以将内存中的数据周期性地保存到磁盘上。主要有两种方式:RDB(快照)和 AOF(追加文件)。
  4. 复制和高可用性
    • Redis 支持主从复制,可以将数据从一个 Redis 服务器复制到其他多个服务器上,增强数据的高可用性和故障恢复能力。
    • Redis 还提供 Redis Sentinel,用于监控 Redis 实例并自动进行故障转移。
  5. 集群:Redis Cluster 是 Redis 的一种分布式实现,允许将数据分布在多个节点上,解决单点故障和扩展性问题。
  6. 事务:Redis 支持事务,可以确保一组命令的原子性执行,但其事务模型相对简单,不支持复杂的回滚机制。
  7. Lua 脚本:Redis 支持 Lua 脚本,可以让用户将多个命令打包成一个原子操作来执行,减少网络开销。
  8. 广泛的应用场景
    • 缓存:加速读取速度,减少数据库压力。
    • 会话存储:用于存储用户会话信息。
    • 实时分析:用于统计和分析实时数据。
    • 消息队列:利用列表、集合等结构实现消息队列功能。

Redis 的高性能和灵活性使其成为许多应用程序中不可或缺的一部分,特别是在需要快速响应和处理大量数据的场景中。

# 常用命令
redis-server -redis.conf
redis-cli -p 6379 -a 密码

3 Redis10大数据类型

查看命令官网:https://redis.io/commands/

所谓redis数据类型,指的是kv键值对中v的类型,而key永远是字符串

image-20240515130737722

3.1 Redis自字符串String

序号命令及描述
1SET key value 设置指定 key 的值。
2GET key 获取指定 key 的值。
3GETRANGE key start end 返回 key 中字符串值的子字符
4GETSET key value 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
5GETBIT key offset 对 key 所储存的字符串值,获取指定偏移量上的位(bit)。
6[MGET key1 key2…] 获取所有(一个或多个)给定 key 的值。
7SETBIT key offset value 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit)。
8SETEX key seconds value 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位)。
9SETNX key value 只有在 key 不存在时设置 key 的值。
10SETRANGE key offset value 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始。
11STRLEN key 返回 key 所储存的字符串值的长度。
12[MSET key value key value …] 同时设置一个或多个 key-value 对。
13[MSETNX key value key value …] 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。
14PSETEX key milliseconds value 这个命令和 SETEX 命令相似,但它以毫秒为单位设置 key 的生存时间,而不是像 SETEX 命令那样,以秒为单位。
15INCR key 将 key 中储存的数字值增一。
16INCRBY key increment 将 key 所储存的值加上给定的增量值(increment) 。
17INCRBYFLOAT key increment 将 key 所储存的值加上给定的浮点增量值(increment) 。
18DECR key 将 key 中储存的数字值减一。
19DECRBY key decrement key 所储存的值减去给定的减量值(decrement) 。
20APPEND key value 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾。

3.2 Redis列表List

Redis列表特点有序有重复,一键多值,底层一个双向链表结构,所以对两端的操作性能很高,通索引下标的操作中间的节点性能会较差

一个列表最多可以包含 232 - 1 个元素(40多亿)

序号命令及描述
1BLPOP key1 [ key2 ] timeout 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
2BRPOP key1 [ key2 ] timeout 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
3BRPOPLPUSH source destination timeout 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
4LINDEX key index 通过索引获取列表中的元素
5LINSERT key BEFORE|AFTER pivot value 在列表的元素前或者后插入元素
6LLEN key 获取列表长度
7LPOP key 移出并获取列表的第一个元素
8LPUSH key value1 [value2] 将一个或多个值插入到列表头部
9LPUSHX key value 将一个值插入到已存在的列表头部
10LRANGE key start stop 获取列表指定范围内的元素
11LREM key count value 移除列表元素
12LSET key index value 通过索引设置列表元素的值
13LTRIM key start stop 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
14RPOP key 移除列表的最后一个元素,返回值为移除的元素。
15RPOPLPUSH source destination 移除列表的最后一个元素,并将该元素添加到另一个列表并返回
16[RPUSH key value1 value2] 在列表中添加一个或多个值到列表尾部
17RPUSHX key value 为已存在的列表添加值

3.3 Redis哈希Hash

Redis哈希Hash类型,一键多值,值为kv键值对

Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。

序号命令及描述
1HDEL key field1 [field2] 删除一个或多个哈希表字段
2HEXISTS key field 查看哈希表 key 中,指定的字段是否存在。
3HGET key field 获取存储在哈希表中指定字段的值。
4HGETALL key 获取在哈希表中指定 key 的所有字段和值
5HINCRBY key field increment 为哈希表 key 中的指定字段的整数值加上增量 increment 。
6HINCRBYFLOAT key field increment 为哈希表 key 中的指定字段的浮点数值加上增量 increment 。
7HKEYS key 获取哈希表中的所有字段
8HLEN key 获取哈希表中字段的数量
9[HMGET key field1 field2] 获取所有给定字段的值
10[HMSET key field1 value1 field2 value2 ] 同时将多个 field-value (域-值)对设置到哈希表 key 中。
11HSET key field value 将哈希表 key 中的字段 field 的值设为 value 。
12HSETNX key field value 只有在字段 field 不存在时,设置哈希表字段的值。
13HVALS key 获取哈希表中所有值。
14[HSCAN key cursor MATCH pattern] [COUNT count] 迭代哈希表中的键值对。

3.4 Redis集合Set

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

集合中最大的成员数为 232 - 1 (40多亿)

序号命令及描述
1SADD key member1 [member2] 向集合添加一个或多个成员
2SCARD key 获取集合的成员数
3SDIFF key1 [key2] 返回第一个集合与其他集合之间的差异。
4SDIFFSTORE destination key1 [key2] 返回给定所有集合的差集并存储在 destination 中
5SINTER key1 [key2] 返回给定所有集合的交集
6SINTERSTORE destination key1 [key2] 返回给定所有集合的交集并存储在 destination 中
7SISMEMBER key member 判断 member 元素是否是集合 key 的成员
8SMEMBERS key 返回集合中的所有成员
9SMOVE source destination member 将 member 元素从 source 集合移动到 destination 集合
10SPOP key 移除并返回集合中的一个随机元素
11[SRANDMEMBER key count] 返回集合中一个或多个随机数
12SREM key member1 [member2] 移除集合中一个或多个成员
13SUNION key1 [key2] 返回所有给定集合的并集
14SUNIONSTORE destination key1 [key2] 所有给定集合的并集存储在 destination 集合中
15[SSCAN key cursor MATCH pattern] [COUNT count] 迭代集合中的元素

3.5 Redis有序集合Sorted Set

Redis有序集合Sorted Set(也叫Zset)特点和Set集合一样无序无重复,不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序

有序集合的成员是唯一的,但分数(score)却可以重复。

集合是通过哈希表实现的,所以增删找的复杂度都是 O(1)。 集合中最大的成员数为 232 - 1

序号命令及描述
1ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员,或者更新已存在成员的分数
2ZCARD key 获取有序集合的成员数
3ZCOUNT key min max 计算在有序集合中指定区间分数的成员数
4ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
5ZINTERSTORE destination numkeys key [key …] 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中
6ZLEXCOUNT key min max 在有序集合中计算指定字典区间内成员数量
7ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合指定区间内的成员
8ZRANGEBYLEX key min max [LIMIT offset count] 通过字典区间返回有序集合的成员
9[ZRANGEBYSCORE key min max WITHSCORES] [LIMIT] 通过分数返回有序集合指定区间内的成员
10ZRANK key member 返回有序集合中指定成员的索引
11[ZREM key member member …] 移除有序集合中的一个或多个成员
12ZREMRANGEBYLEX key min max 移除有序集合中给定的字典区间的所有成员
13ZREMRANGEBYRANK key start stop 移除有序集合中给定的排名区间的所有成员
14ZREMRANGEBYSCORE key min max 移除有序集合中给定的分数区间的所有成员
15[ZREVRANGE key start stop WITHSCORES] 返回有序集中指定区间内的成员,通过索引,分数从高到低
16ZREVRANGEBYSCORE key max min [WITHSCORES] 返回有序集中指定分数区间内的成员,分数从高到低排序
17ZREVRANK key member 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序
18ZSCORE key member 返回有序集中,成员的分数值
19ZUNIONSTORE destination numkeys key [key …] 计算给定的一个或多个有序集的并集,并存储在新的 key 中
20[ZSCAN key cursor MATCH pattern] [COUNT count] 迭代有序集合中的元素(包括元素成员和元素分值)

3.6 Redis地理空间 GEO

用于存储地理位置信息

Redis GEO 操作方法有:

  • geoadd:添加地理位置的坐标。
  • geopos:获取地理位置的坐标。
  • geodist:计算两个位置之间的距离。
  • georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
  • georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。
  • geohash:返回一个或多个位置对象的 geohash 值。

3.7 Redis基数统计 HyperLogLog

用于基数统计的数据结构。基数统计是指统计集合中不同元素的数量,例如在统计唯一访问用户数、唯一 IP 地址数等场景中非常有用。HyperLogLog 提供了一种高效且低内存占用的方法来近似计算集合的基数。

HyperLogLog 的特点

  1. 近似计算:HyperLogLog 使用概率算法来近似计算基数,误差率约为 0.81%。
  2. 低内存使用:无论集合大小如何,HyperLogLog 的内存使用量都是固定的,为每个 HyperLogLog 使用 12 KB 左右的内存。
  3. 高效的基数统计:相比传统方法,HyperLogLog 可以在常数时间内完成基数统计,非常适合大规模数据的统计。

常用命令:

  • PFADD
# 将一个或多个元素添加到 HyperLogLog 中,可以添加多个元素。
PFADD key element [element ...]
  • PFCOUNT
# 返回 HyperLogLog 的基数估计值。可以一次性查询多个 HyperLogLog,返回它们的并集基数估计值。
PFCOUNT key [key ...]
  • PFMERG
# 将多个 HyperLogLog 合并成一个 HyperLogLog。合并结果存储在 `destkey` 中。
PFMERGE destkey sourcekey [sourcekey ...]

3.8 Redis位图bitmap

Bitmap,即位图,存储的是一串连续的二进制数组(0和1)。位图操作非常适合用于处理大规模的布尔值集合,比如用户签到、活动参与标记等。

常用方法

  • SETBIT
# 将键 key 的位图中指定偏移量 offset 的位设置为 value(0 或 1)。
SETBIT key offset value
#如 SETBIT mybit 7 1 将mybit的第7位设置为 1
  • GETBIT
# 获取键 key 的位图中指定偏移量 offset 的位值
GETBIT key offset
  • BITCOUNT
# 统计键key的位图中值为1的位的数量。可以指定范围 [start, end] 来统计特定范围内的位数
BITCOUNT key [start end]
  • BITOP
# 对一个或多个键 key 的位图执行按位操作,并将结果存储到 destkey 中。operation 可以是 AND、OR、XOR、NOT 之一。
BITOP operation destkey key [key ...]
  • BITPOS
# 查找键key的位图中第一个值为 bit(0或1)的位的位置。可以指定可选的start和end字节范围
BITPOS key bit [start] [end]

3.9 Redis位域bitField

BITFIELD 用于对字符串的二进制表示进行复杂的位操作。BITFIELD 命令允许你以原子的方式操作多个位域,非常适合需要处理复杂位操作的场景。

BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]

# BITFIELD 的数据类型type
# u8、u16、u32 等:无符号整数,分别占 8 位、16 位、32 位。
# i8、i16、i32 等:有符号整数,分别占 8 位、16 位、32 位。

# 设置一个 8 位无符号整数值 100 到 mykey 的第 0 位开始
BITFIELD mykey SET u8 0 100

# 获取 mykey 的第 0 位开始的 8 位无符号整数
BITFIELD mykey GET u8 0

# 设置一个 8 位无符号整数值 255 到 mykey 的第 0 位开始
BITFIELD mykey SET u8 0 255

# 将 mykey 的第 0 位开始的 8 位无符号整数增加 1,溢出行为为环绕(WRAP)
BITFIELD mykey OVERFLOW WRAP INCRBY u8 0 1

# 获取 mykey 的第 0 位开始的 8 位无符号整数
BITFIELD mykey GET u8 0

3.10 Redis流Stream

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

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

4 Redis持久化

image-20231215212348650

官网:https://redis.io/docs/manual/persistence/

redis是kv键值对的内存数据库,一旦发生服务器崩溃或重启,内存中的数据就会丢失,因此引入redis持久化。持久化使得在服务器重新启动时可以从磁盘加载数据,从而避免了数据丢失

4.1 RDB Redis DataBase

**RDB:定期将内存中的数据集快照写入磁盘,也就是Snapshot内存快照。**每次备份时执行的都是全量快照,也就是把内存中的所有数据都记录到磁盘中。默认持久化文件为dump.rdb,redis重启会加载该持久化文件(加载文件的名称可以在配置文件中指定)

当Redis需要进行RDB持久化时,redis会创建一个子进程,子进程将内存数据持久化到临时RDB文件,完成后持久化后,它会替代原来的持久化文件

触发持久化的方式有两种,分别时手动触发和自动触发

4.1.1 RDB手动触发持久化

手动触发对应两个指令:save和bgsave

4.1.1.1 save

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

save持久化不会创建子进程

4.1.1.2 bgsave

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

image-20231215170010157

4.1.1.3 LASTSAVE

返回最后一次成功执行的数据库保存的UNIX时间。客户端可以通过读取LASTSAVE值来检查BGSAVE命令是否成功,然后发出BGSAVE命令并以每N秒的固定间隔检查LASTSAVE是否发生变化。在启动时,Redis认为数据库已成功保存。

4.1.2 RDB自动触发持久化

自动触发:根据配置文件save <seconds> <changes>,在<seconds>时间后,存在<changes>次修改未保存,就自动触发一次RDB持久化(网上很多是错的,看英文!指定之间流失,存在指定次数写操作未修改则RDB持久化)自动触发的RDB持久化,也是会fork子进程的

<seconds>内发生<changes>次修改会触发持久化。如果很久前,有<changes>-1次未保存的修改操作,此时再次修改也会触发持久化!即使时间间隔远超<seconds>

4.1.3 RDB自动触发测试

  1. 指定规则:5秒存在2次未保存修改

  2. 修改dump文件保存路径:保存在/myredis/dumpfiles文件夹下

  3. 修改dump文件名称:持久化文件名为dump6379.rdb

  4. 持久化触发:五秒存在两次未保存修改触发持久化

4.1.4 优势与劣势

优势:

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

劣势:

  • 存在数据丢失的可能性,自动触发的RDB持久阿虎在一定间隔时做一次备份,所以如果redis意外down掉的话,就会丢失从当前至最近一次快照期间的数据,快照之间的数据会丢失
  • 内存数据的全量同步,如果数据量太大会导致I/0严重影响服务器性能,RDB依赖于主进程的fork,在更大的数据集中,这可能会导致服务请求的瞬间延迟。
  • RDB持久化期间可能会出现临时的dump.rdb持久化文件

测试

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

所以RDB会丢失当前到最近一次快照期间的数据

4.4.5 检查修复dump.rdb文件

某些情况导致 dump.rdb 文件损坏或不完整,使用redis-check-rdb可能修复dump.rdb文件

image-20231215204945784

4.4.6 快照

在下面的情况会触发RDB快照

  • 配置文件中默认的快照配置,触发持久化生成快照
  • 手动save/bgsave命令
  • 执行flushall/flushdb命令也会产生dump.rdb文件
  • 执行shutdown且没有设置开启AOF持久化主从复制时,主节点自动触发

有些时候需要禁用快照,如何禁用呢?

  • 命令方式:动态所有停止RDB保存规则的方法: redis-cli config set save ""

  • 配置方式:

4.4.7 RDB优化配置

也就是snapshot模块相关配置

  • save <seconds> <changes>

  • dbfilename

  • dir

  • stop-writes-on-bgsave-error

    image-20231215205720861

    建议yes

    在 Redis 配置中,stop-writes-on-bgsave-error 是一个与持久化相关的选项,用于控制在后台保存(BGSAVE)过程中出现错误时是否停止写入操作

    当该选项的值设置为 yes 时,如果在执行后台保存操作时发生错误,Redis 将停止接受写入操作,以防止数据不一致。具体而言,这是因为在 BGSAVE 过程中发生错误可能导致持久化文件(如 dump.rdb)不完整或损坏,为了避免使用不完整的数据进行恢复,Redis 选择暂停写入操作,等待问题得到解决

    当该选项的值设置为 no 时,即使在执行后台保存操作时发生错误,Redis 仍然会接受写入操作。这样可能导致在后续的数据恢复过程中使用不完整或损坏的持久化文件,可能会引入一致性问题

  • rdbcompression

    建议yes

    rdbcompression 是 Redis 的一个配置选项,用于控制在保存快照(RDB 持久化)时是否对数据进行压缩。

    RDB文件压缩减小存储空间,可以降低IO,但保存和加载RDB文件的压缩和解压操作会增减CPU的使用

  • rdbchecksum

    建议yes

    rdbchecksum 是 Redis 的一个配置选项,用于控制在保存快照(RDB 持久化)时是否对数据进行校验和计算。

    启用校验和计算可以提高对持久化文件完整性的信心,确保文件在保存和加载时没有发生损坏或篡改。然而这也会增加一些计算开销

  • rdb-del-sync-files

    image-20231215210524661

    删除在没有启用持久化的实例中用于复制的 RDB 文件。默认情况下,此选项是禁用的,但在某些环境中,由于法规或其他安全问题,需要尽快删除由主服务器在磁盘上持久保存以供给副本使用的 RDB 文件,或由副本在磁盘上存储以用于初始同步的 RDB 文件。请注意,此选项仅在同时禁用 AOF 和 RDB 持久化的实例中起作用,否则将被完全忽略。 另一种(有时更好的)获得相同效果的方法是在主服务器和副本实例上都使用无磁盘复制。但在副本的情况下,无磁盘不始终是一个选项。

4.4.8 总结

image-20231215212121915

4.2 AOF Append Only File

4.2.1 AOF概述

  • AOF(Append Only File),即只允许追加不允许改写的文件

  • redis持久化AOF,即记录 redis 执行过的所有写指令,redis启动加载AOF文件实现数据恢复

  • 通过配置 redis.conf 中的 appendonly yes 就可以打开 AOF 功能(默认没开AOF)。如果有写操作(如 SET 等),redis 就会被追加到 AOF 文件的末尾。

  • 默认写回策略为每秒写回

  • 如果在追加日志时,恰好遇到磁盘空间满、inode 满或断电等情况导致日志写入不完整,也没有关系,redis 提供了 redis-check-aof 工具,可以用来进行日志修复

  • redis 提供了 AOF 文件重写(rewrite)机制解决aof文件过大的问题,即当 AOF 文件的大小超过所设定的阈值时,redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了 100 次 INCR 指令,在 AOF 文件中就要存储 100 条指令,但这明显是很低效的,完全可以把这 100 条指令合并成一条 SET 指令,这就是重写机制的原理

  • AOF文件

    redis6有且只有一个aof文件appendonly.aof

    redis7使用multi part AOF,对应三个文件:分别是基础AOF,增量AOF,历史AOF

    来看看redis copliot怎么说

    image-20240515160933520

    通俗易懂的话来说就是:写指令首先会被记录在base aof中,当aof文件过大,base aof重写策略生成 incr aof,只保留可恢复数据的最小指令集。同时redis会根据base aof和incr aof生成history aof,生成出来的history aof就是最接近完美的aof文件了,此时history aof文件会替换吊base aof和incr aof文件,接着删除history aof文件

4.2.2 AOF工作流程

  1. Client作为命令的来源,向redis服务器发送命令
  2. redis会将写命令先放入AOF缓存中进行保存
  3. 根据同步文件的三种写回策略将AOF缓存区命令写入AOF文件
  4. 随着写入AOF内容的增加为避免文件膨胀,会根据规则进行命令的合并(又称AOF重写,可以理解为一种优化存储策略,保留可以恢复数据的最小指令集),从而起到AOF文件压缩的目的
  5. 当Redis Server 服务器重启的时候会从AOF文件载入数据
三种写回策略
配置项写回时机优点缺点
Always同步写回可靠性高,数据基本不丢失每个写命令都要同步写回,性能影响较大
Everysec每秒写回性能适中宕机丢失1秒内数据
No操作系统控制写回性能好宕机丢失数据可能较多

所谓的写回策略就是决定何时将AOF缓存中的数据写入AOF文件

4.2.3 AOF相关配置

  1. 开启AOF持久化

  2. 使用默认的每秒写回策略

  3. AOF保存路径

    redis6及以前,aof文件的位置和rdb文件的位置一样,都是通过redis.conf配置文件的dir配置

    redis7中,aof文件的位置在dir/appenddirname

    为了便于区分,这里就重新设置了一个dir

4.2.4 AOF重写机制

redis 提供了 AOF 文件重写(rewrite)机制解决aof文件过大的问题,所谓AOF重写,就是**保留可以恢复数据的最小指令集**。

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

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

AOF重写机制触发有两种:自动触发和手动触发

4.2.4.1 自动触发

image-20231217004548649

同时满足两个条件触发AOF重写

  1. 当前AOF文件大小,较上次重写完后AOF文件大小,增长了一倍
  2. AOF文件大小大于auto-aof-rewrite-min-size 64mb

默认配置是当前AOF文件是上次rewrite后大小的一倍,且文件大于64mb

下面测试自动触发重写

  • 开启aof持久化

  • 将触发aof自动重写的大小设置未1k:auto-aof-rewrite-min-size 1k

  • 关闭混合:aof-use-rdb-preamble no

  • 删除全部aof和rdb,重启redis

  • 查看三大配置文件

  • 不断set k1 11111111111111111111111,增量aof文件持续增大,直到触发重写

  • 触发重写:可以看到只留了一个写指令!瘦身计划成功

4.2.4.2 手动触发

使用bgrewriteaof指令

image-20231217010411130

回忆下history aof的工作呢

4.2.4.3 AOF重写底层

AOF重写触发方式就不说了,自动触发或者手动触发,接下来看AOF如何重写

官网是这样说的:来自Redis 持久性 |文档

当AOF变得太大时,Redis能够在后台自动重写AOF。重写是完全安全的,因为当 Redis 重写时会生成一个全新的文件,记录当前数据集所需的最少操作集,一旦第二个文件准备就绪,Redis 就会切换两者(如果失败,原来的aof还在不是吗?)。那么重写期间有新的写指令怎么办

Redis < 7.0 版用内存解决

  • 如果在重写期间对数据库有写入,则 AOF 可以使用大量内存记录这些写入(这些写入在内存中并在最后写入新的 AOF)。
  • 在重写期间到达的所有写入命令都会写入磁盘两次。
  • Redis 可以在重写结束时冻结这些写入命令并将其同步到新的 AOF 文件。

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

当计划 AOF 重写时,Redis 父进程会打开一个新的增量 AOF 文件以继续写入。 子进程执行重写逻辑并生成新的base AOF。 Redis 将使用临时清单文件(temporary manifest file)来跟踪新生成的基础文件和增量文件。 准备就绪后,Redis 将执行原子替换操作(将原来的base AOF和原有增量 AOF生成一个新的base AOF),使此临时清单文件生效。 为了避免在AOF重写反复失败和重试的情况下创建许多增量文件的问题, Redis 引入了 AOF 重写限制机制,以确保失败的 AOF 重写以越来越慢的速率重试。

4.2.5 恢复测试

第一种正常恢复,开启RDB和AOF测试。在aof文件夹下对应三个文件

其中写操作一般追加到增量aof中,读者可以自行测试观察大小

先拷贝RDB文件和AOF文件,FLUSHDB,重启数据库发现Redis恢复数据使用的是AOF文件!

第二种测试有异常恢复

生成种可能会出现写指令写入异常(如突然断电导致指令未写完全),导致AOF文件异常。

这里故意乱写AOF文件,模拟网络闪断文件写error。由于Redis启动会进行AOF文件载入,此时Redis是无法启动的。使用redis-check-aof --fix aof文件名对aof文件修复。再次启动成功。

4.2.6 优势和劣势

  • 优势:更好的保存数据不丢失,性能高,可做紧急恢复

    image-20231217003614597

    image-20231217003654792

  • 劣势:相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb文件。且aof运行效率慢于rdb,每秒同步策略效率好,不同步效率和rdb相同

    image-20231217004002320

4.2.7 总结

image-20231217010802658

4.3 RDB-AOF同时开启和混合持久化

首先明确一点,同时开启AOF和RDB,不等价于开启RDB-AOF混合持久化!!混合模式是开启AOF和RDB的同时,通过配置aof-use-rdb-preamble的值为 yes,开启混合持久化

这两个模式,aof文件也是有区别的!后面会分析

4.3.1 Redis启动加载谁?

根据网上的资料(本人推测是redis7以前的说法),开同时开启AOF和RDB时,redis重启的时候只会载入AOF文件来恢复原始的数据,因为通常情况下AOF文件保存的数据要比RDB文件保存的数据集完整,如下过程

下面测试,无论是AOF+RDB同时开启,还是混合模式,Redis都不会加载RDB文件,只认AOF文件

在只开启RDB文件持久化时,启动时加载RDB文件毋庸置疑。

此时在配置文件中开启aof测试appendonly yes,发现

redis数据库为空,也就是redis根本就不会加载对应的RDB文件!!只认识AOF文件!

  • RDB-AOF混合持久化

通过配置aof-use-rdb-preamble的值为 yes,开启混合持久化

4.3.2 AOF文件分析

错误说法?

根据网上的说法(推测是在redis7以前):

在开启混合持久化的情况下,AOF 重写时会把 Redis 的持久化数据,以 RDB 的格式写入到 AOF 文件的开头,之后的数据再以 AOF 的格式化追加的文件的末尾,这样既保证了数据完整性,又提高了恢复数据的性能

redis7后aof文件为三部分组成,对应增量aof,基础aof,清单aof

首先在同时开启aof和rdb时(使用全新Redis数据库)

[root@Answer-Linux ~]# redis-cli -a redis2296
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set key1 lin
OK
127.0.0.1:6379> set key2 zhuo
OK
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started

这里加入两个数据,并手动触发重写,此时,增量aof文件是全新的,没必要看了,重写aof内容在基础aof文件中(不懂可以回看AOF重写机制),下面查看基础的AOF

现在开启RDB-AOF混合模式,重新测试该过程:

[root@Answer-Linux ~]# redis-server /myRedis/redis7.conf 
[root@Answer-Linux ~]# redis-cli -a redis2296
Warning: Using a password with '-a' or '-u' option on the commande interface may not be safe.
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set key1 linzhuowei
OK
127.0.0.1:6379> set key2 linjiayan
OK
127.0.0.1:6379> BGREWRITEAOF
Background append only file rewriting started

还是加入两个数据,手动触发重写,查看baseaof文件

image-20231217150125559

可以看到存储的方式截然不同,同时开启AOF和RDB时,重写只是简单的,将增量aof文件迁移到基础的aof文件,而混合模式下,无论是迁移方式还是基础aof文件的存储方式都截然不同。本人推测基础aof文件头多出的那部分对应的就是之前版本里面的RDB头(只是推测,因为网上现在还很少redis7相关的介绍,并且官网也很少关于混合模式的讲解)

但总之,redis7建议使用混合模式(默认开启)

4.3.3 纯缓存模式

有时为了充分利用 Redis 高效的内存存储和快速读写特性,提升系统的性能和响应速度,如高并发的情况下,需要使用纯缓存模式

  • 禁用自动RDB持久化

    配置文件中save ""

    此时依然可以通过save,bgsave来进行持久化

  • 禁用aof

    appendonly no

    此时我们依然可以使用命令bgrewriteaof生成aof文件

5 Redis事务

Redis事务(Transaction)是一种将多个命令打包成一个原子操作的机制,事务中命令顺序执行

Redis事务并非严格意义上的事务,只是用于帮助用户在一个步骤中执行多个命令,可以理解为就是一个命令打包并顺序执行的功能

Redis事务对比数据库事务

  1. 事务独占执行:Redis的事务仅仅是保证事务里的操作会被连续独占的执行,redis命令执行是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的
  2. 没有隔离级别:因为事务完成前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这种事务隔离级别的问题了
  3. 不保证原子性:Redis不支持事务回滚,也就不能像数据库事务一样,事务内命令要么全部成功,要么全部失败
  4. 排它性:Redis会保证一个事务内的命令依次执行,而不会被其它命令插入

5.1 常用命令

  1. DISCARD:取消事务,放弃执行事务块内所有命令
  2. EXEC:执行事务
  3. MULTI:开启事务
  4. UNWATCH:取消WATCH命令对所有key的监视
  5. WATCH key[key...]:监视一个或者多个key,如果在事务执行前,监视的key被其他命令有所改动,那么事务将会被打断,返回nil

5.2 Redis事务测试

MULTI开始事务,接着命令入队,等待EXEC提交执行事务

  1. 正常执行:MULTI+EXEC

  2. 放弃事务:MULTI+DISCARD

  3. 全体连坐:事务提交执行前出错,也就是命令加入事务队列过程中出错,事务无法执行

  4. 冤头债主:事务提交执行后出错,对的执行错的停(不保证原子性)

  5. WATCH监控:Redis使用WATCH提供乐观锁。

    • 悲观锁:访问共享资源之前,先获取锁,并在访问完成后释放锁。整个访问过程中,都持有锁,以防止其他线程或进程的干扰
    • 乐观锁:访问共享资源之前,不需要一直持有锁,而是在更新之前检查数据是否被其他线程修改过。如果检测到冲突,可以采取相应的措施(停止执行)

    事务执行前,监控key被修改,事务整体拒绝执行(whole transaction aborts)

  6. UNWATCH就是取消监控

    事务一旦执行exec,提交执行,之前加的监控锁都会被取消

在关系型数据库中,事务具有原子性,要么全部成功要么全部失败。在Redis中不同,事务可能全部成功,也可能部分成功(事务提交后命令出错),也可能全部失败(监控key在事务提交前被修改)

redis事务就是将一系列命令打包成集合,让这些命令顺序执行,redis事务不同于数据库,redis采用单线程,事务独占执行,在事务完成前不执行其他请求,自然就不需要什么隔离级别了。且redis事务不支持原子性,采取冤有头债有主的策略,允许部分成功部分失败

6 Redis管道

Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。

一个请求会遵循以下步骤:

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

  2. 命令由客户端发送给Redis服务器,Redis服务器将结果返回客户端,这两个过程组成Round Trip Time(简称RTT,数据包往返时长)

    image-20231217232702054

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

管道Pipline,允许客户端将多个命令一次发送给服务器,并在一次通信中获取多个命令的执行结果。显著提高 Redis 的性能和吞吐量,减少通信开销RTT

pipline的使用:将命令放在文件中,交给redis执行即可

Pipline与原生批量命令对比

  • 原生批量命令是原子性(如mset)pipline是非原子性,pipline文件中指令顺序执行,如果发生指令异常,将会继续执行后续指令
  • 原生命令一次只能执行一种命令,pipline支持批量执行不同命令
  • 原生批命令是服务端实现,而pipeline需要服务端与客户端共同完成

Pipline和事务对比

  • 事务和管道都不具有原子性
  • 管道已从将多条命令发送到服务器,解决RTT(数据包往返时间)的问题,而事务中的命令是一条一条的发给redis服务器,并在接收到exec后执行
  • 事务执行,命令独占redis,阻塞其他命令的执行,而管道不会阻塞

pipline和事务都是将命令打包执行,但他们的目的和作用不尽相同。事务其实就是一个命令打包并顺序执行的功能,而pipline是为了解决RTT数据包往返时间的问题问题

7 Redis发布订阅

这部分了解即可,实际生产中毫无用武之地。

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

Redis只一种消息通信模式:发布者PUBLISH往频道发送消息,订阅者SUBSCRIBE从频道接收消息,可以实现进程之间的消息传递。

如图频道channel1,有三个客户端订阅该频道

接下来往channel1发布消息

7.1 常用命令

  1. SUBSCRIBE chanenl [channel ...]:订阅一个或者多个频道信息

    推荐先订阅后发布,否则订阅前端鹅消息是收不到的

    客户端收到发布者消息时有三个部分:消息种类,来源频道,消息内容

  2. PUBLISH channel message:发布消息到指定的频道

  3. PSUBCRIBE pattern [pattern ...]:按照模式批量订阅,订阅一个或者多个符合给定模式的频道

  4. PUBSUB subcommand [argument [argument ....]]:查看订阅与发布系统状态

    PUBSUB CHANNELS:查看活跃频道组成的列表

    PUBSUB NUMSUB [chanenl [channel ...]]:某个频道有几个订阅者

    PUBSUB NUMPAT:只统计使用PSUBSRIBE命令执行的,返回客户端定于的唯一模式的数量

  5. UNSUBSCRIBE [chanenl [channel ...]]:取消订阅

  6. PUNSUBSCRIBE [pattern [pattern ...]]:退定所有给定模式的频道

7.2 案例演示

  • 开启3个客户端,演示客户端A、B订阅消息,客户端C发布消息

  • 批量订阅和发布

  • 取消订阅

7.3 缺点

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

8 Redis复制

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

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

  • Master主机可读可写,从机只能读
  • Slave从机启动后,首次数据同步会从头同步,随后跟随Master主机追加同步
  • Master主机shutdown后,从机原地待命,从机可读
  • Master主机shutdown重启后,主从关系依旧

8.1 前置

记住一句话,配从(库)不配主(库)

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

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

8.2 常用命令

  • replicaof 主库IP 主库端口:写在redis.conf配置文件内,为从机指定主机
  • info replication:redis中运行命令,可以查看当前Redis的主从关系和配置信息
  • slaveof 主库IP 主库端口:redis中运行命令,设置主机
    • 每次与master断开之后,都需要重新连接,除非你配置进redis.conf文件
    • 在运行期间修改slave节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系转而和新的主数据库同步,重新拜码头
  • slaveof no one:redis中运行命令,使当前数据库停止与其他数据库的同步,转为主数据库,自立为王

8.3 主从配置

一个Master两个Slave,对应三台虚拟机(这里如果式克隆产生的虚拟机,注意配置IP地址并生成MAC地址)。测试三台虚拟机是否能ping通

本人三台虚拟机:

  • 192.168.145.130 端口6379
  • 192.168.145.140 端口6380
  • 192.168.145.150 端口6381

三台主机对应三个配置文件:redis6379.conf、redis6380.conf、redis6381.conf

  1. 关闭防火墙!!!!

  2. 开启daemonize yes

    redis以守护进程的方式运行

  3. 注释掉bind 127.0.0.1

  4. protected-mode no

  5. 指定端口

    Master端口6379,Slave1端口6380,Slave2端口6381

  6. 指定当前工作目录dir

  7. pid文件名字,pidfile

    从机pid文件名称对应端口号

  8. log文件名字,logfile

    从机log名称对应端口号

  9. requirepass

  10. dump.rdb名字

  1. aof文件,appendfilename

  2. 此步配从不配主:从机需要配置,主机不需要配置。从机访问主机的通行密码masterauth

    从机配置:

至此配置好了主从关系

8.4 主从复制三板斧

8.4.1 一主二从

方式一:配置文件方式

从机使用replicaof 主库IP 主库端口:写入redis.conf配置文件内,指定主机。

Redis主从复制,主机内存数据同步从机

image-20231218233137747

可以使用info replication命令查看当前redis主从配置情况

方式二:命令操作方式

使用slaveof 主库IP 主库端口,为当前redis服务器指定Master主机。从机重启后,主从关系失效。主机重启后,主从关系依旧

8.4.2 薪火相传

如果一个主机同时有多个从机,那么对于主机压力还是很大的。因此有了薪火相传的用法:Slave从机可以是另一个Slave从机的主机!

slaveof 主库IP 主库端口指定从机即可

重新指定后,会清除之前的数据,重新建立拷贝最新的数据(总之就是主从同步数据

8.4.3 自立为王

Slaveof no one:当前Redis数据库称为Master主机

8.4.4 查看日志

新人配置主从失败,不知道错误在哪里?可以通过查看日志文件,查看warning或者error警告,而这里展示配置成功日志文件

Master日志:

Slave日志

8.5 主从复制流程

slave启动,主从同步:

  • slave启动成功连接到master后会发送一个sync请求同步命令(日志可见哦)
  • slave首次全新连接master,一次完全同步(全量复制)将被自动执行,slave自身原有数据会被master数据覆盖清除

首次连接,全量复制:

  • master节点收到sync命令后会开始在后台保存快照(即RDB持久化,主从复制时会触发RDB).同时将所有接收到的用于修改数据集命令缓存起来,master节点执行RDB持久化完后masterrdb快照文件和所有缓存的命令发送到所有slave,以完成一次完全同步。

  • 而slave服务在接收到数据库文件数据后,将其存盘并加载到内存中,从而完成复制初始化

心跳持续,保持通信:

  • repl-ping-replica-period 10,默认10s一次,由主机发送信号,确认从机在不?

从机待命,增量复制:

  • Master继续将新收集到的修改命令自动依次传给slave,完成同步

从机下线,断点续传

  • master会检查backlog里面的offsetmasterslave都会保存一个复制的offset还有一个masterId.offset是保存在backlog中的。Master只会把已经复制的offset后面的数据复制给Slave,类似断点续传

8.6 缺点

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

甚至master挂了,所有从机原地待命,并不会自动重选一个master,每次都要人工干预选主机?是否可以无人值守?

9 Redis哨兵

9.1 哨兵概述

Redis SentinelRedis提供了高可用(High availablity)解决方案,实际上这意味着使用Sentinel可以部署一套Redis,再没有认为干预的情况下处理各种各样的失败事件

Redis哨兵(Sentinel)用于监控主机和从机的状态,并在主机失效时自动将从机提升为新的主机。俗称无人值守运维

下面是Sentinel的功能列表:

  • 监控(Monitoring):Sentinel不断的去检查你的主从实例是否按照预期在工作。心跳机制
  • 通知(Notification):Sentinel可以通过一个api来通知系统管理员或者另外的应用程序,被监控的Redis实例有一些问题。
  • 自动故障转移(Automatic failover):主节点若故障,Sentinel会做故障转移工作,把一个从节点提升为主节点,并重新配置其他的从节点使用新的主节点。
  • 配置提供者(Configuration provider):Sentinel给客户端的服务发现提供来源:对于一个给定的服务,客户端连接到Sentinels来寻找当前主节点的地址。当故障转移发生的时候,Sentinels将报告新的地址。

Sentinel的分布式特性

Redis Sentinel是一个分布式系统,Sentinel运行在有许多Sentinel进程互相合作的环境下,它本身就是这样被设计的。有许多Sentinel进程互相合作的优点如下:

  1. 当多个Sentinel同意一个master不再可用的时候,就执行故障检测。这明显降低了错误概率。
  2. 即使并非全部的Sentinel都在工作,Sentinel也可以正常工作,这种特性,让系统非常的健康。

所有的Sentinels,Redis实例,连接到Sentinel和Redis的客户端,本身就是一个有着特殊性质的大型分布式系统。

9.2 Redis哨兵实战

9.2.1 Redis Sentinel架构

笔记中哨兵机制架构:

  • 三个哨兵:自动监控和维护集群,不存放数据,只是吹哨人
  • 一主二从:用于数据读取和存放

9.2.2 sentinel.conf配置文件

默认的sentinel.conf文件在/opt目录下

我们需要用sentinel.conf配置文件来配置哨兵。

哨兵sentinel.conf文件基本配置如下:

#sentienl26379.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 26379
logfile "/myRedis/sentinel26379.log"
pidfile /var/run/redis-sentinel26379.pid
dir /myRedis
sentinel monitor mymaster 192.168.145.130 6379 2
sentinel auth-pass mymaster 111111
  • bind:服务监听地址,此处设定监听任意地址
  • daemonize:是否以后台daemon方式运行,此处设定Redis Sentinel在后台作为守护进程运行
  • port:端口,配置Redis Sentinel监听端口26379
  • logfile:日志文件,Sentienl日志文件存放路径
  • pidfile:配置进程ID文件路径和名称
  • dir:工作目录,配置Redis Sentinel使用的工作目录
  • sentinel monitor <master-name> <ip> <redis-port> <quorum>:告诉Redis Sentinel监控名为<master-name>的Redis的主节点,该主节点的IP地址为<ip>,端口为<redis-port>quorum对应投票数,指最少有几个哨兵认可客观下线
  • sentinel auth-pass <master-name> <password>:设置连接名为<master-name>的Redis的主节点的密码<password>

9.2.3 案例演示

9.2.3.1 sentinel.conf配置

由于本人16g内存,三个虚拟机已经跑满内存了,再给哨兵加主机电脑就要爆炸了,所以哨兵案例规划如下:

  • 192.168.145.130 6379:Master主机,三个哨兵
  • 192.168.145.140 6380:Slave从机
  • 192.168.145.150 6381:Slave从机

主从关系上一小节配置过,这里关注三个哨兵的配置文件:

#sentienl26379.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 26379
logfile "/myRedis/sentinel26379.log"
pidfile /var/run/redis-sentinel26379.pid
dir /myRedis
sentinel monitor mymaster 192.168.145.130 6379 2
sentinel auth-pass mymaster 111111

#sentienl26380.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 26380
logfile "/myRedis/sentinel26380.log"
pidfile /var/run/redis-sentinel26380.pid
dir /myRedis
sentinel monitor mymaster 192.168.145.130 6379 2
sentinel auth-pass mymaster 111111

#sentienl26381.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 26381
logfile "/myRedis/sentinel26381.log"
pidfile /var/run/redis-sentinel26379.pid
dir /myRedis
sentinel monitor mymaster 192.168.145.130 6379 2
sentinel auth-pass mymaster 111111

在master主机上配置三个sentinel.conf

Attention!由于之前的Redis主从复制Master主机确定,就不需要配置访问新主机的密码。但是在哨兵中6379后续可能会变成从机因此需要配置访问新主机的密码,否则后续可能出问题!

在本案例中就是192.168.145.130主机需要配置

9.2.3.2 哨兵威力
redis-sentinel sentinel26379.conf --sentinel
redis-sentinel sentinel26380.conf --sentinel
redis-sentinel sentinel26381.conf --sentinel

先启动一主二从,接着启动三个哨兵,查看哨兵log日志文件:

image-20231220162944739

哨兵log日志文件中记录了master和slave主机的信息,哨兵自身ID和其余两个哨兵的ID(这里查看的是sentinel26379.log,另外两个哨兵log文件类似)

再次查看主从关系info replication,端口6379主机,端口6380从机,端口6381从机。符合预期

接下来shutdown端口6379主机Master,模拟主机嘎了。哨兵会经过激烈的投票,选出新的主机。

通过info replication发现6380端口Slave上位!黄袍加身

重启端口6379的Redis服务器:江山易主,哥们变从机了

再次查看哨兵日志文件,记录了6379master主机down,到选举出新的主机6380的过程

此时再次shutdown主机6380,一样也会重新选举出主机。但需要提醒等待哨兵完成选举过程需要耐心

选择主机期间,可能会出现以下错误,这些错误等一会就行,问题不大的别慌

Error:Server closed the connection
或者
Error:Broken pipe
# 往对端已关闭的的管道里写数据的情况下

image-20231220000234114

在运行期间,redis配置文件会被sentinel动态更改,哨兵机制运行下,主从关系发生变化,master-redis.confslave-redis.confslave-redis.conf的内容都会发生改变,即master-redis.conf中会多一行slaveof的配置,sentinel.conf的监控目标也会随之调换

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

当一个主从配置中的master失效之后,sentinel可以选举出一个新的master,用于自动接替原master的工作,主从配置中的其他redis服务器自动指向新的master同步数据

哨兵运行流程:哨兵的故障迁移failover操作由sentinel独自完成(无人值守运维)

主观下线—客观下线—

步骤一:SDOWN:主观下线(Subjectively Down)

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

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

sentinel down-after-milliseconds <masterName> <timeout>

步骤二:ODOWN:客观下线(Objectively Down)

需要指定数量的sentinel达成一致意见才能认为master客观下线

在配置文件中

masterName是对某个master+slave组合的一个区分标识(一套sentinel可以监听多组master+slave这样的组合)

quorum这个参数是进行客观下线的一个依据,译为法定人数或者法定票数

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

步骤三:选举领导者哨兵(哨兵中选出兵王)

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

如何选出兵王?Raft算法,简单说是先到先得

步骤四:兵王负责故障迁移

  • 新王登基:选出新的Master

    图解

    1. redis.conf文件中,优先级slave-priority或者replica-priority最高的从节点(数字越小优先级越高)
    2. 复制偏移量位置offset最大的从节点
    3. 最小Run ID的从节点(根据ASCll码比较)
  • 群臣俯首

    Sentinel leader对选举出来的新master执行slaveof no one命令,将其提升为master节点。Sentinel leader通过slaveof 主库IP 主库端口命令让其他节点成为其从节点

  • 旧主拜服

    Sentinel leader将老master主机设置为新master的从节点,当老master重新上线后,降级为Slave并恢复正常工作

9.4 哨兵使用建议

  • 哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用
  • 哨兵节点的数量应该是奇数
  • 各个哨兵节点的配置应一致
  • 如果哨兵节点部署在Docker等容器里面,尤其要注意端口的正确映射
  • 哨兵集群+主从复制,并不能保证数据零丢失-承上启下引出集群

10 集群

Redis集群(Redis Cluster)是Redis提供的一种分布式存储解决方案,旨在实现数据的自动分片和高可用性。它通过将数据分布在多个节点上来实现扩展性和故障恢复。

  1. 数据分片:Redis集群将数据分成多个分片(shards),每个分片由一个或多个Redis节点(node)负责。数据分片基于哈希槽(hash slot)进行,每个Redis集群有16384个哈希槽。
  2. 高可用性:Redis集群通过主从复制实现高可用性。每个分片有一个主节点(master)和一个或多个从节点(slave)。主节点负责处理读写请求,从节点则用于故障转移和读取负载均衡。如果主节点发生故障,集群会自动选举一个从节点成为新的主节点。
  3. 自动故障恢复:Redis集群具有自动故障检测和恢复功能。当某个节点发生故障时,集群中的其他节点会感知到,并进行故障恢复操作,例如将从节点提升为主节点,以确保集群继续运行(内置sentinel机制)
  4. 线性扩展:Redis集群支持动态扩展,可以在线添加或删除节点而不会中断服务。这使得Redis集群能够根据需要灵活调整容量。

典型使用场景

  • 大规模缓存:适用于需要大规模缓存数据的应用,如电商网站、社交媒体平台等。
  • 高并发读写:适用于需要处理高并发读写请求的应用,如实时分析系统、在线游戏等。
  • 分布式存储:适用于需要分布式存储和管理大量数据的应用,如大数据处理、日志收集等。

结论

Redis集群是一种强大的分布式解决方案,通过数据分片、主从复制和自动故障恢复,提供了高性能、高可用性的Redis服务。尽管其配置和管理相对复杂,但对于需要大规模数据存储和高并发处理的应用来说,Redis集群是一个非常有效的选择。

  • 客户端与Redis的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可
  • 槽位slot负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系

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

10.1 Redis集群槽位与分片

10.1.1 槽位slot

Redis集群没有使用一致性hash,而是引入了哈希槽的概念

Redis集群有16384个哈希槽,数据集的**每个key通过CRC16校验后对16384取模**来决定数据存储在哪个槽,集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么

10.1.2 分片

Redis集群将数据分成多个分片,每个分片由一个Redis节点负责。简言之,集群中的Redis实例的数据集被认为是整个数据的一个分片。

那么如何找到分片呢?对key进行CRC16(key)算法处理并通过对总分片数量取模确定存储的槽位,

10.1.3 槽位和分片的优势

最大优势,方便扩缩容和数据分派查找

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

10.1.4 slot槽位映射

10.1.4.1 哈希取余分区

2亿条记录就是2亿个k,v,我们单机不行必须要分布式多机,假设有3台机器构成一个集群,用户每次读写操作都是根据公式:

hash(key) % N个机器台数,计算出哈希值,用来决定数据映射到哪一个节点上。

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

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

    某个redis机器宕机了,由于台数数量变化,会导致hash取余全部数据重新洗牌。

哈希取余分区方式的slot槽位映射扩缩容困难,因此有了一致性哈希算法分区

10.1.4.2 一致性哈希算法分区

一致性哈希算法分区设计是为了解决分布式缓存数据变动和映射问题。当服务器个数发生变化时,尽量减少客户端到服务器的映射关系(其实就是哈希求模的模数从redis服务器台数(变)到232(不变),从变到不变解决了解决分布式缓存数据变动和映射问题

步骤一:算法构建一致性哈希环

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

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

步骤二:Redis服务器IP节点映射

以Redis服务器IP或者主机名等作为关键字,计算Redis服务器的哈希值,将Redis服务器映射到哈希环上。例如假如4个节点NodeA、B、C、D,经过IP地址的哈希函数计算(hash(ip)),使用IP地址哈希后在环空间的位置如下:

步骤三:key落到服务器的落键规则

需要存储kv键值对时,计算key哈希值获得key在哈希环上的位置,顺时针行走遇到的第一台服务器就是该键值对应该定位到的服务器,将该键值对存储在该节点上

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

  • 优点:

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

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

  • 缺点:一致性哈希在节点少时容易因为节点分布不均而造成数据倾斜

一致性哈希算法为了在节点数目发生改变时尽可能少的迁移数据

将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash后会顺时针找到临近的存储节点存放。

加入和删除节点只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响。

但由于数据的分布和节点的位置有关,因为节点并非均匀的分布在哈希环上,所以数据在进行存储时达不到均匀分布的效果。甚至在节点少时发生数据倾斜

概述:将hash值空间组成一致性哈希环,将集群的redis实例映射到一致性哈希环中,接下来存储的数据也映射到一致性哈希环中,并存储在顺时针第一个redis实例中

10.1.4.3 哈希槽分区

为了解决一致性哈希算法的数据倾斜问题,诞生了哈希槽

  • 哈希槽实质就是一个数组,数组 [0,214 -1] 形成hash slot空间。
  • Redis 集群中内置了 16384 个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。(分配策略没有要求)。
  • 集群会记录节点和槽的对应关系,解决了节点和槽的关系后。接下来就需要对key求哈希值,然后对16384取模,根据余数落入对应编号的槽里。HASH_SLOT = CRC16(key) mod 16384。以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了。
  • 解决均匀分配的问题,在数据和节点之间加入了一层哈希槽(slot),用于管理数据和节点之间的关系,现在就相当于节点里放的是槽,槽里放的是数据

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

10.1.5 为什么redis集群的最大槽数为16384?

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

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

https://github.com/redis/redis/issues/2576

正常的心跳包携带了一个节点的完整配置,可以通过幂等方式替换为旧配置,从而更新旧配置。这意味着它们包含了节点的槽位配置,以原始形式表示,对于16,000个槽位来说,需要2k的空间,但对于65,000个槽位来说,需要使用的8k空间。

与此同时,由于其他设计权衡的原因,Redis Cluster不太可能扩展到超过1000个主节点。因此在最大情况1000个主节点下,选择16,000个槽位是为了确保每个主节点有足够的槽位,但又足够小,以至于能将槽位配置信息以原始位图的形式轻松传播。请注意,在小型集群中位图可能难以压缩,因为当N较小时,位图 将设置的slot / N位 占设置位的很大百分比。

深度解析:

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

    • 在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb
    • 在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为16384时,这块的大小是: 16384÷8÷1024=2kb

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

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

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

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

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

总结:65536对应产生的心跳包8k过大,且对于不超过1000个节点来说16384个槽位绝对够用了,并且在槽位小,节点少的情况下,压缩比高容易传输。

10.2 集群案例

10.2.1 3主3从redis集群配置

在3台真实的虚拟机中,各自新建mkdir -p /myRedis/cluster,我们在该目录下写配置文件

下面时六个配置文件,对应三台主机

192.168.145.130+端口6381/端口6382

vim /myRedis/cluster/redisCluster6381.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 6381
logfile "/myRedis/cluster/cluster6381.log"
pidfile /myRedis/cluster6381.pid
dir /myRedis/cluster
dbfilename dump6381.rdb
appendonly yes
appendfilename "appendonly6381.aof"
requirepass 111111
masterauth 111111
 
cluster-enabled yes
cluster-config-file nodes-6381.conf
cluster-node-timeout 5000
vim /myRedis/cluster/redisCluster6382.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 6382
logfile "/myRedis/cluster/cluster6382.log"
pidfile /myRedis/cluster6382.pid
dir /myRedis/cluster
dbfilename dump6382.rdb
appendonly yes
appendfilename "appendonly6382.aof"
requirepass 111111
masterauth 111111
 
cluster-enabled yes
cluster-config-file nodes-6382.conf
cluster-node-timeout 5000

192.168.145.140+端口6383/端口6384

vim /myRedis/cluster/redisCluster6383.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 6383
logfile "/myRedis/cluster/cluster6383.log"
pidfile /myRedis/cluster6383.pid
dir /myRedis/cluster
dbfilename dump6383.rdb
appendonly yes
appendfilename "appendonly6383.aof"
requirepass 111111
masterauth 111111

cluster-enabled yes
cluster-config-file nodes-6383.conf
cluster-node-timeout 5000
vim /myRedis/cluster/redisCluster6384.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 6384
logfile "/myRedis/cluster/cluster6384.log"
pidfile /myRedis/cluster6384.pid
dir /myRedis/cluster
dbfilename dump6384.rdb
appendonly yes
appendfilename "appendonly6384.aof"
requirepass 111111
masterauth 111111
 
cluster-enabled yes
cluster-config-file nodes-6384.conf
cluster-node-timeout 5000

192.168.145.150+端口6385/端口6386

vim /myRedis/cluster/redisCluster6385.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 6385
logfile "/myRedis/cluster/cluster6385.log"
pidfile /myRedis/cluster6385.pid
dir /myRedis/cluster
dbfilename dump6385.rdb
appendonly yes
appendfilename "appendonly6385.aof"
requirepass 111111
masterauth 111111
 
cluster-enabled yes
cluster-config-file nodes-6385.conf
cluster-node-timeout 5000
vim /myRedis/cluster/redisCluster6386.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 6386
logfile "/myRedis/cluster/cluster6386.log"
pidfile /myRedis/cluster6386.pid
dir /myRedis/cluster
dbfilename dump6386.rdb
appendonly yes
appendfilename "appendonly6386.aof"
requirepass 111111
masterauth 111111
 
cluster-enabled yes
cluster-config-file nodes-6386.conf
cluster-node-timeout 5000

启动6台Redis机器(分别在三台VM上)

redis-server /myRedis/cluster/redisCluster6381.conf

redis-server /myRedis/cluster/redisCluster6382.conf

redis-server /myRedis/cluster/redisCluster6383.conf

redis-server /myRedis/cluster/redisCluster6384.conf

redis-server /myRedis/cluster/redisCluster6385.conf

redis-server /myRedis/cluster/redisCluster6386.conf

现在有六台redis服务器以集群方式运行

image-20231222233820816

下面配置他们的主从关系

10.2.2 redis-cli构建集群关系

//注意自己的IP地址
redis-cli -a 111111 --cluster create --cluster-replicas 1 192.168.145.130:6381 192.168.145.130:6382 192.168.145.140:6383 192.168.145.140:6384 192.168.145.150:6385 192.168.145.150:6386
//--cluster-replicas 1 表示为每个master创建一个slave节点
//其中3个实例会被设为主节点(master),另外3个实例会作为从节点(replica)分配给这3个主节点。具体的角色分配由Redis自动完成。

上述输出包含信息:三个master主机哈希槽分配情况、六台主机的主从关系及它们Redis run ID的对应情况

但主从关系以实际分配为准,比如6382本来时6381的从机,但实际分配时并非如此。也就是说主机指定,从机分配

10.2.3 常用命令

以6381未切入点,连接redis,查看集群状态

info replication:查看主从关系

cluster info:获取关于集群状态的信息,获取有关节点、分区、槽分配等方面的详细信息

cluster nodes:获取有关 Redis 集群中所有节点的详细信息

cluster keyslot k1:查看某个key的槽位值

10.2.4 集群读写

在master主机6381上写入键值对

127.0.0.1:6381> set k1 v1
(error) MOVED 12706 192.168.145.150:6385

报错原因:该键值对槽位未12706,并非6381主机的范围,而是6385主机范围(槽位和主机对应关系在构建集群关系时有展示)

解决方式:连接redis时添加参数c。

-c选项指示Redis客户端在执行命令时,自动根据命令中的Key来选择正确的槽(slot)并将请求发送到负责该槽的节点。

redirected也就是说重定向到对应的节点了

我们可以通过cluster keyslot k1:查看某个key的槽位值,如k1:

10.2.5 容错切换故障迁移

前面说过集群内置哨兵机制,也就是说,主机故障,从机自动上位,主机回复以从机运行,江山易主。

并且值得注意:Redis集群不保证强一致性,这意味着在特定的条件下,Redis集群可能会丢掉一些被系统收到的写入请求命令(可能写命令写入主机,在未同步从机前,主机故障,此时从机上位,该写命令就丢失了)

在redis集群中我们还可以手动触发故障转移CLUSTER FAILOVER 命令是用于手动触发故障转移的指令。也就是让从机上位

10.2.6 主从扩容案例

前面有了三主三从的集群,现在希望加入一主一从,形成四主四从的集群

步骤一:创建一主一从两个Redis服务器:

vim /myRedis/cluster/redisCluster6387.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 6387
logfile "/myRedis/cluster/cluster6387.log"
pidfile /myRedis/cluster6387.pid
dir /myRedis/cluster
dbfilename dump6387.rdb
appendonly yes
appendfilename "appendonly6387.aof"
requirepass 111111
masterauth 111111

cluster-enabled yes
cluster-config-file nodes-6387.conf
cluster-node-timeout 5000
vim /myRedis/cluster/redisCluster6388.conf
bind 0.0.0.0
daemonize yes
protected-mode no
port 6388
logfile "/myRedis/cluster/cluster6388.log"
pidfile /myRedis/cluster6388.pid
dir /myRedis/cluster
dbfilename dump6388.rdb
appendonly yes
appendfilename "appendonly6388.aof"
requirepass 111111
masterauth 111111
 
cluster-enabled yes
cluster-config-file nodes-6388.conf
cluster-node-timeout 5000

步骤二:启动6387/6388两个节点实例,此时均为master

redis-server /myRedis/cluster/redisCluster6387.conf

redis-server /myRedis/cluster/redisCluster6388.conf

步骤三:6387节点以master加入原集群

将新增的6387作为master节点加入原有集群

redis-cli -a 密码 --cluster add-node 新增节点IP:端口 原集群一个masterIP:端口

此处新增节点为192.168.145.150:6387

而原集群的master:

这里选择192.168.145.130:6381作为引路人

redis-cli -a 111111 --cluster add-node 192.168.145.150:6387 192.168.145.130:6381

步骤四:检查集群情况

redis-cli -a 密码 --cluster check 真实ip地址:6381

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

步骤五:重新分派槽号

重新分派槽号命令:redis-cli -a 密码 --cluster reshard 节点IP:节点端口

redis-cli -a 111111 --cluster reshard 192.168.111.1505:6387

新增节点哈希槽我们就采取16384/4,而新增节点的ID可以通过cluster nodes查看,或者redis-cli -a 密码 --cluster check 真实ip地址:6381

步骤六:再次检查集群情况

redis-cli -a 密码 --cluster check 节点IP:节点端口

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

可以看到新增节点的哈希槽并非连续,可见分配所得哈希槽分别来自之前的三个master主机(每个人匀一些)

步骤七:为master主节点分配Slave从节点

命令:redis-cli -a 密码 --cluster add-node slaveIP:slave端口 主节点IP:主节点端口 --cluster-slave --cluster-master-id 主节点ID

这里我们要给6387主节点分配slave从节点

主节点ID如图:

redis-cli -a 111111 --cluster add-node 192.168.145.150:6388 192.168.145.150:6387 --cluster-slave --cluster-master-id 7b2f64ab1caa050640e6b2342d74b62995de2838

步骤八:再次检查集群情况

redis-cli -a 密码 --cluster check 节点ip地址:节点端口

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

分配成功!

10.2.7 主从缩容案例

前面扩容案例,添加6387和6388一主一从节点,缩容就是让他们下线

步骤一:检查集群情况,获得6388从节点ID

118f86b07edab0b79035e933b4c713445e43bea8

步骤二:删除从节点6388

命令:redis-cli -a 密码 --cluster del-node 节点IP:节点端口 节点ID

redis-cli -a 111111 --cluster del-node 192.168.145.150:6388 118f86b07edab0b79035e933b4c713445e43bea8

步骤三:检查集群情况

redis-cli -a 密码 --cluster check 真实ip地址:6381

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

步骤四:重新分派槽号

其实就是将6387主机哈希槽归还集群

重新分派槽号命令:redis-cli -a 密码 --cluster reshard 节点IP:节点端口

redis-cli -a 111111 --cluster reshard 192.168.145.150:6387

这里就将6387节点的哈希槽一次性分配给6381节点(不然输入三次,很麻烦)

步骤五:检查集群情况

redis-cli -a 密码 --cluster check 真实ip地址:6381

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

6387节点从原来master主节点,将哈希槽给了6381节点后,变成6381节点的从节点

步骤六:删除6387从节点

命令:redis-cli -a 密码 --cluster del-node 节点ip:节点端口 节点ID

redis-cli -a 111111 --cluster del-node 192.168.145.150:6387 7b2f64ab1caa050640e6b2342d74b62995de2838

步骤七:检查集群情况

redis-cli -a 密码 --cluster check 真实ip地址:6381

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

image-20231223010849963

10.3 集群相关

10.3.1 不同slot槽位的批操作

不在同一个slot槽位下的键值无法使用mset、mget等多键操作

可以通过{}来定义同一个组的概念,使key中{}内相同内容的键值对放到一个slot槽位去,对照下图类似k1、k2、k3都映射为x,自然槽位一样(这里是他人说法,但是明显不是同一个槽位,但网上找不到相关用法mdb)

10.3.2 常用命令

  • cluster-require-full-coverage

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

    cluster-require-full-coverage: 默认值 yes , 即需要集群完整性,方可对外提供服务 通常情况,如果这3个小集群中,任何一个(1主1从)挂了,你这个集群对外可提供的数据只有2/3了, 整个集群是不完整的, redis 默认在这种情况下,是不会对外提供服务的。

    如果你的诉求是,集群不完整的话也需要对外提供服务,需要将该参数设置为no ,这样的话你挂了的那个小集群是不行了,但是其他的小集群仍然可以对外提供服务。

  • cluster countkeysinslot 槽位数字编号:1占 0未占

  • cluster keyslot 键名称keyName:返回keyName对应的槽位

  • redis-cli -a 密码 -p 端口 --c --raw--raw支持中文,--c支持获取非当前redis实例的kv

11 SpringBoot集成Redis

Jedis、Lettuce、RedisTemplate都是与Redis交互的客户端库,用于Java应用程序中访问和操作Redis数据

  • Jedis的设计为单线程,在高并发环境下,需要采取额外的措施来处理连接的并发性。
  • Lettuce支持连接池和异步执行命令,提供更好的性能和并发处理能力,适用于高并发场景
  • Redis Template是 是 Spring Data Redis 提供的一个模板类,帮助你再spring中更快的使用Redis。它封装了与 Redis 进行交互的常见操作。RedisTemplate 可以使用不同的底层客户端实现,其中包括 Jedis 和 Lettuce。因此,你可以选择在应用中使用 Jedis 或 Lettuce 作为 RedisTemplate 的底层实现

测试前

bind配置请注释掉、保护模式设置为no、Linux系统的防火墙设置、redis服务器的IP地址和密码是否正确、访问redis的服务端口号和auth密码

11.1 Jedis测试

新建maven工程

pom.xml文件springboot工程必须继承spring-boot-start-parent,

<!--    所有springboot项目都必须继承自 spring-boot-starter-parent -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.0.5</version>
</parent>

导入依赖包

<!--jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.3.1</version>
</dependency>

<!--web开放场景启动器-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--后续使用lombok的日志功能-->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>

主程序类:其实不需要主程序类,直接就是一个普通的的java连接Redis数据库的测试

@SpringBootApplication
public class MainApplication {
    public static void main(String[] args) {
        SpringApplication.run(MainApplication.class,args);
    }
}

测试Redis数据库操作,五大数据类型

@Slf4j
public class JedisDemo
{
    public static void main(String[] args)
    {
        //连接本地的 Redis 服务,自己的ip和端口和密码
        Jedis jedis = new Jedis("192.168.145.130",6379);
        // 如果 Redis 服务设置了密码,需要下面这行,没有就不需要
        jedis.auth("111111");

        //key
        Set<String> keys = jedis.keys("*");
        for (Iterator iterator = keys.iterator(); iterator.hasNext();) {
            String key = (String) iterator.next();
            System.out.println(key);
        }
        System.out.println("jedis.exists====>"+jedis.exists("k2"));
        System.out.println(jedis.ttl("k1"));
        //String
        //jedis.append("k1","myreids");
        System.out.println(jedis.get("k1"));
        jedis.set("k4","k4_redis");
        System.out.println("----------------------------------------");
        jedis.mset("str1","v1","str2","v2","str3","v3");
        System.out.println(jedis.mget("str1","str2","str3"));
        //list
        System.out.println("----------------------------------------");
        //jedis.lpush("mylist","v1","v2","v3","v4","v5");
        List<String> list = jedis.lrange("mylist",0,-1);
        for (String element : list) {
            System.out.println(element);
        }
        //set
        jedis.sadd("orders","jd001");
        jedis.sadd("orders","jd002");
        jedis.sadd("orders","jd003");
        Set<String> set1 = jedis.smembers("orders");
        for (Iterator iterator = set1.iterator(); iterator.hasNext();) {
            String string = (String) iterator.next();
            System.out.println(string);
        }
        jedis.srem("orders","jd002");
        System.out.println(jedis.smembers("orders").size());
        //hash
        jedis.hset("hash1","userName","lisi");
        System.out.println(jedis.hget("hash1","userName"));
        Map<String,String> map = new HashMap<String,String>();
        map.put("telphone","138xxxxxxxx");
        map.put("address","atguigu");
        map.put("email","zzyybs@126.com");//课后有问题请给我发邮件
        jedis.hmset("hash2",map);
        List<String> result = jedis.hmget("hash2", "telphone","email");
        for (String element : result) {
            System.out.println(element);
        }

        //zset
        jedis.zadd("zset01",60d,"v1");
        jedis.zadd("zset01",70d,"v2");
        jedis.zadd("zset01",80d,"v3");
        jedis.zadd("zset01",90d,"v4");

        List<String> zset01 = jedis.zrange("zset01", 0, -1);
        zset01.forEach(System.out::println);
    }
}

(启动Redis服务器)运行该main函数,测试成功

11.2 Lettuce测试

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

Jedis和Lettuce的区别

jedis和Lettuce都是Redis的客户端,它们都可以连接Redis服务器,但是在SpringBot2.0之后默认都是使用的Lettuce这客户端连接Redis服务器。因为当使用Jedis客户端连接Redis服务器的时候,每个线程都要拿自己创建的Jedis实例去连按Redis客户端,当有很多个线程的时候,不仅开销大需要反复的创建关闭一个Jedis连接,而且也是线程不安全的,一个线程通过Jedis实例更改Redis服务器中的数据之后会影响另一个线程。

但是如果使用Lettuce这个客户端连接Redis服务器的时候,就不会出现上面的情况,Lettuce底层使用的是Netty,当有多个线程都需要连接Redis服务器的时候,可以保证只创建一个Letuce连接,使所有的线程共享这一个Letuce连接,这样可以减少创建关闭一个Letuce连接时候的开销。而且这种方式也是线程安全的,不会出现一个线程通过Lettuce更改Redis服务器中的数据之后而影响另一个线程的情况:

pom.xml导入依赖包

<!--lettuce-->
<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>6.2.1.RELEASE</version>
</dependency>

测试Redis数据库操作

@Slf4j
public class LettuceDemo
{
    public static void main(String[] args)
    {
        //使用构建器 RedisURI.builder
        RedisURI uri = RedisURI.builder()
                .redis("192.168.111.181")
                .withPort(6379)
                .withAuthentication("default","111111")
                .build();
        //创建连接客户端
        RedisClient client = RedisClient.create(uri);
        StatefulRedisConnection conn = client.connect();
        //操作命令api
        RedisCommands<String,String> commands = conn.sync();

        //keys
        List<String> list = commands.keys("*");
        for(String s : list) {
            log.info("key:{}",s);
        }
        //String
        commands.set("k1","1111");
        String s1 = commands.get("k1");
        System.out.println("String s ==="+s1);

            //list
        commands.lpush("myList2", "v1","v2","v3");
        List<String> list2 = commands.lrange("myList2", 0, -1);
        for(String s : list2) {
         System.out.println("list ssss==="+s);
        }
        //set
        commands.sadd("mySet2", "v1","v2","v3");
        Set<String> set = commands.smembers("mySet2");
        for(String s : set) {
         System.out.println("set ssss==="+s);
        }
        //hash
        Map<String,String> map = new HashMap<>();
            map.put("k1","138xxxxxxxx");
            map.put("k2","atguigu");
            map.put("k3","zzyybs@126.com");//课后有问题请给我发邮件

        commands.hmset("myHash2", map);
        Map<String,String> retMap = commands.hgetall("myHash2");
        for(String k : retMap.keySet()) {
         System.out.println("hash  k="+k+" , v=="+retMap.get(k));
        }

        //zset
        commands.zadd("myZset2", 100.0,"s1",110.0,"s2",90.0,"s3");
        List<String> list3 = commands.zrange("myZset2",0,10);
        for(String s : list3) {
         System.out.println("zset ssss==="+s);
        }

        //sort
        SortArgs sortArgs = new SortArgs();
        sortArgs.alpha();
        sortArgs.desc();

        List<String> list4 = commands.sort("myList2",sortArgs);
        for(String s : list4) {
         System.out.println("sort ssss==="+s);
        }

        //关闭
        conn.close();
        client.shutdown();
    }
}

11.3 RedisTemplate

推荐使用

11.3.1 连接单机

  1. 建module:redis_study

  2. 改POM

    <!--SpringBoot通用依赖模块-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!--SpringBoot与Redis整合依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>${log4j.version}</version>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
    
  3. 写YAML

    server.port=7777
    
    spring.application.name=redis_study
    
    spring.redis.database=0
    # 修改为自己真实IP
    spring.redis.host=192.168.111.185
    spring.redis.port=6379
    spring.redis.password=111111
    spring.redis.lettuce.pool.max-active=8
    spring.redis.lettuce.pool.max-wait=-1ms
    spring.redis.lettuce.pool.max-idle=8
    spring.redis.lettuce.pool.min-idle=0
    
  4. 主启动

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

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

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

    @RestController
    public class OrderController
    {
        @Resource
        private OrderService orderService;
        
        @RequestMapping(value = "/order/add",method = RequestMethod.POST)
        public void addOrder()
        {
            orderService.addOrder();
        }
    
        @RequestMapping(value = "/order/{id}", method = RequestMethod.GET)
        public String findUserById(@PathVariable Integer id)
        {
            return orderService.getOrderById(id);
        }
    }
    

如果我们不添加配置类,那么就会出现序列化问题:

image-20240523104417272

看看源码:

image-20240523104531601

解决方式就是添加RedisConfig配置类,指定序列化工具

11.3.2 连接集群

改写配置文件

# 集群连接配置
server.port=7777

spring.application.name=redis7_study

# ========================redis集群=====================
spring.redis.password=111111
# 获取失败 最大重定向次数
spring.redis.cluster.max-redirects=3
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
spring.redis.cluster.nodes=192.168.145.130:6381,192.168.145.130:6382,192.168.145.140:6383,192.168.145.140:6384,192.168.145.150:6385,192.168.145.150:6386

接下访问redis集群,正常访问–一切正常

集群的高可用如何体现?现在6381模拟宕机shutdown,redis中用cluster nodes发现从机6384上位,高可用!微服务客户端再次读写,发现本来应该写入6381的指令还是头铁,没有去找6384,发生故障报错!!也就是说SpringBoot客户端没有动态感知到RedisCluster的最新集群消息

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

解决办法:

  1. 排除lettuce采用jedis(不推荐)

    image-20240523143140497

  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()));
    
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(clusterConfig, clientConfiguration);
        
        return lettuceConnectionFactory;
    
    }
    
  3. Redis节点集群拓扑动态感应配置

    https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster#user-content-refreshing-the-cluster-topology-view

    image-20240523143445480

    配置文件增加配置:

    #支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
    spring.redis.lettuce.cluster.refresh.adaptive=true
    #定时刷新
    spring.redis.lettuce.cluster.refresh.period=2000
    
  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值