Redis

cccs7 Lv5

#Redis

版本: Redis 6


Redis 介绍

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

主流功能与应用


  • 分布式缓存,挡在 MySQL 数据库之前的带刀侍卫

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

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

  • 高可用架构搭配

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

  • 分布式锁

  • 队列

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

    我们常通过 Redis 的队列功能做购买限制。比如到节假日或推广期间,进行一些活动

    对用户购买行为进行限制,限制今天只能购买几次商品或者一段时间只能购买一次。

  • 排行版 + 点赞

    • 在互联网应用中,有各种各样的排行榜,如 电商销量排行榜。Redis 提供的 zset 数据类型能够快速实现这些复杂的排行榜
    • 比如 小说网站对小说进行排名,根据排名,将排名靠前的小说推荐给用户

总体功能概述


image-20230302233213537

优势


  • 性能极高 - Redis 能读的速度是 110000 次/秒,写的速度是 81000 次/秒
  • Redis 数据类型丰富,不仅仅支持简单的 key- value 类型的数据,同时还支持 list、set、zset、hash 等数据结构的存储
  • Redis 支持数据的持久化,可以将内存中的数据保持在 磁盘中,重启的时候可以再次加载进行使用
  • Redis 支持数据的备份,即 master - slave 模式的数据备份

总结


image-20230302233700886

Redis 是一种 key - value 类型的缓存数据库

资料


Redis 源码地址 : https://github.com/redis/redis

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

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

Redis 命令查询 : http://redis.cn/commands.html

Redis 使用

Redis 的 10 大数据类型


image-20230304105255396

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

redis 字符串 (String )

String 是 redis 最基本的数据类型,一个 key 对应 一个 value


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

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

redis 列表(List)

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

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

redis 哈希表(Hash)

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

Redis 中每个 hash可以存储 2^32 - 1 键值对(40 多亿)

redis 集合 (Set)

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

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

集合中最大的成员是 2^32-1 (每个集合可以存储 40多亿个成员)

redis 有序集合 (ZSet )

zset (sorted set:有序集合)

Redis zset 和 set 一样也是 String 类型元素的集合,且不允许有重复的成员

**不同的是每个元素都会关联一个 double 类型的分数**,redis 正是通过 分数来为集合中的成员进行按照从小到大的排序

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

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

redis 地理空间(GEO)

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

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

redis 基数统计(HyperLogLog)

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

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

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

redis 位图(bitmap)

Bit arrays (orsimply bitmaps) 我们可以称之为 位图

image-20230304112630133

一个字节(一个 byte) = 8 位

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

换句话说,每一个小格子就是一个 bit


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

redis 位域(bitfield)

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


换句话说,就是通过 bitfield 命令,我们可以一次性对多个比特位域进行操作

redis 流(Stream)

redis stream 是 Redis 5.0 版本新增加的数据结构

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

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

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

Redis 键(key)


常用

image-20230304114107289

案例

  • keys * : 查看当前库所有的 key
  • exists key : 判断某个 key 是否存在
  • type key : 查看你的 key 是什么类型
  • del key : 删除指定的 key 数据
  • unlink key : 非阻塞删除,仅仅将 keys 从 keyspace 中 删除,真正的删除在后续异步中操作
  • ttl key : 查看还有多少秒过期, - 1 代表永不过期,- 2 表示已过期
  • expire key 秒钟 : 为给定的 key 设置 过期时间
  • move key dbindex 【0-15】 : 将当前数据库的 key 移动到给定的数据库 db 中
  • select dbindex : 切换数据库【0-15】,默认为0
  • dbsize : 查看当前数据库 key 的 数量
  • flushdb : 清空当前库
  • flushall : 通杀 全部库

数据类型命令及落地运用


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


帮助命令

  • help @String
  • help @list
  • help @hash
  • ……

Redis 字符串 (String)

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

SET 命令有 EX PXNXXX 以及 KEEPTTL 五个可选参数,其中 KEEPTTL 为 6.0 版本添加的可选参数,其他为 2.6.12 版本添加的可选参数

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

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

EXATPXAT 以及 GET 为 Redis 在 6.2 新增的 可选参数


返回值

设置成功 则返回OK,返回 nil 为 未执行 SET 命令,如不满足 NXXX 条件等

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


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

Long.toString(System.currentTimeMillis()/1000L).sout

案例
最常用
  • set key value
    • keepttl
同时设置/获取多个 键值
  • MSET key value[key value ...]

    同时设置一个或多个 key-value键值对

    image-20230306224401060
  • MGET key[key ...]

    获取所有(一个或多个)给定 key 的 值

    image-20230306224503423
  • mset/mget/msetnx

    同时设置 一个 或多个 key-value 对,当且仅当所有给定的 key 都不存在

    image-20230306224806449
获得指定区间范围内的值

getrange

获得指定区间范围内的值,类似 between…and 的关系,从0 对 -1 表示全部

image-20230306225248021

setrange

设置指定区间内范围的值,格式是 setrange key值 具体值

image-20230306225339265
数值增减
  • 一定要是数字才可以增减
  • 递增数字
    • INCR key
  • 增加指定整数
    • INCRBY key increment
  • 递减数值
    • DECR key
  • 减少指定的整数
    • DECRBY key decrement
获取字符串的长度和内容追加
  • STRLEN key
  • APPEND key value
分布式锁
image-20230306225707441
  • EX : key 在多少秒后过期
  • PX :key 在多少毫秒之后过期
  • NX :当 key 不存在的时候,才创建 key,效果等同于丢setnx
  • XX : 当 key 存在的时候,覆盖 key
image-20230306225829951
getset

将给定的 key 的值设为 value,并返回 key 的旧值(old value)

简单一句话,先 get ,然后立即 set

image-20230306230033934
应用场景

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


是否 喜欢的文章

阅读数: 只要点击了 star,直接可以使用 incr key 命令增加一个数字1,完成记录数字

Redis列表(List)

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

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

left、right 都可以插入添加

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

如果 键已存在,新增内容

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


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

image-20230307142705891
案例
  • lpush / rpush /lrange

  • lpop / rpop

  • lindex

    • lindex key index
    • 按照 索引下标 获得元素(从上到下)
  • llen

    • 获取 列表中的元素的个数
  • lrem key

    • 数字 N , 给定值 v1 解释(删除 N 个值等于 v1 的元素)
  • rpoplpush

    • 源列表 目的列表
    • 移除列表的最后一个元素,并将该元素添加到另一个列表中并返回
  • lset key index value

    • image-20230308141338454
  • linsert key before/after 已有值 插入的新值

    • 在 list 某个已有值的前后再添加具体值

Redis哈希(Hash)

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

KV 模式不变,但 v 是一个键值对

Map<String,Map<Object,Object>>

案例
  • hset/hget/hmset/hmget/hgetall/hdel
    • image-20230308143612996
    • image-20230308143720534
    • image-20230308143815260
    • image-20230308143910892
  • hlen key
    • 获取某个 key 内的全部数量
    • image-20230308144157562
  • hexists key
    • 在 key 里面的某个值的key
  • hkeys/hvals
    • image-20230308144519547
  • hsetnx
    • 不存在赋值,存在了无效
    • image-20230308144815501
应用场景

JD 购物车早期设计,目前不再采用,当前中小厂可用

Redis集合(Set)

常用
1 sadd key member1 [member2] 向集合中添加一个或多个成员
2 scard key 获取集合的成员数
3 sdiff key1 [key2] 返回给定所有集合的差集
4 sdiffstore destination key1[key2] 返回给定所有集合的差集 并储存在 destination 中
5 sinter key1[key2] 返回给定所有集合的交集
6 sinterstore destination key1[key2] 返回给定所有集合的交集并储存在 destination中
7 sismember key member 判断 member 元素 是否是集合 key 的成员
8 smembers key 返回集合中 的所有成员
9 smove source destination member 将 member 成员从 source 集合移动到 destination 集合
10 spop key 移除并返回集合中的一个随机元素
11 srandmember key [count] 返回集合中一个或多个随机元素
12 srem key member1 [member2] 移除集合中一个或多个成员
13 sunion key1 [key2] 返回所有给定集合的并集
14 sunionstore destination key1[key2] 所有 给定集合的并集存储在 destination 集合中
15 sscan key cursor [MATCH pattern] [COUNT conut] 迭代集合中的元素

单值多 value,且无重复

案例
  • sadd key member[member...]
    • 添加元素
    • image-20230308204308162
  • smembers key
    • 遍历集合中的所有元素
    • image-20230308204428789
  • sismember key member
    • 判断元素是否集合中
    • image-20230308205517261
  • srem key member [member...]
    • 删除元素
    • image-20230308204638709
  • scard key
    • 获取集合里面的元素个数
    • image-20230308204855632
  • srandmember key[数字]
    • 从集合中 随机展现设置的数字个数,元素不删除
    • image-20230308205249094
  • spop key[数字]
    • 从集合中随机弹出一个元素,出一个删一个
    • image-20230308205429379
  • smove key1 key2 在 key1 已经存在的值
    • 将 key1 里的值赋给 key2
    • image-20230308210227997
  • 集合运算
    • A、B
      • A
        • abc12
      • B
        • 123ax
    • 集合的差集运算 A - B
      • 属于 A,但不属于 B 的元素构成的集合
      • sdiff key [key...]
    • 集合的 并集运算
      • 属于 A 或者 属于 B 的元素合并后的 集合
      • sunion key1 [key2]
    • 集合的 交集运算
      • 属于 A 同时也 属于 B 的共同拥有的元素构成的集合
      • sinter key [key2]
      • sintercard numkeys key[key... ][Limit limit]
        • redis 新命令
        • 他不返回结果集,而只返回结果的基数。返回由所有给定集合的交集的基数
    • image-20230308211817758
应用场景
  • 微信抽奖小程序
    • image-20230308211952925
  • 微信朋友圈 查看共同点赞好友
    • image-20230308212016419
  • QQ 内推 可能认识的人

Redis 有序集合Zset (sorted set)

常用

在 set 基础上,每个 val 值前加一个 score 分数值

之前 set 是 k1 v1 v2 v3

现在 zset 是 k1 score1 v1 score2 v2


1 zadd key score1 member1[score2 member2] 向 有序集合添加一个或多个成员,或者 更新 已存在成员的分数
2 zcard key 获取有序集合的成员数
3 zcount key min max 计算在有序集合中 指定区间分数的成员数
4 zincrby key increment member 有序集合中对指定成员的分数加上 增量 increment
5 zinterstore destiantion numkeys key [key1] 计算给定的一个或多个有序集的交集并将结果存储在新的有序集合 key 中
6 zlexcount key min max 在有序集合中计算指定字典区间内成员的数量
7 zrange key start stop [WITHSCORES] 通过 索引区间返回有序集合成指定区间的成员
8 zrangebylex ley min max[LIMIT offset count] 通过字典区间返回有序集合的成员
9 zrangebyscore key min max [WITHSCORES][LIMIT] 通过分数返回有序集合指定区间内的成员
10 zrank key member 返回有序集合中指定成员的索引
11 zrem key member [member…] 移除有序集合中的一个或多个成员
12 zremrangebylex key min max 移除有序集合中给定的字典区间的所有成员
13 zremrangebyrank key start stop 移除有序集合中给定的排名区间内的所有成员
14 zremrangebyscore key min max 移除有序集合中给定的分数区间的所有成员
15 zrevrange key start stop [WITHSCORES] 返回有序集合中指定区间的成员,通过索引,分数从高到低
16 zrevrangebyscore key max min [WITHSCORES] 返回有序集合中 指定分数区间内的成员,分数从高到低
17 zrevrank key member 返回有序集合中指定成员的排名,有序集合成员按分数递减(从大到小)排序
18 zscore key member 返回有序集合中,成员的分数值
19 zunionstore destiantion numkeys key [key2] 计算给定的一个或多个有序集的并集,并存储在 新的 key 中
20 zscan key cursor [MATCH pattern] [COUNT count] 迭代有序集合中的元素(包括元素成员和元素值)
案例

向 有序集合中加入一个元素和该元素的分数

  • zadd key score member[score member...]

    • 添加元素
    • image-20230308220302832
    • image-20230308220344820
  • zrange key start stop[WITHSCORES]

    • 按照元素分数大小从小到大排序。按照索引 从 start 到 stop 之间的所有元素
  • zrevrange key start stop [WITHSCORES]

    • image-20230308220649883
  • zscore key member

    • 获取元素的分数
    • image-20230308220816200
  • zcard key

    • 获取集合中元素的数量
    • image-20230308223347560
  • zrem key 某 score 下对应的 value 值,作用

    • image-20230309102305684
  • zincrby key increment member

    • 增加某个元素的分数
    • image-20230309102718285
  • zcount key min max

    • 获得指定分数范围内的元素个数
    • image-20230309102846804
  • zmpop

    • 从键名列表中的第一个非空排序列表中弹出一个或多个元素,他们是成员分数对
  • zrank key values

    • 作用是 获得下标值
    • image-20230309103641322
  • zrevrank key values 值

    • 作用是 逆序获得下标值
应用场景

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

Redis位图(bitmap)

由 0 1 表现的 二进制位的 bit 数组


需求

  • 用户是否登陆过 Y、N ,比如京东每日签到
  • 电影、广告是否被点击过
  • 钉钉打卡上班。签到统计

是什么


用处

用于状态统计

Y、N ,类似 AtomicBoolean

基本命令
setbit key offset val 给指定的 key 的 第 offset 个赋值 val O(1)
getbit key offset 获得指定 key 的第 offset 位 O(1)
bitcount key start end 返回指定 key 中 从 [start,end ] 中 1 的数量 O(n)
bitop operation destkey key 对不同的二进制存储数据进行 位运算(AND、OR、NOT、XOR) O(n)

  • setbit

  • setbit key offset value - offset 偏移量,只能为 0、1

    • image-20230309110948405
    • bitmap 的偏移量是从 0 开始计算的
  • getbit

    • getbit key offset
  • strlen

    • 不是字符串长度。而是占据几个字节,超过 8 位后自己按照8 位一组 一 byte 再扩容
  • bitcount

    • 全部建包含多少个1
  • bitop

应用场景
  • 一年 365 天,全年天天登陆占多少字节
  • 按照 年

Redis基数统计(HyperLogLog)

需求
  • 统计某个网站的uv,统计某个文章的 uv
  • 什么是 uv
    • unique visitor - 独立访客,一般理解为 客户端ip
    • 需要去重考虑
  • 用户搜索网站关键词的数量
  • 统计用户每天搜索不同词条的个数
是什么
  • 去重统计功能的基数估计算法 - 就是 HyperLoglog
  • 基数
    • 是一种数据集,去重复后的真实个数
    • 案例 - case
      • image-20230309140726948
  • 基数统计
    • 用于统计一个集合中不重复的元素个数,就是对集合进行去重复操作后剩余的元素的计算

一句话,去重脱水后的真实数据

常用命令
1 pfadd key element[element… ] 添加指定元素到 HyperLogLog 中
2 pfcount key [key…] 返回给定 Hyperloglog 的基数估算值
3 pfmerge destkey sourcekey[sourcekey…] 将多个 HyperLoglog 合并成一个 HyperLogLog
应用场景

天猫网站首页 亿级 uv 的redis 统计方案

Redis地理空间(GEO)

移动互联网时代LBS应用越来越多,交友软件中附近的小姐姐、外卖软件中附近的美食店铺、高德地图附近的核酸检查点等等,那这种附近各种形形色色的XXX地址位置选择是如何实现的?

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

例如滴滴打车,最直观的操作就是实时记录更新各个车的位置,

然后当我们要找车时,在数据库中查找距离我们(坐标x0,y0)附近r公里范围内部的车辆

使用如下SQL即可:

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

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

1.查询性能问题,如果并发高,数据量大这种查询是要搞垮数据库的

2.这个查询的是一个矩形访问,而不是以我为中心r公里为半径的圆形访问。

3.精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差

原理

核心 思想就是将球体转换为平面,区块转换为一点

image-20230309202150455

主要分三步

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

redis 在 3.2 版本之后,增加了地理位置的处理

常用命令
  • geoadd 多个经度(longitude)、维度(latitude)、位置名称(member) 添加到指定的 key 中

    • geoadd 用于存储指定的地理空间位置,可以将一个或多个经度(longitude)、维度(latitude)、位置名称(member) 添加到指定的 key 中
    • 语法格式 : geoadd key longitude latitude member [longitude latitude member ...]
      • image-20230309204025147
    • 乱码问题
      • redis-cli --raw
        • image-20230309210846102
  • geopos 从键里面返回所有给定位置元素的位置(经度和维度)

    • geopos 用于从给定的 key 里返回所有指定名称(member) 的位置(经度、维度),不存在的返回 nil
    • 语法格式
      • geopos key member [member ...]
      • image-20230309211548874
  • geodist 返回两个给定位置之间的距离

    • geodist 用于 返回两个给定位置之间的距离
    • geodist 语法格式
      • geodist key member1 member2 [m|km|ft|mi]
        • 后面参数是 距离单位
          • m : 米
          • km : 千米
          • ft : 英尺
          • mi : 英里
      • image-20230309212359777
  • georadius 以给定的经纬度为中心,返回与中心的距离不超过给定最大距离的所有位置元素

    • georadius 以给定的经纬度为中心,返回键包含的位置元素当中,与中心的距离不超过给定的最大距离的所有位置元素
    • GEORADIUS city 116.418017 39.914402 10 km withdist withcoord count 10 withhash desc
    • GEORADIUS city 116.418017 39.914402 10 km withdist withcoord withhash count 10 desc
      • withdist : 在返回位置元素的同时,将位置元素与中心之间的距离也一并返回,距离的单位和用户给定的范围单位保持一致
      • withcoord : 将位置元素的经度和维度也一并返回
      • withhash : 以 52 位有符号整数的形式,返回位置元素经过原始 geohash 编码的有序集合分值。这个选项主要用于底层应用或者调试,实际中作用并不大
      • count : 限定返回的记录数
    • image-20230309213207870
  • georadiusbymember 跟georadius 类似

    • 找出位于指定 范围内的元素,中心点是由给定的位置元素决定
  • geohash 返回一个或多个位置元素的 Geohash 表示

    • Redis GEO 使用 geohash 来保存地理位置的坐标
    • geohash 用于获取一个或多个位置的元素 的 geohash 值
    • geohash 的 语法格式
      • geohash key member [member...]
      • image-20230309212041610

如何获得某个位置的经纬度?

http://api.map.baidu.com/lbsapi/getpoint/

Redis流(Stream)

是什么

redis 5.0 之前的痛点

  • Redis 消息队列 的两种方案

    • List 实现消息队列

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

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

        image-20230309213659075

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

    • (Pub/sub)

      • image-20230309213958521

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


Redis 5.0 版本新增了一个更强大的 数据结构 – Stream


一句话, redis 版的 MQ消息中间件 + 阻塞队列

能干嘛

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

底层结构和原理说明

Stream 结构

image-20230309214654127


一个消息链表,将所有加入的消息都串起来,每个消息都有唯一一个的 ID 和对应的内容


image-20230309214754029

基本命令理论
队列相关指令
1 xadd 添加消息到队列末尾
2 xtrim 限制 Stream 的长度,如果已经超长 会进行截取
3 xdel 删除消息
4 xlen 获取 stream 中的消息长度
5 xrange 获取消息列表(可以指定范围),忽略删除的消息
6 xrevrange 和 xranger 相比,区别在于 反向获取,ID 从大到小
7 xread 获取消息(阻塞/非阻塞),返回大于指定ID 的消息
消费组相关指令
1 xgroup create 创建消费组
2 xreadgroup group 读取 消费组中的消息
3 xack ack 消息,消息被标记为 “已处理”
4 xgroup setid 设置消费组最后递送消息的 id
5 xgroup delconsumer 删除消费组
6 xpending 打印待处理消息的详细信息
7 xclaim 转移消息的归属权(长期未被处理/无法处理的消息,转交为其他消费组进行处理)
8 xinfo 打印 stream / consumer /group 的详细消息
9 xinfo groups 打印消费者组的详细消息
10 xinfo stream 打印 stream 的详细消息
4 个特殊符号
  • - + : 最小和最大可能出现的 ID
  • $ : ¥ 只表示消费新的消息,当前流量中最大的ID,可用于将来要到来的 消息
  • > : 用于 xreadgroup 命令,表示迄今为止还没有发送给组中使用者的消息,会更新消费组的最后 ID
  • * : 用于 xadd 命令,让系统自动生成 id
Redis 流 实例演示
队列相关指令
  • xadd

    • 添加消息到队列末尾

    • xadd 用于向 stream 队列中添加消息,如果指定的消息不存在,则该命令执行时会新建一个 Steam 队列

    • image-20230309221059508

      • * 号 表示服务器自动生成 MessageID(类似于MySQL中的 auto_increment) ,后面顺序跟着一堆业务 key/value

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

      • 客户端显示传入规则:

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

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

    • 消息 ID 必须比上个 ID 大

    • 默认用 * 号 表示自动生成规则

  • xrange

    • 用于获取消息列表(可以指定范围),忽略删除的内容
    • start : 表示开始值, - 表示最小值
    • end : 表示结束值, + 表示最大值
    • count : 表示最多获取多少个值
    • image-20230309221514454
  • xrevrange

    • 与 xrange 的区别在于,获取消息列表的方向是相反的 ,end 在前,start 在后
  • xdel

    • image-20230309221625500
  • xlen

    • 用于获取 stream 队列消息的长度
  • xtrim

    • 用于对 stream 长度进行截取,如 超长会被截取
    • MAXLEN
      • 允许的最大长度,对流进行修剪限制长度
      • image-20230309221820721
    • MINID
      • 允许的最小 id,从 某个值开始比该 id 值小的将会被抛弃
      • image-20230309221918376
  • xread

    • 用于 获取消息(阻塞/非阻塞) ,只会返回大于指定 ID 的消息

      • image-20230309222042352
      • COUNT : 最多读取多少条消息
      • BLOCK : 是否 已阻塞的方式读取消息默认不阻塞,如果 millseconds 设置 为 0 ,表示永远阻塞
    • 非阻塞

      • image-20230309222228782
      • image-20230309222258355
    • 阻塞

      • redis-cli 连接第二个 客户端
      • image-20230309222343107
    • 小总结(类似 Java 的 阻塞队列)

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

      • image-20230309222458593

        对比 List 结构

        image-20230309222516902
消费者相关指令

//TODO

Redis 位域(bitfield)

是什么

官网 : https://redis.com.cn/commands/bitfield.html

能干嘛
  • 位域修改
  • 溢出控制

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

命令
image-20230309223404208

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

Redis 持久化(RDB + AOF)


总体介绍

redis 如何将数据写入硬盘


持久化是指将数据写入持久存储,例如 SSD(固态硬盘)。Redis 提供了 一系列持久化选项,这些包括:

  • RDB (Redis Database 【redis 数据库】):Redis 持久化以指定的时间间隔执行 数据集 的快照
  • AOF (Append Only file ) : AOF 持久化记录 服务器收到的每一个 写 操作。然后可以在 服务器启动时再次重放这些操作,重建原始数据集。使用与Redis 协议本身相同的格式记录命令
  • 无持久化 :可以完全禁用持久性,这有时在 缓存时使用
  • RDB + AOF : 可以在 同一个案例下同时使用 RDB + AOF

image-20230310151651119

RDB (Redis DataBase)

RDB 持久性以指定的时间间隔执行数据集的时间点快照

能干嘛
  • 在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的 snapshot 内存快照,它恢复时再将磁盘快照文件直接读回 内存中
  • redis 的数据都在内存中,保存备份时他执行的是 全量快照,也就是说,把内存中的所有数据都记录到 磁盘中,一锅端
  • rdb 保存的是 dump.rdb 文件
案例演示
image-20230310105325930

RDB 保存到磁盘的文件 叫 dump.rdb


自动触发

redis 6.0.16 以下

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

image-20230310152748384

Redis 6.2 以及 redis 7.0.0

image-20230310171018738
操作步骤
  • 自动触发
    • redis 7版本 ,按照 redis.conf 里配置的 save
      • image-20230310171309483
      • 修改 dump 文件保存路径
        • 默认
          • image-20230310171505245
        • 自定义修改的路径且可以进行 redis里用 config get dir 获取目录
          • image-20230310171801493
          • image-20230310172126770
        • 修改 dump 文件名称
          • image-20230310172237300
        • 触发备份
          • image-20230310172344836
          • image-20230310172404148
        • 如何恢复
          • 将备份文件(dump.rdb) 移动到 redis 安装目录并启动服务即可
          • 备份成功后故意用 flushdb 清空 redis,看是否可以恢复数据
            • 执行 flushall/flushdb 命令也会产生 dump.rdb 文件。但里面是空的。无意义
          • 物理恢复,一定服务和备份 分机隔离
            • image-20230310173458197
            • 备注 : 不可以把 备份文件 dump.rdb 和生产 redis 服务器放在同一台机器,必须分开各自存储,以防生产机物理损坏后备份文件也挂了

手动触发

  • save 和 bgsave

    • redis 提供了两个命令来生成 rdb 文件,分别是 save 和 bgsave

    • save

      • 在主程序中执行 会阻塞 当前 redis 服务器,直到持久化工作完成

        执行 save 命令期间,redis 不能处理 其他命令,**线上禁止使用**

      • image-20230310174335493
      • image-20230310174355231
    • bgsave (默认)

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

      • fork 是什么

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

        • image-20230310174957853

          image-20230310175023057

      • LASTSAVE

        • 可以 通过 lastsave 命令获取最后一次成功执行快照的时间
优势
官网说明
  • rdb 是 redis 数据 的一个非常紧凑的单文件时间点表示,RDB 文件非常是和备份,例如,您可能希望在 最近的 24小时内 每一个小时归档一次 RDB 文件,并在 30 天内每天保存一个 RDB 快照。这使您可以在发生灾难时轻松的恢复不同版本的数据集
  • RDB 非常适合灾难恢复,他是一个可以传输到远程数据中心 或 Amazon S3 (可能已加密)的压缩文件
  • RDB 最大限度地提高 了 Redis 的性能,因为 redis 父进程为了持久化而需要做的唯一工作就是派生一个将完成所有其余工作的子进程。父进程永远不会执行磁盘 I/O 或类似的操作
  • 与AOF相比,RDB 允许使用 大数据集更快的重启
  • 在副本上,RDB支持 重启和故障转移后的部分重新同步
小总结
  • 适合大规模的数据恢复
  • 按照业务定时备份
  • 对数据完整性和一致性要求不高
  • RDB 文件 在内存中的加载速度要比 AOF快得多
劣势
官网说明
  • 如果您需要在Redis停止工作时(例如断电后)将数据丢失的可能性降到最低,那么RDB并不好。您可以配置生成RDB的不同保存点(例如,在对数据集至少5分钟和100次写入之后,您可以有多个保存点)。但是,您通常会每五分钟或更长时间创建一次RDB快照,因此,如果Redis由于任何原因在没有正确关闭的情况下停止工作,您应该准备好丢失最新分钟的数据。
  • RDB需要经常fork0以便使用子进程在磁盘上持久化。如果数据集很大,fork()可能会很耗时,并且如果数据集很大并且CPU性能不是很好,可能会导致Redis停止为客户端服务几毫秒甚至一秒钟。AOF也需要fork()但频率较低,您可以调整要重写日志的频率,而不需要对持久性进行任何权衡。
小总结
  • 在一定间隔时间做一次备份,所以 如果redis 意外 down 掉的话,就会丢失从当前至最近一次快照期间的数据, 快照之间的数据会丢失
  • 内存数据的全量同步,如果数据量太大会导致 I/O 严重影响服务器的性能
  • RDB 依赖 主进程的 fork ,在更大的数据集中,这可能会导致服务器请求的瞬间延迟。fork 的时候内存中的数据被克隆了一份,大致 2 倍的膨胀性,需要考虑
如何 检查修复 dump.rdb 文件
image-20230310181547571
那些情况会触发 rdb 快照
  • 配置文件中默认的 快照配置
  • 手动 save/bgsave 命令
  • 执行 flushdb/flushall 命令也会产生 dump.rdb 文件,但里面是空的 ,无意义
  • 执行 shutdown 且没有设置开启 AOF 持久化
  • 主从复制时,主节点自动触发
如何禁用快照
  • 动态所有停止 rdb 保存规则的方法 redis-cli config set save ""
  • 快照禁用
    • image-20230310181954382
rdb 优化配置项详解

配置文件 snapshotting 模块

  • save <seconds> <changes>
  • dbfilename
  • dir
  • stop-writes-on-bgsave-error
    • 默认 yes
    • 如果配置为 no,表示你不在乎数据不一致或者有其他的手段发现和控制这种不一致,那么在快照写入失败时,也能保证 redis 继续接受新的写请求
  • rdbcompression
    • 默认 yes
    • 对于存储到硬盘中的快照,可以设置是否进行压缩存储。如果是的话,redis 会采用 LZF 算法进行压缩,如果你不想消耗 cpu 来进行压缩的话,可以设置关闭此功能
  • rdbchecksum
    • 默认 yes
    • 在 存储快照后,还可以让 redis 使用 CRC64 算法来进行数据校验,但是这样做会增加大约10 % 的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能
  • rdb-del-sync-files
    • 在没有持久性的前提下删除复制中使用的 RDB文件启用,默认情况下为 no,此选项是禁用的
总结
image-20230310182755358

AOF(Append only file)

是什么
  • **以日志的形式来吉林每个写操作**,将 redis 执行过的所有写命令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis 启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作
  • 默认情况下,redis 是没有开启 AOF 的,开启AOF 功能需要设置配置 appendonly yes

AOF 保存的是 appendonly.aof 文件

AOF 持久化 工作流程
image-20230310201153659

image-20230310201213908

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

image-20230310201704195
案例演示和说明
配置文件说明(6 + 7)

如何开启 AOF

image-20230310202127540

image-20230310202328514


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

image-20230310202445944

aof 文件 - 保存路径

redis 6

  • AOF 保存文件的位置和 RDB 保存文件的位置一样,都是通过 redis.conf 配置文件的 dir 配置
  • image-20230310203049588

redis 7

image-20230310203116779

image-20230310203128686


aof 文件 - 保存名称

redis 6

image-20230310203301762

有且只有一个


redis 7.0 MultiPart AOF 的设计

  • image-20230310203405224

    从Redis 7.0.0开始,当调度AOF重写时,Redis父进程打开一个新的增量AOF文件继续写入。子进程执行重写逻辑并生成新的基础AOF。Redis会使用一个临时清单文件来跟踪新生成的基础文件和增量文件。当它们准备就绪后,Redis将执行原子替换操作,使这个临时清单文件生效。为了避免AOF重写反复失败重试时创建很多增量文件的问题,Redis引入了AOF重写限制机制,保证失败的AOF重写重试的速度越来越慢。

  • image-20230310204052313
    • base 基本文件
    • incr 增量文件
    • manifest 清单文件
  • Redis 7.0 config 中对应的配置项

    • image-20230310204214424
正常恢复
  • 启动 : 设置 yes 修改默认的 appendonly no ,改为 yes
  • 写操作继续,生成 AOF 文件到指定目录
  • 恢复1 : 重启 redis 然后重新加载,结果 OK
  • 恢复2
    • 写入数据进 redis,然后 flushdb + shutdown 服务器
    • 新生成了 dump 和 aof
    • 备份新生成 的 aof.bak,然后删除 dump/aof 再看恢复
      • image-20230310204747742
    • 重启 redis 然后 重新加载试试?
    • 停止服务器,拿出我们的备份修改后再重新启动服务器看看
      • image-20230310204820165
异常恢复
  • 故意乱写正常的 AOF 文件,模拟网络闪断文件写 error
  • 重启 redis 之后就会进入 AOF 文件的载入,发现启动都不行
  • 异常修复命令 : redis-check-aof --fix 进行修复
  • 重新 OK
优势
image-20230310205614892
  • 使用AOF Redis更加持久:您可以有不同的fsync策略:根本不fsync、每秒fsync、每次查询时fsync。使用每秒fsync的默认策略,写入性能仍然很棒。fsync是使用后台线程执行的,当没有fsync正在进行时,主线程将努力执行写入,因此您只能丢失一秒钟的写入。
  • AOF 日志是一个仅附加日志,因此不会出现寻道问题,也不会在断电时出现损坏问题。即使由于某种原因(磁盘已满或其他原因)日志以写一半的命令结尾,redis-check-aof工具也能够轻松修复它。
  • 当AOF变得太大时,Redis能够在后台自动重写AOF。重写是完全安全的,因为当Redis继续附加到旧文件时,会使用创建当前数据集所需的最少操作集生成一个全新的文件,一旦第二个文件准备就绪,Redis就会切换两者并开始附加到新的那一个。
  • AOF以易于理解和解析的格式依次包含所有操作的日志。您甚至可以轻松导出AOF文件。例如,即使您不小心使用该FLUSHALL命令刷新了所有内容,只要在此期间没有执行日志重写,您仍然可以通过停止服务器、删除最新命令并重新启动Redis来保存您的数据集,
劣势
image-20230310210113921
  • 相同数据集的数据而言,aof文件要远大于 rdb 文件,恢复速度慢于 rdb
  • aof 运行效率要慢于 rdb,每秒的同步策略效率较好,不同步效率和 rdb 相同
AOF 重写机制

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


为了解决这个问题, redis 新增了重写机制,当 aof 文件的大小超过所设定的峰值时,redis 就会自动 启动 aof 文件的内容压缩,只保留可以恢复数据集的最小指令集

或者

可以 手动使用命令 bgrewritedaof 来重写


一句话 : 启动 aof 文件的内容压缩,只保留可以恢复数据集的最小指令集

触发机制

官网默认配置

image-20230311113446953

注意 : 同时满足,且的关系 才会触发

  • 1 根据 上次重写 后的 aof 大小,判断当前 aof 大小是不是增长了 1 倍
  • 2 重写时满足的文件大小

自动触发

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


手动触发

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

案例说明
  • 需求说明

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


      举个例子:比如有个key

      一开始你setk1v1

      然后改成setk1v2

      最后改成setk1V3

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

      但是,我们实际效果只需要setk1V3这一条,所以,

      开启重写后,只需要保存setk1v3就可以了只需要保留最后一次修改值,相当于给aof文件瘦身减肥,性能更好。


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

  • 需求验证

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

    • 前期配置准备

      • 开启 aof

        image-20230311114131465
      • 重写 峰值修改为 1K

        image-20230311114205231
      • 关闭混合,设置为 no

        image-20230311114348093
      • 删除 之前的 全部 的aof 和 rdb,排除干扰项

    • 自动触发案例

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

        image-20230311114557100
      • 查看 三大配置文件

        • 配置项

          image-20230311114659669
        • 本次操作

          image-20230311114833549
      • k1 不停暴涨

      • 触发重写

    • 手动触发案例

      • 客户端向服务器端发送 bgrewriteaof 命令
    • 结论

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

配置文件 APPEND ONLY MODE 模块

image-20230311115731747
总结
image-20230311115758363

RDB-AOF 混合持久化

image-20230311120335538

rdb vs aof
问题
  • 可否共存
  • 如果共存听谁的
官网文档
image-20230311120518129
数据回恢复顺序 和 加载流程

在同时开启 rdb 和 aof 的时候,重启只会加载 aof 文件,不会加载 rdb 文件

image-20230311120716237
怎么选
  • rdb 持久化方式能够在指定的时间的时间间隔能够对你的数据进行快照存储
  • aof 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,aof 命令以redis 协议追加保存每次写的操作到文件末尾
同时开启两种持久化方式
  • 在这种情况下, 当redis 重启的时候会优先载入 aof 文件来恢复原始的数据,因为在通常情况下,aof 文科, 保存的数据要比 rdb 文件保存的数据要完整
  • rdb 的数据不实时,同时使用两者时,服务器重启也只会找 aof 文件,那要不要 只使用 aof 呢,建议不要,因为 rdb 更适合用于备份数据库(aof 在不断变化,不好备份),留着 rdb 作为一个万一的手段
推荐方式
rdb + aof 混合模式

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

开启混合方式设置

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

rdb + aof 的混合模式 结论

rdb 镜像做全量持久化,aof 做增量持久化

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

image-20230311122600508
纯缓存模式
同时关闭rdb + aof
  • save ""
    • 禁用 rdb
    • 禁用 rdb 持久化模式下,我们仍然可以使用命令 save,bgsave 生成 rdb 文件
  • appendonly no
    • 禁用 aof
    • 禁用 aof 持久化模式下,我们仍然可以 使用 命令 byrewriteaof 生成 aof 文件

Redis 事务(transactional)


Redis Transactions allow the execution of a group of commands in a single step, they are centered around the commands MULTI, EXEC, DISCARD and WATCH. Redis Transactions make two important guarantees:

  • All the commands in a transaction are serialized and executed sequentially. A request sent by another client will never be served in the middle of the execution of a Redis Transaction. This guarantees that the commands are executed as a single isolated operation.
  • The EXEC command triggers the execution of all the commands in the transaction, so if a client loses the connection to the server in the context of a transaction before calling the EXEC command none of the operations are performed, instead if the EXEC command is called, all the operations are performed. When using the append-only file Redis makes sure to use a single write(2) syscall to write the transaction on disk. However if the Redis server crashes or is killed by the system administrator in some hard way it is possible that only a partial number of operations are registered. Redis will detect this condition at restart, and will exit with an error. Using the redis-check-aof tool it is possible to fix the append only file that will remove the partial transaction so that the server can start again.

Starting with version 2.2, Redis allows for an extra guarantee to the above two, in the form of optimistic locking in a way very similar to a check-and-set (CAS) operation. This is documented later on this page.


redis 事务允许在单个步骤中执行一组命令,他们以 命令 MULTI、EXEC、DISCARD 和 WATCH 为中心。Redis 事务有两个重要的保证 :

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

从 2.2 开始,redis 允许对上述两者 提供额外保证,其形式为 乐观锁,其方式与检查和设置(CAS) 操作 非常相似


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

能干嘛

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

Redis 事务 Vs 数据库事务

  1. 单独的隔离操作

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

  2. 没有隔离级别的概念

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

  3. 不保证原子性

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

  4. 排他性

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

常用命令

1 discard 取消事务,放弃执行事务块内的所有命令
2 exec 执行所有事务块内的命令
3 multi 标记一个事务块的开始
4 unwatch 取消 watch 命令对所有 key 的监视
5 watch key [key…] 监视一个或多个key ,如果在事务执行之前这个或这些 key 被其他命令所改动,那么事务将会被打断

  • 正常执行

    image-20230311134915817
    • MULTI
    • EXEC
  • 放弃事务

    image-20230311135003940
    • MULTI
    • DISCARD
  • 全体连坐

    image-20230311135057916
    • During a transaction it is possible to encounter two kind of command errors:
    • A command may fail to be queued, so there may be an error before EXEC is called. For instance the command may be syntactically wrong (wrong number of arguments, wrong command name, …), or there may be some critical condition like an out of memory condition (if the server is configured to have a memory limit using the maxmemory directive).
      • 命令可能无法排队,因此在 exec 之前可能会出现错误。 例如,该命令可能是句法上错误的(参数的数量错误,命令名称错误,…),或者可能存在某些关键条件(例如,如果服务器配置为使用 maxmemory指令)。
    • A command may fail after EXEC is called, for instance since we performed an operation against a key with the wrong value (like calling a list operation against a string value).
  • 冤头债主

    image-20230311135416687
    • Errors happening after EXEC instead are not handled in a special way: all the other commands will be executed even if some command fails during the transaction.

      This is more clear on the protocol level. In the following example one command will fail when executed even if the syntax is right:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      Trying 127.0.0.1...
      Connected to localhost.
      Escape character is '^]'.
      MULTI
      +OK
      SET a abc
      +QUEUED
      LPOP a
      +QUEUED
      EXEC
      *2
      +OK
      -ERR Operation against a key holding the wrong kind of value

      EXEC returned two-element bulk string reply where one is an OK code and the other an -ERR reply. It’s up to the client library to find a sensible way to provide the error to the user.

      It’s important to note that even when a command fails, all the other commands in the queue are processed – Redis will not stop the processing of commands.

    • 补充 : Redis does not support rollbacks of transactions since supporting rollbacks would have a significant impact on the simplicity and performance of Redis.

      • redis 不提供事务回滚的功能,开发者必须在事务执行出错后,自行恢复数据库状态
    • 注意与传统数据库事务的区别,不一定要么一起成功,要么一起失败

  • WATCH 监控

  • redis 使用 watch 来提供乐观锁,类似于 CAS(check-and-set)

    • 悲观锁(Pessimistic Lock)

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

    • 乐观锁(Optimistic Lock)

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

      • 乐观锁策略 :

        提交版本必须大于 记录当前版本才能执行更新

    • CAS (check-and-set)

      WATCH is used to provide a check-and-set (CAS) behavior to Redis transactions.

      WATCHed keys are monitored in order to detect changes against them. If at least one watched key is modified before the EXEC command, the whole transaction aborts, and EXEC returns a Null reply to notify that the transaction failed.

      • [watch] 用于为REDIS交易提供检查和核算行为。

        [watch] ED键进行监视以检测针对它们的更改。 如果在[exec] 命令之前修改了至少一个 watched 键,则整个事务中止和 return a Null ,以通知交易是否失败。

  • watch

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

- 有加塞篡改

  - watch 命令 是一种 乐观锁的实现,redis 在修改的时候 ,会检测数据是否被更改,如果更改了,则执行失败,第一个窗口 蓝框 step5 返回结果为 nil ,也就是相当于失败

    <img src="https://cs7eric-image.oss-cn-hangzhou.aliyuncs.com/images/image-20230311141641622.png" alt="image-20230311141641622" style="zoom: 33%;" />

    详细见 上 CAS 说明

- unwatch

  <img src="https://cs7eric-image.oss-cn-hangzhou.aliyuncs.com/images/image-20230311142103904.png" alt="image-20230311142103904" style="zoom:33%;" />

- 小结

  - 一旦执行了 exec 之前加的 监控锁 都会被取消掉了
  - 当客户端连接丢失的时候(比如退出连接),所有东西都会被取消监控

总结

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

Redis 管道(pipelining)


面试题

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

  • 问题由来

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

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

      • 服务端处理命令,并将借个车返回给客户端

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

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

是什么

解决思路

引出 管道 这个概念

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

image-20230311143804152

Redis pipelining is a technique for improving performance by issuing multiple commands at once without waiting for the response to each individual command. Pipelining is supported by most Redis clients.

REDIS管道是一种通过立即发出多个命令而无需等待每个单独命令的响应来提高性能的技术。 pipeline 被很多 redis client 所支持。


pipeline 是为了解决 RTT 往返时,仅仅是将命令打包一次性发送,对整个 redis 的执行不造成 其他的影响


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

案例演示

image-20230311144334770

总结
pipeline 与原生批命令对比
  • 原生批命令是 原子性的(mget、mset),pipelining 是非原子性的
  • 原生批命令一次只能执行一种命令,pipelining 支持批量执行不同的命令
  • 原生批命令是服务端实现,而 pipelining 需要服务器端与客户端共同完成
pipelining 与 事务 对比
  • 事务具有原子性,管道不具有原子性
  • 管道一次性将多条命令发送给服务器,事务是一条一条的发,事务只有在接收到 exec 命令才会执行,管道不会
  • 执行事务时 会阻塞其他命令的执行,而执行管道中的命令时不会
使用 pipelining 的注意事项
  • pipelining 缓冲的执行只是会依次执行,不保证原子性,如果执行中执行发生异常,将会继续执行后续的命令
  • 使用 pipelining 组装的命令个数不能太多,不然数据量过大,客户端阻塞阻塞的时间可能过久,同时服务器端此时也被迫回复一个队列答复,占用很多内存

Redis 发布/订阅(pub/sub)


学习定位 : 了解即可

是什么

定义

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

官网

SUBSCRIBE, UNSUBSCRIBE and PUBLISH implement the Publish/Subscribe messaging paradigm where (citing Wikipedia) senders (publishers) are not programmed to send their messages to specific receivers (subscribers). Rather, published messages are characterized into channels, without knowledge of what (if any) subscribers there may be. Subscribers express interest in one or more channels, and only receive messages that are of interest, without knowledge of what (if any) publishers there are. This decoupling of publishers and subscribers can allow for greater scalability and a more dynamic network topology.

订阅,取消订阅和发布实施发布/订阅消息范式,其中(引用Wikipedia)发件人(发布者)未编程以将其消息发送给特定的接收器(订阅者)。 相反,已发布的消息被描述为渠道,而不知道可能有什么(如果有)。 订阅者在一个或多个渠道中表达了兴趣,并且仅接收感兴趣的消息,而没有知道有什么(如果有)出版商的消息。 发布者和订户的这种解耦可以使更大的可扩展性和更动态的网络拓扑结构。


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

能干嘛

redis 客户端可以订阅任意数量的频道,类似于我们的微信可以关注很多的公众号

image-20230311151937817

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

image-20230311152021596
总结

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

image-20230311153829678

常用命令

  • subscribe channel [channel ...]
    • 订阅给定的一个或多个频道的信息
    • 推荐先 执行订阅后再发布,订阅成功之前发布的消息是收不到的
    • 订阅的客户端每次可以收到一个 3 个参数的消息
      • 消息的种类
      • 始发频道的名称
      • 实际消息内容
      • image-20230311154315851
  • publish channel message
    • 发布信息到指定频道
  • psubscribe pattern [pattern ...]
    • 按照模式 批量订阅,订阅一个或多个符合给定模式(支持 * 号 ? 号 之类的) 的频道
  • pubsub subcommand [argument[argument...]]
    • 查看订阅与发布系统状态
    • pubsub channels
      • 由活跃频道组成的列表
      • image-20230311154604730
    • pubsub numsub [channel [channel...]]
      • 某个频道有几个订阅者
      • image-20230311154654125
    • pubsub numpat
      • 只统计使用 psubscribe 命令执行的,返回客户端订阅的唯一 模式的数量
  • unsubscribe [channel [channel ...]]
    • 取消订阅
  • punsubscribe [pattern [pattern ...]]
    • 退订所有给定模式的频道

总结

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

Redis 复制(replication)


image-20230311155500746

At the base of Redis replication (excluding the high availability features provided as an additional layer by Redis Cluster or Redis Sentinel) there is a leader follower (master-replica) replication that is simple to use and configure. It allows replica Redis instances to be exact copies of master instances. The replica will automatically reconnect to the master every time the link breaks, and will attempt to be an exact copy of it regardless of what happens to the master.

在Redis复制的基础上(不包括Redis Cluster或Redis Sentinel提供的额外层的高可用性功能),有一个易于使用和配置的领导者追随者(Master-Replica)复制。 它允许REPIS REDIS实例是主实例的确切副本。 每次链接断开时,复制品都会自动重新连接到主人,并尝试成为其确切的副本,无论主人发生了什么。


就是 主从复制, master 以写为主,slave 以读为主

当 master 数据变化时,自动将新的数据异步同步到其他的 slave 数据库

能干嘛

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

怎么用

配从(库)不配主(库)
权限细节,重要
  • master 如果配置了 requirepass 参数,需要密码登陆
  • 那么 slave 就要配置 masterauth 来设置 校验密码,否则的话 master 会拒绝 slave 的访问请求
    • image-20230311163412295
基本操作命令
  • info replication

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

  • replicaof 主库ip 主库端口

    • 一般 写入进 redis.conf 的配置文件内
  • slaveof 主库ip 主库端口

    • 每次与 master 断开之后,都需要重新连接,除非你配置进 redis.conf 中
    • 在运行期间 修改 slave节点的信息,如果该数据库已经是某一个主数据库的从数据库,那么会停止和源主数据库的同步关系 转而和新的主数据库同步
  • slaveof no one

    • 使当前数据库停止与其他数据库的同步, 转为主数据库

案例演示

架构说明
  • 一个 Master 两个 Slave
    • image-20230311165425992
    • 三台虚拟机都安装 redis
  • 拷贝多个 redis.conf 文件
    • redis6379.conf
    • redis6380.conf
    • redis6381.conf
口诀
  • 三边网络相互 ping 通,且注意 防火墙配置
  • 三大命令
    • 主从复制
      • replicaof 主库ip 主库端口
      • 配从(库)不配主(库)
    • 改换门庭
      • slaveof 新主库ip 新主库端口
    • 自立为王
      • slaveof no one
修改配置文件细节操作

redis6379.conf 为例

  1. 开启 daemonize yes

    image-20230311170108402
  2. 注释掉 bind 127.0.0.1

    image-20230311170247155
  3. protected-mode no

    image-20230311170510847
  4. 指定端口

    image-20230311170549390
  5. 指定当前工作目录 dir

    image-20230311170747641

  6. pid 文件名字, logfile

    image-20230311171115631

  7. log 文件名字,logfile

    image-20230311171331006

  8. requirepass

    image-20230311171922852

  9. dump.rdb 名字

    image-20230311172032044

  10. aof 文件,appendfilename

<img src="https://cs7eric-image.oss-cn-hangzhou.aliyuncs.com/images/image-20230311172108222.png" alt="image-20230311172108222" style="zoom:33%;" />
  1. 从机 访问 主机 的通行密码 masterauth 必须

    • 从机需要配置,主机不需要
    • image-20230311172422569
常用
一主二仆
  • 方案 1 : 配置文件固定写死

    • 配置执行

      • replicaof 主机ip 主库端口
    • 配从(库)不配主(库)

      • 配置从机 6380

        image-20230311202003826
      • 配置从机 6381

        image-20230311202020172
    • 先 master 后 两台 slave 依次启动

      • image-20230311202442137
    • 主从关系查看

      • 日志

        • 主机日志

          image-20230311202708291

        • 备机日志

          image-20230311202743095

      • 命令

        • info replication 命令查看

          image-20230311202843615 image-20230311202901684 image-20230311202915330

主从问题演示

  • 从机可以执行写命令

    image-20230311203234915
  • 从机切入点问题

    slave 是从头开始复制还是从 切入点开始复制

    master 启动,写到 k3

    slave1 跟着 master 同时启动,跟着写到 k3

    slave2 写到 k3 后才启动,那之前的是否也可以复制


    Y ,首次一锅端,后续跟随,master 写 ,slave 跟

  • 主机 shutdown 后,从机 会上位吗?

    主机 shutdown 后情况如何?从机是上位还是原地待命


    从机不动,原地待命,从机数据可以正常使用;等待主机重启动归来

    image-20230311203934135 image-20230311203948622 image-20230311204002528
  • 主机shutdown 后,重启后主从关系还在吗?从机还能顺利复制吗

    image-20230311204120619、、

  • 某台 从机 down后,master继续,从机重启后他还能跟上大部队吗


  • 方案二 : 命令操作手动指定

    • 从机停机去掉配置文件中的配置项。3台目前都是主机状态,各不从属

      image-20230311204846212
    • 3 台 master

      image-20230311204952099 image-20230311205009219 image-20230311205021859
    • 预设的从机上执行命令

      • slaveof 主库ip 主库端口

      • 效果

        image-20230311205353995 image-20230311205403777
    • 用命令使用的话,2台从机重启后,关系还在吗

      image-20230311205729395

配置和命令的区别

  • 配置,持久稳定
  • 命令,当次生效
薪火相传
  • 上一个 slave 可以是下一个 slave 的 master,slave 同样可以接收其他 slave 的连接和 同步请求,那么 该 slave 作为了 链条中下一个的 master,可以有效减轻主 master 的写压力
  • 中途变更转向 : 会清除的之前的数据,重新建立拷贝最新的
  • slaveof 新主库ip 新主库端口
反客为主

slaveof no one : 使当前数据库停止与其他数据库的同步,转成主数据库

复制原理和工作原理

  • 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 秒
    • image-20230311213413392
  • 进入平稳,增量复制
    • Master 继续将新的所有收集到的 修改命令自动依次传给 slave,完成同步
  • 从机下线,重连续传
    • master 会检查 backlog 里面的 offset,master 和 slave 都会保存一个复制的 offset 还有一个 masterId,offset 是保存在 backlog 里面的,master 只会把已经复制的 offset 后面的数据复制给 slave,类似于断点续传
复制的缺点
复制延时,信号衰减

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

image-20230311214334251
master 挂了怎么办
  • 默认情况下,不会在 slave 节点自动重选一个 master
  • 那每次都要人工干预?
    • 无人值守安装变成刚需

Redis 哨兵(sentinel)


High availability for non-clustered Redis - 非集群的高可用性

Redis Sentinel provides high availability for Redis when not using Redis Cluster .

Redis Sentinel also provides other collateral tasks such as monitoring, notifications and acts as a configuration provider for clients.


吹哨人巡查监控后台 master 主机是否故障,如果故障了,根据**投票数**自动 将某一个从库转换为新主库,继续对外服务


作用

  • 监控 redis 的运行状态,包括 master 和 slave
  • **当 master down机,能自动将 slave 切换成新 master **
image-20230311221745138

无人值守运维


能干嘛

This is the full list of Sentinel capabilities at a macroscopic level (i.e. the big picture):

  • Monitoring. Sentinel constantly checks if your master and replica instances are working as expected.
  • Notification. Sentinel can notify the system administrator, or other computer programs, via an API, that something is wrong with one of the monitored Redis instances.
  • Automatic failover. If a master is not working as expected, Sentinel can start a failover process where a replica is promoted to master, the other additional replicas are reconfigured to use the new master, and the applications using the Redis server are informed about the new address to use when connecting.
  • Configuration provider. Sentinel acts as a source of authority for clients service discovery: clients connect to Sentinels in order to ask for the address of the current Redis master responsible for a given service. If a failover occurs, Sentinels will report the new address.

这是宏观级别的前哨功能的完整列表

  • 监视: Sentinel不断检查您的主人和复制实例是否按预期工作。
  • 通知: Sentinel可以通过API通知系统管理员或其他计算机程序,其中一个被监视的REDIS实例出现了问题。
  • 自动故障转移: 如果主人无法按预期工作,Sentinel可以启动故障转移过程,将复制品促进给主人,则重新配置了其他额外的副本以使用新的主机,并且使用REDIS服务器的应用程序已通知有关新地址的信息 连接时。
  • 配置提供商。:Sentinel充当客户服务发现的授权来源:客户连接到哨兵,以要求当前的Redis Master的地址负责给定服务。 如果发生故障转移,前哨将报告新地址。

主从监控

监控主从 redis 库运行是否正常

消息通知

哨兵可以将故障转移的结果发送给客户端

故障转移

如果 master异常,则会进行主从切换,将其中一个 slave 作为新的 master

配置中心

客户端通过连接哨兵来获得当前 redis 服务的主节点地址

案例演示

redis sentinel 架构
image-20230311215355661
  • 3 个哨兵
    • 自动监控和维护集群,不存放数据,只是吹哨人
  • 1 主 2 从
    • 用于数据读取和存放
案例步骤
  • /myredis 目录下 新建或者拷贝 sentinel.conf 文件,名字绝不能错

  • 先看看 /opt 目录下默认的 sentinel.conf 文件的内容

    image-20230311223435317
  • 重点参数项说明

    • bind

      服务器监听地址,用于客户端连接,默认本机地址

    • daemonize

      是否以后台 daemonize 方式运行

    • protected-mode

      安全保护模式

    • port

      端口

    • logfile

      日志文件路径

    • pidfile

      pid 文件路径

    • dir

      工作目录

    • sentinel monitor <master-name> <ip> <redis-port> <quoru>

      • 设置要监控的 master 服务器

      • quorum 表示最少有多少个哨兵认可客观下线,同意故障迁移的法定票数

        • quorum 表示确认客观下线的最少的哨兵数量

          image-20230312092107982

          网络是不可靠的,有时候一个sentinel会因为网络堵塞而 误以为 一个master redis已经死掉了,在sentinel集群环境下需要多个sentinel互相沟通来确认某个master 是否真的死了 ,quorum这个参数是进行客观下线的一个依据,意思是至少有quorum个sentinel认为这个master有故障,才会对这个master进行下线以及故障转移。因为有的时候,某个sentinel节点可能因为自身网络原因,导致无法连接master,而此时master并没有出现故障,所以,这就需要多个sentinel都一致认为该master有问题,才可以进行下一步操作,这就保证了公平性和高可用。

    • sentinel auth-pass <master-name> <password>

      • master 设置了密码,连接 master 服务的密码
    • sentinel down-after-milliseconds <master-name> <milliseconds>

      指定多少毫秒之后,主节点没有应答哨兵,此时哨兵主观上认为主节点下线

    • sentinel parallel-syncs <master-name> <nums>

      表示允许并行同步的slave个数,当Master挂了后,哨兵会选出新的Master,此时,剩余的slave会向新的master发起同步数据

    • sentinel failover-timeout <master-name> <milliseconds>

      故障转移的超时时间,进行故障转移时,如果超过设置的毫秒,表示故障转移失败

    • sentinel notification-script <master-name> <script-path>

      配置当某一事件发生时所需要执行的脚本

    • sentinel client-reconfig-script <master-name> <script-path>

      客户端重新配置主节点参数脚本

  • 哨兵 sentinel 文件通用配置

    • sentinel26379.conf

      1
      2
      3
      4
      5
      6
      7
      8
      9
      bind 0.0.0.0
      daemonize yes
      protected-mode no
      port 26381
      logfile "/opt/myredis/sentinel26381.log"
      pidfile /var/run/redis-sentinel26381.pid
      dir "/opt/myredis"
      sentinel monitor mymaster 192.168.21.169 6379 2
      sentinel auth-pass mymaster C020611.
    • sentinel26380.conf

      1
      2
      3
      4
      5
      6
      7
      8
      9
      bind 0.0.0.0
      daemonize yes
      protected-mode no
      port 26381
      logfile "/opt/myredis/sentinel26381.log"
      pidfile /var/run/redis-sentinel26381.pid
      dir "/opt/myredis"
      sentinel monitor mymaster 192.168.21.169 6379 2
      sentinel auth-pass mymaster C020611.
    • sentinel26381.conf

      1
      2
      3
      4
      5
      6
      7
      8
      9
      bind 0.0.0.0
      daemonize yes
      protected-mode no
      port 26381
      logfile "/opt/myredis/sentinel26381.log"
      pidfile /var/run/redis-sentinel26381.pid
      dir "/opt/myredis"
      sentinel monitor mymaster 192.168.21.169 6379 2
      sentinel auth-pass mymaster C020611.
    • sentinel26379、26380、26381.conf

      image-20230312093408239
    • master 主机配置文件说明

      image-20230312093440394
  • 先启动 一主二从 3个 redis 示例实例,测试正常的主从复制

    • 架构说明

      image-20230312093624411

      image-20230312093637099

    • redis6379/80/81.conf 内容,填写主从配置

      • 主机 6379

        6379 后续可能会变成从机,需要设置访问密码,请设置 masterauth C020611.

        不然后续可能报错 master_link_status:down

      • 6380/81

        参考上面配置

    • 三台不同的虚拟机实例,启动三部真实机器示例并连接

      • redis-cli -a C020611. -p 6379/80/81
  • ————————————————-哨兵部分—————————————————-

  • 再启动三个哨兵,完成监控

    • redis-sentinel sentinel26379.conf --sentinel
    • redis-sentinel sentinel26380.conf --sentinel
    • redis-sentinel sentinel26381.conf --sentinel
  • 再启动 3 个哨兵监控后,再测试一次主从复制

  • 原有的 master down机了

    • 手动模拟关闭 6379 服务器,模拟down 机

    • 问题

      • 两台从机数据是否正常
      • 是否会从剩下的两台机器上选出新的 master
      • 之前 down 机的master 机器重启归来,谁会是老大,会不会双 master 冲突
    • 数据 OK

      • 两个小问题

        image-20230312094511038 image-20230312094520137
      • 6380/6381

        Broken pipe

      • Broken pipe

        • pipe

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

        • 解决问题

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

        • 总结

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

          image-20230312095222544
    • 投票新选

      • sentinel26379.log

        image-20230312095903020

      • sentinel26380.log

        image-20230312095922085

      • sentinel26381.log

        image-20230312095937279

    • 谁是 master (此案例)

    • 6381 被选为 新的 master,上位成功

      image-20230312100046138
    • 以前的 6379 从 master 降级 成为 slave

      image-20230312100130475
    • 6380 还是 从机,只不过换了个 新老大 6381(79变81),6380 还是 slave

  • 对比配置文件

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

生产都是不同机房,不同服务器,很少出现三个 哨兵全挂掉的情况

可以同时监控多个master,一行一个

哨兵运行流程和选举原理

当一个主从配置中的 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不可以用了

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

表示master被当前sentinel实例认定为失效的间隔时间,这个配置其实就是进行主观下线的一个依据

master在多长时间内一直没有给Sentine返回有效信息,则认定该master主观下线。也就是说如果多久没联系上redis-servevr,认为这个redis-server进入到失效(SDOWN)状态。

ODwown 客观下线(Objectively Down)

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

sentinel monitor <master-name> <ip> <redsi-port> <quorum>

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

quorum **这个参数 是进行客观下线的一个依据**,法定人数 / 法定票数

意思是 :

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

选举出 leader(领导者) 哨兵

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

三哨兵 log 文件解读

sentinel26379.log

image-20230312102841293

sentinel26380.log

image-20230312102929005

sentinel26381.log

image-20230312102957390

选举算法(Raft 算法)
image-20230312103108846

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

raft 算法的 基本思路是 先到先得

即在一轮选举中,哨兵 a 向 b 发送成为领导者的 申请,如果 b 没有同意过 别的 哨兵,,则会同意 a 成为 领导者

由 领导者 开始推动故障切换流程并选出新的 master
  • 新主登基

    • 某个 slave 被选为 新 master

    • 选出新 master 的规则,剩余 slave 节点健康前提下

      image-20230312104131914
      • redis.conf 文件中,优先级 slave-priority 或者 replica-priority 最高的从节点(数字越小优先级越高)

        image-20230312104251234
      • 复制偏移位置 offset 最大的从节点

      • 最小 Run ID 的从节点

        • 字典顺序,ASCII 码
  • 群臣拜服

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

    • 之前的master回来后,成为 slave,选择 现在的master成为其 slave
    • 将之前下线的 老 master 设置为 新选出的 新 maser 的从节点,当老 master 重新上线后,他会成为 新 master 的 从节点
    • sentinel leader 会让原来的 master 降级成为 slave 并恢复正常工作

总结

image-20230312105204360

上述的 failover 操作均由 sentinel 自己独立完成,完全不需要 人工干预

哨兵使用 建议

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

Redis 集群(cluster)


由于数据量过大,单个 master 复制集 难以承担,因此需要对多个复制集进行集群,形成水平扩展。每个复制集只负责整个数据集的一部分,这就是 redis 集群,其作用就是在 多个 redis节点间 共享数据的程序集

image-20230312110549308

redis 集群是一个提供在 多个 redis 节点间的共享数据的程序集

redis 集群可以支持多个 master

能干嘛

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

集群算法-分片-槽位 slot

The cluster’s key space is split into 16384 slots, effectively setting an upper limit for the cluster size of 16384 master nodes (however, the suggested max size of nodes is on the order of ~ 1000 nodes).

Each master node in a cluster handles a subset of the 16384 hash slots. The cluster is stable when there is no cluster reconfiguration in progress (i.e. where hash slots are being moved from one node to another). When the cluster is stable, a single hash slot will be served by a single node (however the serving node can have one or more replicas that will replace it in the case of net splits or failures, and that can be used in order to scale read operations where reading stale data is acceptable).

The base algorithm used to map keys to hash slots is the following (read the next paragraph for the hash tag exception to this rule):

1
HASH_SLOT = CRC16(key) mod 16384

The CRC16 is specified as follows:

  • Name: XMODEM (also known as ZMODEM or CRC-16/ACORN)
  • Width: 16 bit
  • Poly: 1021 (That is actually x^16 + x^12 + x^5 + 1)
  • Initialization: 0000
  • Reflect Input byte: False
  • Reflect Output CRC: False
  • Xor constant to output CRC: 0000
  • Output for “123456789”: 31C3

14 out of 16 CRC16 output bits are used (this is why there is a modulo 16384 operation in the formula above).

In our tests CRC16 behaved remarkably well in distributing different kinds of keys evenly across the 16384 slots.

Note: A reference implementation of the CRC16 algorithm used is available in the Appendix A of this document.

集群的密钥空间被分成16384个插槽,有效地设置了16384个主节点的集群大小上限(但是,建议的最大节点大小大约为1000个节点)。

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

redis集群的数据分片

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

redis 集群有 16384 个 哈希槽,每个 key 通过 CRC16 校验后对 16384 进行取模 来决定 放置哪个槽。集群的每个节点负责一小部分的 hash 槽,举个例子,如果当前集群有三个节点,那么 :

image-20230312112116663
redis 集群的分片
分片

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

如何找到给定key 的分片

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

image-20230312112603743
槽位 和 分片的优势

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

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

slot 槽位映射

哈希取余分区
image-20230312113334736

image-20230312113356671

一致性哈希算法分区
算法背景

设计目标是为了解决 分布式缓存数据 变动和映射问题,某个机器 宕机了,分母数量变了,自然取余数不 OK 了

能干嘛

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

三大步骤
  • 算法构建一致性哈希环

    一致性哈希算法必然有个 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-20230312122518309
  • redis 服务器 IP 节点映射

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


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

    image-20230312123059222
  • key 落到服务器的落键规则

    当我们需要存储一个 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-20230312123607085
优点
  • 一致性 哈希算法的 容错性

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

    image-20230312124123568
  • 一致性 哈希算法的 扩展性

    数据量增加了,需要增加一台 Node x,X 的位置在 A 和 B 之间,那受到影响的只有 A 到 X 之间的 数据,重新把从 A 到 X 的 数据录入到 X 上即可,不会导致 hash 取余全部数据重新洗牌

    image-20230312124457726
缺点

Hash 环的数据倾斜问题

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

image-20230312124647270
总结

为了在节点数目发生改变时尽可能少的迁移数据


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

而当有节点加入或退出时仅影响该节点在Hash环上顺时针相邻的后续节点。


优点

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


缺点

数据的分布和节点的位置有关,因为这些节点不是均匀的分布在哈希环上的,所以数据在进行存储时达不到均匀分布的效果。

哈希槽分区
是什么

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

能干什么

为了 解决 一致性哈希算法 数据倾斜 问题而出现。**在数据和节点之间又加入了一层,把这层成为 哈希槽(slot),用于管理数据与节点之间的关系**,现在就相当于节点上放的是 槽,槽里放的是数据

image-20230312125104245

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

多少个 hash slot

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

槽 会记录节点和槽的对应关系,解决了节点和槽的关系之后,接下来就需要对 key 求 hash值,然后 对 16384 取模,余数是几,key 就落入相对应的槽里

HASH_SLOT = CRC16(key) mod 16384

以槽为单位移动数据,因为槽的数目是固定的,处理起来比较容易,这样数据移动问题就解决了

hash slot 计算

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

image-20230312125708103

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

redis 集群并没有用 一致性 hash 而是引入了 哈希槽的 概念。 **redis 集群有 16384 个哈希槽**,每个 key 通过 CRC16 校验后 对 16384 进行取模来决定防止哪个槽,集群的每一个节点负责一部分槽。但是 为什么是 16384 个呢


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

说明一

The reason is:

  1. Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
  2. At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.

So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.


正常心跳包携带节点的完整配置,可以用旧配置以幂等的方式替换,以便更新旧配置。这意味着它们包含原始形式的节点插槽配置,该配置使用2k空间和16k插槽,但使用65k插槽时会使用禁止性的8k空间。
同时,由于其他设计上的权衡,Redis集群不太可能扩展到超过1000个主节点。
因此,16k是在正确的范围内,以确保每个主机有足够的插槽,最多1000个maters,但这个数字足够小,可以轻松地将插槽配置作为原始位图进行传播。注意,在小集群中,位图将很难压缩,因为当N较小时,位图将具有 slot / N 位集 占设置为的很大百分比

image-20230312194505703
(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的压缩率就很低。

计算结论
image-20230312194650243

write safety

Redis Cluster uses asynchronous replication between nodes, and last failover wins implicit merge function. This means that the last elected master dataset eventually replaces all the other replicas. There is always a window of time when it is possible to lose writes during partitions. However these windows are very different in the case of a client that is connected to the majority of masters, and a client that is connected to the minority of masters.

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


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

集群环境案例步骤

三主三从 redis 集群配置
  • 找三台真是虚拟机,各自新建

    • mkdir -p /opt/myredis/cluster
  • 新建 6 个独立的redis 实例服务

    • IP : 192.168.21.129 + 端口6381 / 6382

      • vim /opt/myredis/cluster/redisCluster6381.conf

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        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 /opt/myredis/cluster/redisCluster6382.conf

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        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
    • IP : 192.168.21.xx + 端口6383 / 6384

      • vim /opt/myredis/cluster/redisCluster6383.conf

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        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 /opt/myredis/cluster/redisCluster6384.conf

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        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
    • IP : 192.168.21.xx + 端口6385 / 6386

      • vim /opt/myredis/cluster/redisCluster6385.conf

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        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 /opt/myredis/cluster/redisCluster6386.conf

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        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 实例

      • redis-server /opt/myredis/cluster/redisCluster6381.conf
  • 通过 redis-cli 命令为 6台机器构建集群关系

    • 构建主从关系

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

        注意主机 IP

        image-20230312200234000
      • 一切 OK

        image-20230312200305092
  • 连接进入 6381 作为切入点, 查看并检验集群状态

    • image-20230312200411648
    • info replication

      image-20230312200522988
    • cluster info

      image-20230312200451847
    • cluster nodes

      image-20230312200509463

主从容错切换迁移案例
  • 容错切换转移

    • 主 6381 和 从机切换,先停止主机 6381

      • 6381 停了,对应的真实主机上位
      • 6381 作为 1 号主机分配的从机以实际情况为准,具体是几号机器就是几号机器
    • 在此查看集群信息,本次 6381 主 和 6384从

      • image-20230312201901933
    • 停止主机 6381,再次查看集群信息

      image-20230312202010598

      • 6384 成功上位并正常使用

        image-20230312202046706
    • 随后,原来的主机 6381 回来了,是否会上位

      • 恢复前

        image-20230312202215337
      • 恢复后

        image-20230312202232708

      • 6381 不会上位,并以节点形式回归

  • 集群不保证数据一致性 100 %OK,一定会有数据丢失情况

    • redis 集群不保证强一致性,这意味着在特定的条件下,redis 集群可能会丢掉一些被系统收到的写入请求命令
  • 手动故障转移 or 节点从属调整该如何处理

    • 上面 一换后 6381 和 6384 主从对调了,和原始不一样了,该怎么办
    • 重新登陆 6381 机器
    • 常用命令
      • cluster failover
主从扩容案例
  • 新增 6387 、6388 两个服务实例配置文件 + 新建后启动

    • IP : 192.168.21.129 + 端口 6387/6388
      • vim /opt/myredis/cluster/redisCluster6387.conf
  • 启动 87/88 两个实例,此时他们自己都是 master

    • redis-server /opt/myredis/cluster/redisCluster6387.conf
  • 将新增的 6387 节点(空槽号) 作为 master 节点加入 原集群

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

      redis-cli -a 密码 –cluster add-node 自己实际IP地址:6387 自己实际IP地址:6381

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

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

      1
      2
      3
      redis-cli -a 密码 --cluster add-node 自己实际IP地址:6387 自己实际IP地址:6381

      redis-cli -a 111111 --cluster add-node 192.168.111.174:6387 192.168.111.175:6381
      image-20230312215333163
    • 检查集群情况第一次

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

      • redis-cli -a C020611. --cluster check 192.168.21.129:6381

      • image-20230312215514762
    • 重新分派槽号(**reshard**)

      • 命令 : redis-cli -a 密码 –cluster reshard ip地址:port
      • redis-cli -a C020611. --cluster reshard 192.168.21.128:6381
      • image-20230312215804154
    • 检查集群第二次

      • redis-cli -a C020611. --cluster check 192.168.21.129:6381

        image-20230312215915482
      • 槽号分派说明

        为什么 6387 是3 个新的区间,以前的还是连续?

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

        image-20230312220107441
    • 为主节点 6387 分配从节点 6388

      • 命令 : redis-cli -a 密码 –cluster add-node ip:新 slave 端口 ip:新master 端口 –cluster-slave –cluster-master-id 新主机id

        redis-cli -a C020611. --cluster add-node 192.168.21.129:6388 192.168.21.129:6387 --cluster-slave --cluster-master-id 4feb6a7ee0ed2b39ff86474cf4189ab2a554a40f --------这个是6387的编号,按照自己实际情况

      • image-20230312220539864

  • 检查集群第三次

    • redis-cli -a C020611. --cluster check 192.168.21.129:6381

      image-20230312220733255
主从缩容案例
  • 目的:6387 和 88 下线

  • 检查集群情况第一次,鲜活的从节点 6388 的节点 ID

  • 从集群中将 4 号从节点 6388 删除

    • 命令:redis-cli -a 密码 –cluster del-node ip:从机端口 从机6388节点ID

    • redis-cli -a C020611. --cluster del-node 192.168.21.129:6387 218e7b8b4f81be54ff173e4776b4f4faaf7c13da

      image-20230312221434481

    • redis-cli -a C020611. --cluster check 192.168.21.129:6385

      image-20230312221521484

      剩 7 台

  • 将 6387 的槽号清空,重新分配,本例子将清出来的槽号都给 6381

    • redis-cli -a C020611. --cluster reshard 192.168.21.129:6381

      image-20230312221615730
  • 检查集群第二次

    • redis-cli -a C020611. --cluster check 192.168.21.129:6381

      4096个槽位都指给6381,它变成了8192个槽位,相当于全部都给6381了,不然要输入3次,一锅端

      image-20230312221705690
  • 将 6387 删除

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

    • redis-cli -a C020611. --cluster del-node 192.168.21.129:6387 4feb6a7ee0ed2b39ff86474cf4189ab2a554a40f

      image-20230312221824955

  • 检查集群情况第三次,6387/88 被彻底去除

    • redis-cli -a C020611. --cluster check 192.168.21.129:6381

      image-20230312221928580

集群常用命令和CRC16 算法分析

通识占位符

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

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

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

image-20230312222318281
常用命令
  • 集群是否完整才能对外提供服务

    image-20230312222410954
    默认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-require-full-coverage
  • CLUSTER CONUTKEYSINSLOT 槽位数字编号

    • 1 : 表示 该槽位被占用
    • 0 : 表示 该槽位没有被占用
  • CLUSTER KEYSLOT 键名称

    该键应该存在那个槽位上

Springboot 集成 redis


总体概述

jedis-lettuce-RedisTemplate 三者的联系

本地 Java 连接 Redis 常见问题

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

集成 Jedis

Jedis

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

步骤
建 Module

redis_study

改 POM
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.atguigu.redis7</groupId>
<artifactId>redis7_study</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
<relativePath/>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
</properties>

<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
<!--通用基础配置-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
写 YML
1
2
3
server.port=7777

spring.application.name=redis7_study
业务类

入门案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.cs7eric.jedis.demo;

import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;

@Slf4j
public class TestJedis {

public static void main(String[] args) {
Jedis jedis = new Jedis("192.168.21.129", 6379);
jedis.auth("C020611.");
log.info("redis conn status:{}","连接成功");
log.info(jedis.ping());
jedis.set("k1222","2222");
log.info(jedis.get("k1222"));
}
}

集成 Lettuce

lettuce

Lettuce是一个Redis的Java驱动包,Lettuce翻译为生菜,没错,就是吃的那种生菜,所以它的Logo长这样

lettuce 和 jedis 的区别

jedis和Lettuce都是Redis的客户端,它们都可以连接Redis服务器,但是在SpringBoot2.0之后默认都是使用的Lettuce这个客户端连接Redis服务器。因为当使用Jedis客户端连接Redis服务器的时候,每个线程都要拿自己创建的Jedis实例去连接Redis客户端,当有很多个线程的时候,不仅开销大需要反复的创建关闭一个Jedis连接,而且也是线程不安全的,一个线程通过Jedis实例更改Redis服务器中的数据之后会影响另一个线程;但是如果使用Lettuce这个客户端连接Redis服务器的时候,就不会出现上面的情况,Lettuce底层使用的是Netty,当有多个线程都需要连接Redis服务器的时候,可以保证只创建一个Lettuce连接,使所有的线程共享这一个Lettuce连接,这样可以减少创建关闭一个Lettuce连接时候的开销;而且这种方式也是线程安全的,不会出现一个线程通过Lettuce更改Redis服务器中的数据之后而影响另一个线程的情况;

案例
pom
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.atguigu.redis7</groupId>
<artifactId>redis7_study</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
<relativePath/>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
</properties>

<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
<!--lettuce-->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.2.1.RELEASE</version>
</dependency>
<!--通用基础配置-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
业务类
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package com.atguigu.redis7.test;

import io.lettuce.core.RedisClient;
import io.lettuce.core.RedisFuture;
import io.lettuce.core.RedisURI;
import io.lettuce.core.SortArgs;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.async.RedisAsyncCommands;
import io.lettuce.core.api.sync.RedisCommands;
import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;

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

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

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

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

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

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

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

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

集成 RedisTemplate (推荐)

连接单机
module

redisTemplate

pom
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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.atguigu.redis7</groupId>
<artifactId>redis7_study</artifactId>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.10</version>
<relativePath/>
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
</properties>


<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.3.1</version>
</dependency>
<!--lettuce-->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.2.1.RELEASE</version>
</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>
<!--swagger2-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--通用基础配置junit/devtools/test/log4j/lombok/hutool-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
yaml
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
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.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
业务类
  • 配置类

    • RedisConfig

      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
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      package com.atguigu.redis7.config;

      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
      import org.springframework.data.redis.core.RedisTemplate;
      import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
      import org.springframework.data.redis.serializer.StringRedisSerializer;

      /**
      * @auther zzyy
      * @create 2022-11-17 17:34
      */
      @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;
      }
      }
    • SwaggerConfig

      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
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      package com.atguigu.redis7.config;

      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import springfox.documentation.builders.ApiInfoBuilder;
      import springfox.documentation.builders.PathSelectors;
      import springfox.documentation.builders.RequestHandlerSelectors;
      import springfox.documentation.service.ApiInfo;
      import springfox.documentation.spi.DocumentationType;
      import springfox.documentation.spring.web.plugins.Docket;
      import springfox.documentation.swagger2.annotations.EnableSwagger2;

      import java.time.LocalDateTime;
      import java.time.format.DateTimeFormatter;

      /**
      * @auther zzyy
      * @create 2022-11-17 17:44
      */
      @Configuration
      @EnableSwagger2
      public class SwaggerConfig
      {
      @Value("${spring.swagger2.enabled}")
      private Boolean enabled;

      @Bean
      public Docket createRestApi() {
      return new Docket(DocumentationType.SWAGGER_2)
      .apiInfo(apiInfo())
      .enable(enabled)
      .select()
      .apis(RequestHandlerSelectors.basePackage("com.atguigu.redis7")) //你自己的package
      .paths(PathSelectors.any())
      .build();
      }
      public ApiInfo apiInfo() {
      return new ApiInfoBuilder()
      .title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
      .description("springboot+redis整合,有问题给管理员阳哥邮件:zzyybs@126.com")
      .version("1.0")
      .termsOfServiceUrl("https://www.atguigu.com/")
      .build();
      }
      }
  • service

    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
    34
    35
    36
    package com.atguigu.redis7.service;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;

    import javax.annotation.Resource;
    import java.util.UUID;
    import java.util.concurrent.ThreadLocalRandom;

    /**
    * @auther zzyy
    * @create 2022-07-14 15:11
    */
    @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);
    }
    }
  • controller

    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
    34
    35
    36
    37
    38
    39
    40
    41
    42
    package com.atguigu.redis7.controller;

    import com.atguigu.redis7.service.OrderService;

    import io.swagger.annotations.Api;
    import io.swagger.annotations.ApiOperation;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;

    import javax.annotation.Resource;
    import java.util.concurrent.ThreadLocalRandom;

    /**
    * @auther zzyy
    * @create 2022-07-14 15:08
    */
    @Api(tags = "订单接口")
    @RestController
    @Slf4j
    public class OrderController
    {
    @Resource
    private OrderService orderService;

    @ApiOperation("新增订单")
    @RequestMapping(value = "/order/add",method = RequestMethod.POST)
    public void addOrder()
    {
    orderService.addOrder();
    }


    @ApiOperation("按orderId查订单信息")
    @RequestMapping(value = "/order/{id}", method = RequestMethod.GET)
    public String findUserById(@PathVariable Integer id)
    {
    return orderService.getOrderById(id);
    }
    }
测试
连接集群
启动 6 台redis 实例
第一次 改写 yaml
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
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
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
直接通过 微服务访问 redis 集群
问题
  • 人为模拟,master-6381 机器意外宕机,手动 shutdown

  • 先对 redis集群命令方式,手动验证各种读写命令,看看 6384 是否上位

  • redis cluster 集群能够自动感应并自动完成主备切换,对应的 slave 6384 会被选举为新的 master 节点

  • 微服务客户端再次读写访问

    • 故障现象

      • 经典故障

        【故障演练】 Redis Cluster集群部署采用了3主3从拓扑结构,数据读写访问master节点, slave节点负责备份。当master宕机主从切换成功,redis手动OK,but 2个经典故障

        image-20230313233758364

      • Springboot 客户端没有动态感应到 redis cluster 的最新集群消息

    • 导致原因

      • springboot 2.x 版本,redis 默认的连接池采用 lettuce,当 redis 集群节点发生变化后,lettuce 默认是不会刷新节点拓扑的
    • 解决方案

      • 排除 lettuce 采用 jedis

        image-20230313234108359
      • 重写 连接工厂实例(不采用)

        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
        34
        35
        36
        37
        38
        39
        40
        41
        42
        43
        44
        45
        46
        47
        48
        49
        50
        51
        52
        53
        54
        55
        56
        57
        58
        59
        60
        61
        62
        63
        64
        65
        66
        //仅做参考,不写,不写,不写。



        @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;
        }
      • 刷新节点集群拓扑动态感应

    • 第二次改写 yml

      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
      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
  • Title: Redis
  • Author: cccs7
  • Created at: 2023-02-12 17:30:22
  • Updated at: 2023-06-29 23:13:33
  • Link: https://blog.cccs7.icu/2023/02/12/Redis/
  • License: This work is licensed under CC BY-NC-SA 4.0.
 Comments
On this page
Redis