Redis7基础学习笔记

文章目录

image-20240418195131769


1. 简介

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

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

中文文档:https://www.redisio.com/

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

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

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

Redis能做什么?

  • 分布式缓存

    image-20240422133437554

  • 内存存储和持久化(RDB+AOF),redis支持异步将内存中的数据写到硬盘上,同时不影响继续服务。

  • 高可用架构搭配(单机、主从、哨兵、集群)

  • 缓存穿透、击穿、雪崩

  • 分布式锁

  • 队列

  • 排行榜+点赞等

image-20240422132247602

优势:

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

image-20240422154003050

Redis版本号第二位如果是奇数,则为非稳定版本,如2.7、2.9等。如果第二位为偶数,则为稳定版本。

Redis新特性

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

image-20240422154854655

  • Redis Functions

image-20240422155118245

  • Client-eviction

image-20240422155212134

  • multi-part AOF

  • ACL v2

image-20240422155321281

  • listpack

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

  • 新增命令

    新增ZMPOP,BZMPOP,LMPOP,BLMPOP等新命令,对于EXPIRE和SET命令,新增了更多的命令参数选项。例如,ZMPOP的格式如下:ZMPOP numkeys key [key ...] MIN|MAX [COUNT count],而BZMPOP是ZMPOP的阻塞版本。

在Linux环境下安装Redis必须先具备gcc编译环境

查看gcc:gcc -v

如果没有,则执行命令yum -y install gcc-c++进行安装

安装步骤

  • 下载获得redis-7.2.4.tar.gz后将它放入我们的Linux目录/opt

  • /opt目录下解压redis

  • 进入redis目录

  • 在redis-7.2.4目录下执行make命令,make && make install

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

    • redis-benchmark:性能测试工具,服务启动后运行该命令,看性能怎么样
    • redis-check-aof:修复有问题的AOF文件
    • redis-check-dump:修复有问题的dump.rdb文件
    • redis-cli:客户端,操作入口
    • redis-sentinel:redis集群使用
    • redis-server:redis服务器启动命令
  • 将默认的redis.conf拷贝到自己定义好的一个路径下,比如/myredis/redis7.conf

    • 配置文件,改完后确保生效,记得重启
    • 默认daemonize no改为daemonize yes,后台启动
    • 默认protected-mode yes改为protected-mod no
    • 默认bind 127.0.0.1直接注释掉
    • 添加redis密码,requirepass xxx
  • 修改/myredis目录下redis.conf配置文件做初始化设置

  • 启动服务,redis-server /opt/redis-7.2.4/myredis-redis7.conf

  • 连接服务,redis-cli -a xxx -p 6379

quit退出redis-cli客户端。

关闭redis-server服务器

  • 单实例关闭:redis-cli -a xxx shutdown
  • 多实例关闭,指定端口关闭:redis-cli -p 6379 shutdown
  • 在redis-cli内部执行shutdown命令关闭服务器,然后执行quit退出客户端

卸载步骤

  • 停止redis服务
  • 删除/usr/local/bin目录下与redis相关的文件

2. Redis十大类型

2.1 总体概述

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

image-20240507125009561

命令查询手册:

  • https://redis.io/commands/
  • https://www.redis.net.cn/order/

2.2 key操作命令

  • 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:通杀全部库

2.3 大小写和帮助命令

命令不区分大小写,但是Key是区分大小写的

帮助命令查询:

help @string
help @list
help @hash
...

2.4 string字符串类型

string是redis最基本的类型,一个key对应一个value。

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

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

单值单value。

set key value语法

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

参数:

  • EX –-设置指定的过期时间,以秒为单位(正整数)。
  • PX milliseconds – 设置指定的过期时间,以毫秒为单位(正整数)。
  • EXAT timestamp-seconds – 设置密钥过期的指定 Unix 时间,以秒为单位(正整数)。
  • PXAT timestamp-milliseconds – 设置密钥过期的指定 Unix 时间,以毫秒为单位(正整数)。
  • NX– 仅当密钥尚不存在时才设置该密钥。
  • XX– 仅当密钥已存在时才设置该密钥。
  • KEEPTTL– 保留与密钥关联的生存时间。
  • GET– 返回存储在 key 处的旧字符串,如果 key 不存在则返回 nil。SET如果存储在 key 中的值不是字符串,则会返回错误并中止。

get key获取值

同时设置/获取多个键值

MSET key value [key value ...]
MGET key [key ...]
mset/mget/msetnx

获取指定区间范围内的值

getrange/setrange

类似于java中的substring,如

set k1 helloworld

getrange k1 0 4
# hello

setrange k1 1 123
get k1
# h123oworld

数值增减

递增数值

INCR key

增加指定的整数

INCRBY key 3  # 设置步长为3

递减数值

DECR key

减少指定的整数

DECRBY key 3   # 设置步长为3

获取字符串长度和内容追加

STRLEN key   # 获取字符串长度

APPEND key value    # 内容追加

分布式锁

setnx key value

setex(set with expire)键秒值/setnx(set if not exist)

setex:设置带过期时间的key,动态设置。

setex 键 秒值 真实值

setnx:只有在 key 不存在时设置 key 的值。

image-20240507141700985

getset(先get再set)

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

2.5 list列表类型

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

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

单key多value,有序有重复

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

left、right都可以插入添加;

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

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

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

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

常用命令

  • lpush/rpush/lrange
  • lpop/rpop
  • lindex:按照索引下标获得元素(从上到下)
  • llen:获取列表中元素的个数
  • lrem key 数字N 给定值v1:删除N个值等于v1的元素
  • ltrim key 开始index 结束index:截取指定范围的值后再赋值给key
  • rpoplpush 源列表 目的列表
  • lset key index value
  • linsert key before/after 已有值 插入新值

如:

lpush l1 1 2 3 4 5

rpush  l1 1 2 3

lrange l1 0 -1
# 5 4 3 2 1 1 2 3

2.6 hash哈希表类型

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

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

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

常用命令

  • hset/hget/hmset/hmget/hgetall/hdel
  • hlen
  • hexists key 在key里面的某个值的key
  • hkeys/hvals
  • hincrby/hincrbyfloat,增数值增加,小数值增加
  • hsetnx,不存在则新建赋值,存在则不生效

如:

hset user:001 id 11 name codewei age 24 gender 男
hget user:001 id
hgetall user:001

hincrby user:001 age 1

2.7 set集合类型

单值多value,无序无重复

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

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

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

常用命令

  • SADD key member [member ...]:添加元素

  • SMEMBERS key:遍历集合中所有的元素

  • SISMEMBERS key member:判断元素是否在集合中

  • SREM key member [member ...]:删除元素

  • scard:获取集合里面的元素个数

  • SRANDMEMBER key [数字]:从集合中随机展现设置的数字个数元素,元素不删除

  • SPOP key [数字]:从集合中随机弹出一个元素,出一个删一个

  • smove key1 key2 在key1里已存在的某个值:将key1里面已存在的某个值赋值给key2

  • 集合运算

    • A、B

    • 差集运算A-BSDIFF key [key ...]

    • 并集运算A∪BSUNION key [key ...]

    • 交集运算A∩B

      SINTER key [key ...]

      SINTERCARD numkeys key [key ...] [LIMIT limit],它不返回结果集,而返回结果的基数,返回由所有给定集合的交集产生的集合的基数。

如:

sadd s1 1 2 3 1 2 4 5 6

smembers s1
# 1 2 3 4 5 6 会自动去重

SINTER a1 a2   # a 1 2
sintercard 2 a1 a2  # 3

2.8 zset有序集合类型

有序,无重复

Redis zset(Sorted set) 和 set 一样也是string类型元素的集合,且不允许重复的成员。

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

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

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

之前set是k1 v1 v2 v3,现在zset是,k1 score1 v1 score2 v2。

常用操作

  • 向有序集合中加入一个元素和该元素的分数
  • ZADD key score member [score member ...]:添加元素
  • ZRANGE key start stop [WITHSCORES]:按照元素分数从小到大的顺序,返回索引从start到stop之间的所有元素
  • zrevrange:逆序遍历输出
  • ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]:获取指定分数范围的元素,(不包含,limit 作用是返回限制, limit 开始下标步 多少步
  • ZSCORE key member:获取元素的分数
  • ZCARD key:获取集合中元素的数量
  • zrem key 某score下对应的value值:作用是删除元素
  • ZINCRBY key increment member:增加某个元素的分数
  • ZCOUNT key min max:获得指定分数范围内的元素个数
  • ZMPOP:从键名列表中的第一个非空排序集中弹出一个或多个元素,它们是成员分数对
  • zrank key values:获得下标值
  • zrevrank key values:逆序获得下标值

2.9 bitmap位图类型

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

image-20240514093909379

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

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

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

位图可以用于状态统计。

常用命令

image-20240508091847185

  • setbit key offset value

    setbit键,偏移位,只能0或1

    bitmap的偏移量是从0开始算的

  • getbit key offset

  • strlen

    统计字节数占用多少

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

  • bitcount

    统计全部键里面有多少个1

  • bitop

    连续2天都签到的用户

    加入某个网站或者系统,它的用户有1000W,做个用户id和位置的映射

    比如0号位对应用户id:uid-092iok-lkj

    比如1号位对应用户id:uid-7388c-xxx

    image-20240508093937191

如:

setbit b1 0 1
setbit b1 5 1

getbit b1 0   # 1
getbit b1 1   # 0
bitcount b1 0 10  # 2

# bitop演示
hset uid:map 0 uid-092iok-lkj
hset uid:map 1 uid-7388c-xxx

setbit 0508 1 1

setbit 0509 0 1
setbit 0509 1 1

setbit 0510 0 1
setbit 0510 1 1

setbit 0511 0 1
setbit 0511 1 1

# 统计0508和0509都签到的人数
bitop and k 0508 0509
bitcount k   # 1
# 统计0509和0510都签到的人数
bitop and k 0509 0510
bitcount k   # 2
# 统计连续4天都签到的人数
bitop and k 0508 0509 0510 0511
bitcount k  # 1

2.10 HyperLoglog基数统计类型

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

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

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

可以用于统计网站或者文章的UV。UV:Unique Visitor,独立访客,一般理解为客户端IP。需要考虑去重。

可以用于用户搜索网站关键词的数量统计

统计用户每天搜索不同词条的个数

去重统计功能的基数估计算法就是HyperLoglog

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

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

常用命令

  • PFADD key element [element ...]:添加指定元素到HyperLoglog中。
  • PFCOUNT key [key ...]:返回给定HyperLoglog的基数估算值。
  • PFMERGE destkey sourcekey [sourcekey ...]:将多个HyperLoglog和并为一个HyperLoglog。

如:

PFADD webview:0508 129.211.75.68 127.22.71.66 129.28.73.43
PFCOUNT webview:0508 # 3

PFADD webview:0509 129.211.75.68 127.27.89.73 129.59.27.32
PFADD webview:0510 129.57.32.12 127.75.54.87 129.28.73.43

PFMERGE webview:3 webview:0508 webview:0509 webview:0510
PFCOUNT webview:3  # 7

2.11 GEO地理空间类型

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

  • 添加地理位置的坐标。

  • 获取地理位置的坐标。

  • 计算两个位置之间的距离。

根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。

GEO的底层数据结构为zset

地球上的地理位置是使用二维的经纬度表示,经度范围 (-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.精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差

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

主要分为3步:

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

image-20240508140817502

常用命令

  • GEOADD 多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中

    GEOADD key longitude latitude member [longitude latitude member ...]

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

    GEOPOS key member

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

    GEODIST key member member m[km|ft|mi]

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

    GEORADIUS key longitude latitude num m[km|ft|mi] [withdist] [withcoord] [count num] [withhash] [desc]

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

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

    count num:限定返回的记录数

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

  • GEORADIUSBYMEMBER 跟GEORADIUS类似

    GEORADIUS key member num m[km|ft|mi] [withdist] [withcoord] [count num] [withhash] [desc]

  • GEOHASH返回一个或多个位置元素的 Geohash 表示

    GEOHASH key member [member ...]

    geohash算法生成的base32编码值

    3维变2维变1维

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

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

如:

GEOADD city 116.418067 39.886373 天坛 117.022878 36.665813 趵突泉

GEOPOS city 天坛

GEODIST city 天坛 趵突泉 km  # 362.08

GEORADIUS city 117.0324 36.680518 10 km withdist withcoord  count 10 withhash desc
# 趵突泉 1.8431 {"latitude": 36.66581248952775, "longitude": 117.02287763357162} 4065929170479734

GEORADIUSBYMEMBER city 天坛 1000 km  # 趵突泉 天安门

2.12 Stream流类型

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

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

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

Redis消息队列的两种方案:

  • List实现消息队列,其实就是点对点的模式,对于一对多力不从心

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

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

    image-20240514093857124

  • Pub/Sub

    image-20240514093852207

Stream流类型就是Redis版的MQ消息中间件+阻塞队列

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

Stream结构

image-20240514093842793

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

名称介绍
Message Content消息内容
Consumer group消费组,通过XGROUP CREATE 命令创建,同一个消费组可以有多个消费者
Last_delivered_id游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
Consumer消费者,消费组中的消费者
Pending_ids消费者会有一个状态变量,用于记录被当前消费已读取但未ack的消息Id,如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack它就开始减少。这个pending_ids变量在Redis官方被称之为 PEL(Pending Entries List),记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符),它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理

ACK:签收确认

队列相关指令

  • XADD:添加消息到队列末尾

    消息ID必须要比上个ID大,默认用*自动生成规矩,*用于在XADD命令中,让系统自动生成id,像是mysql中的自增主键

    XADD key *|id field value [field value ...]

  • XTRIM:限制Stream的长度,如果已经超长会进行截取

    MAXLEN是允许的最大长度,对流进行修剪限制长度

    MINID为允许的最小ID,从某个ID值开始比该ID值小的将会被抛弃

    XTRIM key MAXLEN num

    XTRIM key MINID messageid

  • XDEL:删除消息

    XDEL key messageid

  • XLEN:获取Stream中的消息长度

    XLEN key

  • XRANGE:获取消息列表(可以指定范围),忽略删除的消息

    XRANGE key start end [COUNT count]-表示最小值,+表示最大值

  • XREVRANGE:和XRANGE相比区别在于反向获取,ID从大到小

    XREVRANGE key end start [COUNT count]

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

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

    COUNT表示最多读取多少条消息,BLOCK是否以阻塞的方式读取消息,默认不阻塞,如果milliseconds设置为0,表示永远阻塞

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

    0-0表示从最小的ID开始获取消息,当不指定count,将会返回stream中所有的消息,也可以使用0,或者00,或者000

如:

xadd mystream * id 1001 cname 张三
xadd mystream * id 1002 cname 李四
xadd mystream * id 1003 cname 王五  # 1715218524218-0  消息ID

xrange mystream - + count 2
# 1715218507959-0 张三 1001
# 1715218522389-0 李四 1002
xrange mystream 1715218507960 + count 2 
# 1715218522389-0 李四 1002
# 1715218524218-0 王五 1003

xdel mystream 1715218522389-0

xlen mystream # 2

xtrim mystream MAXLEN 1  # 截取messgeid最大的1个消息,其他消息抛弃
xtrim mystream minid 1715219718393  # 抛弃掉比 1715219718393 messgeid小的消息

xread count 2 streams mystream $  # nil
xread count 2 streams mystream 0-0
# [{"id": "1715219718393-0", "fields": {"cname": "王五", "id": "1003"}}, {"id": "1715219719939-0", "fields": {"cname": "张三", "id": "1001"}}]

# 读取消息 - 阻塞
xread count 1 block 0 streams mystream $
# 此时该redis客户端在监听最新的消息,然后我们新开启一个redis客户端,再新redis客户端中向该stream中插入一条消息
xadd mystream * id 1006 cname 七七
# 此时,我们之前的客户端就会监听到该消息
# 1) 1) "mystream"
#   2) 1) 1) "1715221058147-0"
#         2) 1) "id"
#            2) "1006"
#            3) "cname"
#            4) "七七"
# (17.94s)

生成的消息ID:1715218524218-0,-前表示毫秒时间戳,-后表示在这个时间戳下的第几条消息

消费组相关指令

  • XGROUP CREATE:创建消费者组

    XGROUP CREATE key groupName 0|$

    0:表示从stream头部开始消费

    $:表示从stream尾部开始消费

  • XREADGROUP GROUP:读取消费者组中的消息

    XREADGROUP group groupName consumer [COUNT count] [BLOCK milliseconds] [NOACK] streams key [key ...] id [id ...] >

    “>”,表示从第一条尚未被消费的消息开始读取

    注意:不同消费组的消费者可以消费同一条消息

    stream中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费者内其他消费者读取了,即同一个消费组里的消费者不能消费同一条消息。

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

  • XACK:ack消息,消息被标记为“已处理”

    XACK key gropuName messageId

  • XGROUP SETID:设置消费组组最后递送消息的ID

  • XGROUP DELCONSUMER:删除消费组组

  • XPENDING:打印待处理消息的详细信息

    XPENDING key groupName

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

    XPENDING key groupName - + count consumer

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

  • XCLAIM:转移消息的归属权(长期未被处理/无法处理的消息,转交给其他消费组组进行处理)

  • XINFO:打印Stream\Consumer\Group的详细信息

    • XINFO GROUPS:打印消费者组的详细信息
    • XINFO STREAM key:打印Stream的详细信息

重点问题

问题基于 Stream 实现的消息队列,如何保证消费者在发生故障或宕机再次重启后,仍然可以读取未处理完的消息?
1Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息保底措施,直到消费者使用 XACK 命令通知 Streams“消息已经处理完成”。
2消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 XACK 命令确认消息已经被消费完成

image-20240509131504075

如:

xgroup create mystream groupA $
xgroup create mystream groupB 0

xreadgroup group groupA consumer1 streams mystream > 
# [{"id": "1715231921890-0", "fields": {"cname": "丹丹", "id": "1006"}}, {"id": "1715231923752-0", "fields": {"cname": "溜溜", "id": "1007"}}]
xreadgroup group groupB consumer1 streams mystream >
# [{"id": "1715231921890-0", "fields": {"cname": "丹丹", "id": "1006"}}, {"id": "1715231923752-0", "fields": {"cname": "溜溜", "id": "1007"}}]
xreadgroup group groupB consumer2 streams mystream >
# nil

# 让组内的多个消费者共同分担读取消息,所以,我们通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的
xgroup create mystream groupC 0
xreadgroup group groupC consumer1 count 1 streams mystream >
xreadgroup group groupC consumer2 count 1 streams mystream >
xreadgroup group groupC consumer3 count 1 streams mystream >

xpending mystream groupA consumer1
# 2 1715231921890-0 1715231923752-0 {"consumer1": 2}
xpending mystream groupA - + 10 consumer1
# 1715231921890-0 consumer1 957752 1
# 1715231923752-0 consumer1 957752 1

xack mystream groupA 1715231921890-0

四个特殊符号

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

2.13 bitfield位域类型

了解即可。

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

说白了就是通过bitfield命令我们可以一次性对多个比特位域进行操作。

作用:

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

常用命令

  • BITFIELD key [GET type offset]
  • BITFIELD key [SET type offset value]
  • BITFIELD key [INCRBY type offset increment]
  • 溢出控制OVERFLOW [WRAP|SAT|FAIL]

3. Redis持久化

将内存中的数据写入到硬盘中。

image-20240509143538010

3.1 RDB

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

在指定的时间间隔,执行数据集的时间点快照。

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

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

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

Rdb保存的是dump.rdb文件

image-20240509144035817

redis6与redis7 RDB配置文件对比

redis7

# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 change was performed
#   * After 300 seconds (5 minutes) if at least 100 changes were performed
#   * After 60 seconds if at least 10000 changes were performed

redis6

#   In the example below the behavior will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
3.1.1 自动触发

按照redis.conf里配置的save <seconds> <changes>

本次案例:

触发间隔时长5秒钟以上且有2次修改就触发RDB

redis.conf中修改(如果使用了自己的自定义配置文件,则在自定义配置文件中修改,如我们使用myredis.conf)

save 5 2

修改dump文件的保存路径

dir /myredis/dumpfiles

注意:我们指定的这个文件夹不能不存在,需要我们手动创建好,mkdir -p /myredis/dumpfiles

修改dump文件的文件名称

dbfilename dump6379.rdb

修改配置文件后,重启redis-server。

验证我们的配置是否正确且是否已经生效,在redis客户端执行命令:

CONFIG GET dir  # /myredis/dumpfiles

触发备份

image-20240509152947406

目前,dumpfiles目录下文件为空。

我们执行如下命令后

set k1 v1
set k2 v2

再次查看目录下文件,发现备份文件已经存在

image-20240509153119356

如何恢复?

注意:

  • 执行flushdbflushall也会产生dump.rdb文件,但是里面是空的,无意义。
  • 当我们执行shutdown命令时,也会生成dump.rdb文件保存一次快照。

将备份文件dump.rdb移动到redis配置的dumpfiles目录并启动服务即可。

注意不要让flushdb、flushall和shutdown等命令产生的rdb文件覆盖我们生成的rdb文件!注意备份迁移!

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

3.1.2 手动触发

我们可以使用SAVE或者BGSAVE命令手动触发数据备份。

SAVE

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

image-20240509165149985

BGSAVE(默认)

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

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

fork是什么?

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

image-20240509165327466

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

优点

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

缺点

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

RDB总结

  • 适合大规模的数据恢复
  • 按照业务定时备份
  • 对数据完整性和一致性要求不高
  • RDB 文件在内存中的加载速度要比 AOF 快得多
  • 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失从当前至最近一次快照期间的数据,快照之间的数据会丢失。
  • 内存数据的全量同步,如果数据量太大会导致I/0严重影响服务器性能
  • RDB依赖于主进程的fork,在更大的数据集中,这可能会导致服务请求的瞬间延迟。fork的时候内存中的数据被克隆了一份,大致2倍的膨胀性,需要考虑。

RDB文件修复命令

redis-check-rdb 文件路径

哪些情况下会产生RDB快照?

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

RDB快照禁用

  • 方法一:动态所有停止RDB保存规则的方法:redis-cli config set save "”
  • 方法二(推荐):在配置文件中修改,save ""

RDB优化参数

  • 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,此选项是禁用的。

3.2 AOF

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

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

AOF保存的是appendonly.aof文件。

AOF持久化工作流程

image-20240512192117947

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

AOF缓冲区三种写回策略

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

image-20240512192416696

redis6到redis7,AOF功能发生了很大的变化。

开启AOF:在redis的配置文件中,进行配置

appendonly yes  # 默认为no

配置写回策略

# appendfsync always
appendfsync everysec   # 默认为everysec
# appendfsync no

AOF文件保存路径

  • redis6中,AOF保存文件的位置和RDB保存文件的位置一样,都是通过redis.conf配置文件的 dir 配置。
  • redis7中,新增了一个配置项appenddirname,最终AOF的文件保存在dir配置路径加上appendirname之后的路径。

如:

dir /myredis/dumpfiles
appenddirname "appendonlydir"

那么,AOF文件就会存储在/myredis/dumpfiles/appendonlydir目录下。

我们之前配置的dir/myredis/dumpfiles,因为配置的路径中有/dumpfiles,此时我们在该目录下再存储aof文件就不是很合适。所以,我们在此处将dir修改为/myredis。最终配置为:

dir /myredis
appenddirname "appendonlydir"

AOF文件名称

  • redis6时,通过appendfilename进行配置,只有一个AOF文件,所以只有一个文件名称。
appendfilename appendonly.aof
  • redis7时,引入了新特性Multi Part AOF。存在三个文件:base基本文件、incr增量文件和manifest清单文件。
# - appendonly.aof.1.base.rdb as a base file.
# - appendonly.aof.1.incr.aof, appendonly.aof.2.incr.aof as incremental files.
# - appendonly.aof.manifest as a manifest file.

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

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

MANIFEST:用来跟踪、管理这些AOF。

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

所以,综上所诉,在redis7中,所有AOF文件的中,几种类型文件的名称前缀为appendfilename来定义。如,会存在appendonly.aof.1.base.rdbappendonly.aof.1.incr.aofappendonly.aof.2.incr.aofappendonly.aof.manifest

其实在redis7中,对于aof文件,只需要配置appenddirnameappendfilename即可。

所有配置完成后,重启Redis,进行测试。

1. 正常恢复

我们首先执行命令set m1 v1set m2 v2

可以看到在/myredis/appendonlydir目录下存在:

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

我们关闭redis,然后再重新开启,看数据是否可以正常恢复。

此时,会产生有个疑问,我们之前配置了RDB,这时候同时存在RDB和AOF,那么这两种方式会同时存在呢?还是会产生错误?

如果可以正常恢复,那么是由RDB恢复的,还是由AOF恢复的呢?后面我们会进行验证。

为了避遍RDB的影响,我们先将RDB文件删除,为了先验证AOF是否能恢复,然后重启Redis。重启后,执行keys *,发现m1m2依然存在,说明恢复成功。

image-20240512202342271

此时,我们不进行任何操作,我们复制一份appendonlydir目录,命名为appendonlydir.bak

此时,我们执行flushdb命令。然后关闭redis,同样的删除RDB文件,然后重启Redis,我们发现,redis中是空的,说明flushdb命令也相当于是写操作,会被记录下来。

flushdb是写操作!

然后,关闭redis,我们将appendonlydir删除,将appendonlydir.bak重命名为appendonlydir。然后重启redis,可以发现数据重新恢复为了appendonlydir所记录的版本。

此时,在appendonlydir目录下,三个文件如下,测试一下,当我们执行set k2 v2命令时,哪个文件会发生变化呢?

image-20240512203615156

显而易见,只有appendonly.aof.1.incr.aof文件的大小发生了改变。

image-20240512203707330

说明,在我们发生写操作时,是incr文件在真正记录我们的写操作行为。

2. 异常恢复

前面我们进行的案例全部都是AOF文件正常的情况。但是存在极端情况,可能在写入AOF文件时,可能内容刚写了一半,突然redis宕机了,这就会导致AOF文件错误。那么该如何修复AOF文件,最后完成数据的恢复呢?

故意乱写正常的AOF文件,模拟网络闪断文件写error。

我们编辑/myredis/appendonlydir/appendonly.aof.1.incr.aof文件,在文件最后随便写入一行,然后保存文件。

image-20240512204843493

此时,我们关闭redis,然后重启。

image-20240512205052080

我们可以发现,redis客户端无法连接redis服务器,说明redis服务器没有正常启动。说明AOF文件存在问题,redis服务器就无法正常启动!

此时,我们可以在/usr/local/bin目录下通过redis-check-aof --fix AOF文件名称命令修复AOF文件。

redis-check-aof --fix /myredis/appendonlydir/appendonly.aof.1.incr.aof

image-20240512205721539

此时,我们再重新启动redis。

image-20240512205816082

可以发现,redis客户端成功连接了redis服务器。

优点

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

缺点

  • 相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb
  • aof运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同
  • 对于相同的数据集,AOF 文件通常比等效的 RDB 文件大。
  • AOF 可能比 RDB 慢,具体取决于确切的 fsync 策略。一般来说,将 fsync 设置为每秒一次的性能仍然非常高,并且禁用 fsync 后,即使在高负载下,它也应该与 RDB 一样快。即使在巨大的写入负载的情况下,RDB仍然能够对最大延迟提供更多的保证。

​ Redis < 7.0时

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

3.3 AOF重写机制

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

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

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

触发机制

  • 自动触发
  • 手动触发

官方默认配置

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

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

  • 根据上次重写后的aof大小,判断当前aof大小是不是增长了100%,也就是1倍

  • 重写时满足的文件大小64mb

自动触发

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

手动触发

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

案例实验

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

比如有个key ,一开始我们set k1 v1,然后改成set k1 v2,最后改成set k1 v3

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

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

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

案例准备工作:

首先在配置文件中开启AOF

appendonly yes

在配置文件中,将重写峰值修改为1k,也就意味着,当aof文件增长1倍,且文件大小达到1k时,触发重写。

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

关闭混合持久化

aof-use-rdb-preamble no  # 默认为yes

删除之前的全部AOF和RDB文件,清楚干扰项

自动触发验证

完成上述配置,重启redis服务器,执行命令set k1 v1

查看三大AOF文件。

image-20240513100555124

image-20240513100523077

此时,我们在执行set k1 11111111111111111111111,多执行几次后。使得appendonly.aof.1.incr.aof文件达到1k。

image-20240513100825318

可以看到,在appendonly.aof.1.incr.aof文件达到了1k之后,触发了重写,base和incr的文件名称都发生了变化,从1变为了2。

再次查看,appendonly.aof.2.incr.aof,发现只保留了最小指令集。

image-20240513101112919

手动触发验证

我们再执行set k2 v2

然后执行bgrewriteaof命令手动触发重写。

bgrewriteaof
#Background append only file rewriting started

此时,我们再查看三个aof文件。

image-20240513102857019

可以发现,aof文件从2变为了3。

重写原理

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

AOF优化配置项

image-20240513173336172

3.4 RDB与AOF混合持久化

前面我们有讲到一个问题:RDB和AOF是否可以共存呢? 如果可以共存,那么恢复的时候是通过哪种方式进行恢复呢?

官方回答,RDB和AOF可以共存。如果AOF开启,Redis则会优先加载AOF

同时开启RDB和AOF时,只会加载AOF文件,不会加载RDB文件。

数据恢复顺序和加载流程

image-20240513173932425

建议同时开启RDB和AOF两种持久化方法,在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。RDB用于备份数据库(AOF在不断变化不好备份),留着rdb作为一个万一的手段。

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

RDB与AOF混合方式

1.开启混合方式设置

设置aof-use-rdb-preamble的值为yes

aof-use-rdb-preamble yes

2.RDB+AOF的混合方式

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

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

3.5 纯缓存模式

纯缓存模式就是不使用持久化,同时关闭AOF和RDB。有利于系统的提升。

禁用RDB,在配置文件中进行配置:

save ""

禁用rdb持久化模式下,我们仍然可以使用命令save、bgsave生成rdb文件

禁用AOF,在配置文件中进行配置:

appendonly no

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


4. Redis事务

数据库的事务:在一次和数据库的连接会话中,所有执行的SQL,要么一起成功,要么一起失败。

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

Redis事务的作用:一个队列中,一次性、顺序性、排他性的执行一系列命令。

性质说明
单独的隔离操作Redis的事务仅仅是保证事务里的操作会被连续独占的执行,redis命令执行是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的
没有隔离级别的概念因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这种问题了
不保证原子性Redis的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力
排它性Redis会保证一个事务内的命令依次执行,而不会被其它命令插入

常用命令

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

案例测试

1. 正常执行

顺序执行以下命令:multiset k1 v1set k2 v2set k3 v3exec

结果正常。

2. 放弃事务

先执行命令:set count 3

顺序执行以下命令:multiset k1 v11set k2 v22incr countdiscard

此时,执行get count,结果仍然为3。说明整个事务中所有命令全部没有生效。

3. 一条命令存在问题,全不生效

假设有n条命令,其中有一条命令出错了,那么整个事务全部都不会生效。

顺序执行以下命令:multiset k1 v111set k2 v222set k3exec

此时,会提示:EXECABORT Transaction discarded because of previous errors.

说明整个事务中的命令全部都没有生效了。因为存在错误命令set k3。此时再执行get k1命令,发现值还是原来的值。

4. 对的命令正确执行,错误的命令停止

当只有执行才能判断命令是错误时,这种情况下,错误的命令不生效,其他正确的命令依然生效。如,我们先执行命令:set test abc。此时只有执行了incr test命令后才会知道该命令是错误的,不像set k3这种命令,不执行也能知道命令是错误的。

所以我们先执行了set test abc

然后依次执行:multiset k1 v11set k2 v22set k3 v33incr countincr testexec

此时,我们再执行get k1,发现值已经更新为了v11。说明正确的命令依然生效。

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

5. watch监控

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

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

watch

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

执行命令:set k1 abcset balance 100watch balancemultiset k1 abc2set balance 110exec

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

为了测试命令加塞,我们需要在启动一个redis客户端2。

首先在客户端1中执行命令,set k1 abc3set balance 120watch balancemultiset k1 abc4

然后在客户端2中执行命令,set balance 150

然后继续在客户端1中执行命令set balance 200exec

此时,执行exec命令后,返回结果为null。此时我们再执行命令get k1,返回结果为abc3,说明值没有改变。执行命令get balance,返回结果为150,说明客户端1执行的命令没有生效。

如果watch监控的key的值被事务之外的其他命令所改变,那么该事务所有命令全部不生效。

unwatch

一旦执行了exec之前执行unwatch,那么监控锁都会被取消掉了。

但是,在执行unwatch之间,被监控的值已经被事务之外的其他命令所修改,也会导致事务的失败,即使unwatch是在exec之前。

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

小总结

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

5. Redis管道

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

问题由来

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

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

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

image-20240513215633195

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

解决方法

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

image-20240513215347321

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

总结:管道就是批处理命令变种优化措施,类似Redis的原生批命令(mget和mset)。

案例演示

我们新建一个cmd.txt文件

set k100 v100
set k200 v200
hset k300 name zhangsan
hset k300 gender male
lpush list 1 2 3 4 5

执行命令cat cmd.txt | redis-cli -a 密码 --pipe

[root@51f78296b791 myredis]# cat cmd.txt | redis-cli -a 密码 --pipe
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 5

这样,就通过管道将命令批量执行了。

Pipeline与原生批量命令对比

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

Pipeline与事务对比

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

使用Pipeline注意事项

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

6. 发布订阅

了解即可。

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

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

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

image-20240514091726796

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

image-20240514091808179

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

image-20240514092152127

常用命令

  • SUBSCRIBE channel [channel ...]:订阅给定的一个或多个频道的信息。

    推荐先执行订阅后再发布,订阅成功之前发布的消息是收不到的。

    订阅的客户端每次可以收到一个 3 个参数的消息:消息的种类、始发频道的名称、实际的消息内容。

  • PUBLISH channel message:发布消息到指定的频道。

  • PSUBSCRIBE pattern [pattern ...]:按照模式批量订阅,订阅一个或多个符合给定模式(支持*号?号之类的)的频道。

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

  • PUBSUB CHANNELS:由活跃频道组成的列表。

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

  • PUBSUB NUMPAT:只统计使用PSUBSCRIBE命令执行的,返回客户端订阅的唯一模式的数量。

  • UNSUBSCRIBE [channel [channel ...]]:取消订阅。

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

不建议使用,专业的事情交给专业的人来做,发布订阅交给消息中间件来做,如RabbitMQ、Kafka等。


7. Redis主从复制

7.1 介绍

主从复制,master以写为主,Slave以读为主。

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

image-20240514095144194

作用:

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

配从库,不配主库

权限细节

master如果配置了requirepass参数,需要密码登陆。那么slave就要配置masterauth来设置校验密码,否则的话master会拒绝slave的访问请求。

基本操作命令

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

7.2 案例实操

7.2.1 架构说明

一个主机Master,两个从机Slave。(使用3台虚拟机或者3个docker容器)

image-20240515104058888

拷贝多个redis.conf配置文件(redis6379.conf,redis6380.conf,redis6381.conf)

注意:三边网络ping通,注意防火墙配置。

修改配置文件细节(以redis6379.conf为例)

  • 开启后台运行,daemonize yes

  • 注释掉bind 127.0.0.1

  • protected-mode no

  • 指定端口port 6379

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

  • pid文件(进程文件)名字,pidfile /var/run/redis_6379.pid

  • log日志文件名字,logfile "/myredis/6379.log"

  • Redis密码设置requirepass 密码

  • RDB文件名称dbfilename dump6379.rdb

  • (非必选,可以不开启)aof文件,appendonly yes开启aof,appendfilename "appendonly.aof"AOF文件名称,appenddirname appendonlydirAOF文件目录名称

  • 从机访问主机的通行密码masterauth(从机需要配置,主机不用),masterauth 密码

注意:我们使用的是docker,我们在启动docker时进行了端口映射 本地端口:主机端口,因为在启动容器时,设置的主机端口全为6379,所以端口号不用改变,使用6379即可,在本地连接时,使用映射到本地的端口即可。

7.2.2 一仆二主

方案一:通过配置文件进行配置

在从库6380和6381的配置文件中配置

replicaof 主库IP 主库端口号
replicaof 172.17.0.2 6379

先启动主库,然后再启动两个从库。注意:指定我们刚刚配置的配置文件进行启动redis-server /xxx/xxx.conf

查看主库的日志6379.log,可以发现从库已经成功连上了

image-20240515152134609

在从机使用replication info命令查看主从关系。

# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_read_repl_offset:616
slave_repl_offset:616
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:d0ba3161ea132d101ef02629e40eb52c14c39ecf
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:616
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:616

在主机使用该命令进行查看

# Replication
role:master
connected_slaves:2
slave0:ip=172.17.0.3,port=6379,state=online,offset=1554,lag=1
slave1:ip=172.17.0.4,port=6379,state=online,offset=1554,lag=1
master_failover_state:no-failover
master_replid:d0ba3161ea132d101ef02629e40eb52c14c39ecf
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1554
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1554

在redis6379主机上进行一次写入操作set k1 v1

此时,在redis6380以及redis6381上执行get k1,同样可以成功获取。

这样一主二从就成功配置好了。

问题:

  • 从机可以执行写命令吗?

    答:不可以,READONLY You can’t write against a read only replica.

  • slave是从头开始复制还是从切入点开始复制?也就是主机启动,执行set k1 v1set k2 v2set k3 v3。那么,当主机执行完这些命令时,再启动从机,是否之前的数据也会复制过来吗?

    答:从头开始复制。之前的数据也会复制过来。首次是全量,后续是主机写一次,从机便跟随主机也写一次。

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

    答:从机数据不会丢,数据可以正常使用。但是不会上位。

  • 主机宕机后,重启后主从关系还在吗?从机还能否顺利复制?

    答:主机重启后,主从关系依然存在。从机依然可以顺利复制。

  • 某台从机宕机后,主机继续,从机重启后它能跟上大部队吗?

    答:从机重启后,会自动同步主机上的数据。

方案二:通过slaveof命令进行配置

首先,将从机关机,删除配置文件中的replicaof配置项,此时3个redis都是主机状态,各自独立。

注意:不能删除masterauth配置,不然会连接不上!当删除后,使用该命令连接时,在日志文件中可以看到MASTER aborted replication with an error: NOAUTH Authentication required.

在从机上执行命令slaveof 主库IP 主库端口,以此绑定主机。

如:

replicaof 172.17.0.2 6379

此时,在主机上使用info replication进行查看,可以看到已经有一个从机连上了。

# Replication
role:master
connected_slaves:1
slave0:ip=172.17.0.3,port=6379,state=online,offset=3844,lag=1
master_failover_state:no-failover
master_replid:14544813fe80d644be723228b35ce10d2eaf97c7
master_replid2:d0ba3161ea132d101ef02629e40eb52c14c39ecf
master_repl_offset:3844
second_repl_offset:3063
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:3063
repl_backlog_histlen:782

注意:使用该命令进行主从配置时,如果从机重启后,那么关系还在吗?

答:主机重启时,主从关系依然存在。但是,从机重启后,该从机的主从关系就不在了。

配置VS命令

  • 配置:持久稳定
  • 命令:当次生效,重启失效
7.2.3 薪火相传

上一个slave可以是下一个slave的master,slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master,可以有效减轻主master的写压力。如下图所示,如果很多从机都直接连到master主机上,那么会对主机的性能产生很大的影响。所以可以让从机可变为主机,然后连接其他从机。

image-20240515162608955

比如:redis02绑定主机redis01,redis03绑定从机redis02。此时redis02依然也是不能进行写操作的。

slaveof 新主库IP 新主库端口中途变更绑定的主机,会清除之前的数据,重新建立拷贝最新的。

7.2.4 反客为主

通过SLAVEOF no one命令,断开绑定的主机,执行该命令的redis,将成为独立的,由slave变为master。

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

7.3 原理及工作流程

  • slave启动,同步初请

    slave启动成功连接到master后会发送一个sync命令

    slave首次全新连接master,一次完全同步(全量复制)将被自动执行,slave自身原有数据会被master数据覆盖清除

  • 首次连接,全量复制

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

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

  • 心跳持续,保持通信

    master发出PING包的周期,默认是10秒。

    通过repl-ping-replica-period 10进行配置。

  • 进入平稳,增量复制

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

  • 从机下线,重连续传

    master会检查backlog里面的offset,master和slave都会保存一个复制的offset还有一个masterId,offset是保存在backlog中的。Master只会把已经复制的offset后面的数据复制给Slave。

7.4 缺点

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

master挂了如何办?

默认情况下,不会在slave节点中自动重选一个master。那每次都要人工干预?这样,无人值守安装变成刚需。


8. Redis哨兵监控

8.1 介绍

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

作用:

  • 监控redis运行状态,包括master和slave

  • 当maste宕机,能自动将slave切换成新master

image-20240515203450651

功能:

  • 主从监控:监控主从redis库运行是否正常
  • 消息通知:哨兵可以将故障转移的结果发送给客户端
  • 故障转移:如果Master异常,则会进行主从切换,将其中一个Slave作为新Master
  • 配置中心:客户端通过连接哨兵来获得当前Redis服务的主节点地址

8.2 案例实操

8.2.1 架构说明

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

1主2从:用于数据读取和存放

哨兵一定要配集群,否则就不要用,最少保证3个哨兵,防止一台机器宕机,哨兵选举失败。

哨兵数量为奇数,可以更好的进行投票选举。

image-20240515204636034

8.2.2 实操

前面我们启动了Redis01、Redis02、Redis03,这三台分别作为Master、Slave1和Slave2。端口分别为6379,6380,6381。

注意:哨兵的默认端口为26379。

此时,我们再启动三个Redis,分别为Redis04,Redis05,Redis06,分别作为Sentinel01,Sentinel02,Sentinel03。端口分别为26379,26380,26381。

根据前面所描述,配置好一主二从。将Redis01作为Master。

注意!!Redis01,也就是现在的Master主机,也要配置好masterauth,因为如果该主机宕机了,他可能就变为了从机,其他机器变为了主机,此时他就要去访问复制那个主机,所以也要配置好访问的密码才能正常访问。

在Redis04,Redis05,Redis06三台机器上面,进行如下配置(以Redis04为例):

  • 查看Redis目录下的sentinel.conf配置文件。

    之前提到过的进行配置,如:

    bind服务监听地址注释掉

    daemonize ye允许后台运行

    protected-mode no关闭安全保护模式

    port端口

    logfile /myredis/sentinel26379.log日志文件路径

    pidfile /var/run/redis-sentinel26379.pidpid文件路径

    dir /myredis工作目录

    此外,哨兵的相关配置,如

    sentinel monitor <master-name> <ip> <redis-port> <quorum>配置哨兵监控的master主机。quorum表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数。(客观下线)

    sentinel auth-pass <master-name> <password>配置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>:客户端重新配置主节点参数脚本。

  • 对于上述配置,我们直接新建一个sentinel26379.conf配置文件,进行配置

bind 0.0.0.0
daemonize yes
protected-mode no
port 26379
logfile "/myredis/sentinel26379.log"
pidfile /var/run/redis-sentinel26379.pid
dir /myredis
sentinel monitor mymaster 172.17.0.2 6379 2
sentinel auth-pass mymaster 密码

其他哨兵主机也按上述配置进行修改。

上诉配置中,sentinel monitor <master-name> <ip> <redis-port> <quorum>中的quorum代表什么意思呢?

quorum:确认客观下线的最少的哨兵数量。

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

完成上诉配置后,启动1主2从Redis01,Redis02,Redis03。

然后启动3个哨兵Redis04,Redis05,Redis06。

redis-server /myredis/sentinel26379.conf --sentinel
redis-server /myredis/sentinel26380.conf --sentinel
redis-server /myredis/sentinel26381.conf --sentinel
或
redis-sentinel /myredis/sentinel26379.conf
redis-sentinel /myredis/sentinel26380.conf
redis-sentinel /myredis/sentinel26381.conf

启动哨兵后,测试一次主从复制。一切正常。

此时,1主2从+3个哨兵,配置成功!

哨兵功能演示

我们自己手动关闭6379服务器,模拟master挂了。

当6379master主机宕机后,哨兵便开始选举。在哨兵的日志文件中,可以看到选举信息。

image-20240516134521706

  • 此时,两个从机数据是否依然存在且可用?

    答:数据依然存在且可用。

  • 是否会从两台从机上选出新的主机master?

    答:会从从机上选出新的主机master。在我们的测试中,redis02,6380成为了新的master主机。此时,6380可以进行写操作了。6381仍然是slave。但是6381绑定的主机变为了6380主机,且在配置文件中也会动态的改变,如redis03 6381的配置文件:(下图中的端口号并没有错误,因为我们使用的是docker,进行了端口映射,在docker中端口为6379,映射到本地为6380)

image-20240516134539088

  • 之前宕机的master机器重启回来,是会成为从机?还是继续是主机master呢?会不会有双master冲突?

    答:会变为从机,其配置文件也会动态的变化,来绑定redis02,也就是6380。

对比redis01宕机后,配置文件的变化

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

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

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

当在一开始,我们刚停掉Reids01,使用Redis02继续执行Get命令,获取数据时,可能出现broken pipe错误。再继续执行get命令又会恢复正常。

了解broken pipe

名称描述
认识broken pipepipe是管道的意思,管道里面是数据流,通常是从文件或网络套接字读取的数据。当该管道从另一端突然关闭时,会发生数据突然中断,即是broken,对于socket来说,可能是网络被拔出或另一端的进程崩溃
解决问题其实当该异常产生的时候,对于服务端来说,并没有多少影响。因为可能是某个客户端突然中止了进程导致了该错误
总结 Broken Pipe这个异常是客户端读取超时关闭了连接,这时候服务器端再向客户端已经断开的连接写数据时就发生了broken pipe异常!

8.3 哨兵流程及选举原理

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

运行流程,故障切换

  • 三个哨兵监控一主二从,正常运行

  • SDown主观下线(Subjectively Down)

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

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

  • ODown客观下线(Objectively Down)

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

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

    当主节点被判断客观下线以后,各个哨兵节点会进行协商,先选举出一个领导者哨兵节点(兵王)并由该领导者节点,也即被选举出的兵王进行failover(故障迁移)。那么哨兵领导者,兵王如何选出来的?是通过Raft算法

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

主观下线SDown

所谓主观下线(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)状态。

客观下线ODown

sentinel monitor <master-name> <ip> <redis-port> <quorum>四个参数含义:

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

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

选取领导者哨兵节点Leader:Raft算法

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

image-20240516135820230

由兵王开始推动故障切换流程并选出一个新master,三个步骤:

1.新主登基

  • 某个Slave被选中成为新Master

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

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

    如果上诉一样,再判断复制偏移位置offset最大的从节点。

    如果上诉一样,最后判断最小Run ID的从节点(字典顺序,ASCII码)。

    image-20240516140244276

2.群臣俯首

执行slaveof no one命令让选出来的从节点成为新的主节点,并通过slaveof命令让其他节点成为其从节点。

Sentinel leader会对选举出的新master执行slaveof no one操作,将其提升为master节点。

Sentinel leader向其它slave发送命令,让剩余的slave成为新的master节点的slave。

3.旧主拜服

将之前已下线的老master设置为新选出的新master的从节点,当老master重新上线后,它会成为新master的从节点。

Sentinel leader会让原来的master降级为slave并恢复正常工作。

总结:

  • 原master宕机
  • 哨兵判断主观下线
  • 哨兵通过投票,判断客观下线
  • 哨兵根据Raft算法选取领导者哨兵Leader
  • 领导者哨兵Leader发动Redis选举,根据优先权、偏移量、RUN ID选取新的Master,并设定slave。

8.4 哨兵使用建议

  • 哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用

    支持读写分离、支持数据的高可用、支持海量数据的读写存储操作

  • 哨兵节点的数量应该是奇数

  • 各个哨兵节点的配置应一致

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

  • 哨兵集群+主从复制,并不能保证数据零丢失,所以引出Redis集群


9. Redis集群分片

9.1 介绍

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

的一部分,这就是Redis的集群,其作用是提供在多个Redis节点间共享数据的程序集。

image-20240516221838619

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

Redis集群可以支持多个Master。

Redis集群作用

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

9.2 集群算法-分片-槽位

9.2.1 介绍

集群的槽位slot

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

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

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

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

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

image-20240518155200790

集群的分片

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

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

槽位与分片的优势

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

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

9.2.2 槽位映射的3种解决方案

1.哈希取余分区

image-20240518160424325

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

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

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

2.一致性哈希算法分区

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

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

3大步骤:

  • 算法构建一致性哈希环

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

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

    image-20240518160843846

  • redis服务器IP节点映射

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

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

    image-20240518160953693

  • 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-20240518161100306

优点:

  • 一致性哈希算法的容错性

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

    image-20240518161522393

  • 一致性哈希算法的扩展性

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

    image-20240518161407721

缺点:

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

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

    image-20240518161325700

小总结:

为了在节点数目发生改变时尽可能少的迁移数据,将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash后会顺时针找到临近的存储节点存放。而当有节点加入或退出时仅影响该节点在Hash环上顺时针相邻的后续节点。

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

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

3.哈希槽分区(建议使用)

为什么会出现?

答:因为一致性哈希算法的数据倾斜问题。

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

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

image-20240518161840583

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

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

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

哈希槽计算

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

9.2.3 经典面试题

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

答:

说明一

正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。

这意味着它们包含原始节点的插槽配置,该节点使用2k的空间和16k的插槽,但是会使用8k的空间(使用65k的插槽)。

同时,由于其他设计折衷,Redis集群不太可能扩展到1000个以上的主节点。

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

说明二(人话)

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

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

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

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

  • redis的集群主节点数量基本不可能超过1000个。(因为太多的Redis主节点可能会导致数据失真)

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

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

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

9.2.4 额外说明

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

比如客户端将输入写入到Redis01,此时Redis01要将输入给从机Redis02,但是!此时,数据刚写入到Redis01,还没来得及讲输入写入到从机,Redis01宕机了,那么此时就会造成了Redis02丢失数据。

9.3 案例实操

9.3.1 集群配置

删除之前Docker中的Redis容器。

重新新建6台Redis机器。Redis01,Redis02,Redis03,Redis04,Redis05,Redis06。映射到本地的端口分别为6379,6380,6381,6382,6383,6384。并在每台Redis机器上进行目录/myredis/cluster

image-20240518180448805

注意:

上诉图,只是一个理想的设计,比如这里我们指定Master01点从机是Slave01,但是Master01点从机到底是哪台,是由集群其内部分配的。

修改配置文件

以Redis01为例。

/myredis/cluster下新建文件redisCluster6379.conf文件。

bind 0.0.0.0
daemonize yes
protected-mode no
port 6381
logfile "/myredis/cluster/cluster6379.log"
pidfile /myredis/cluster6379.pid
dir /myredis/cluster
dbfilename dump6379.rdb
appendonly yes
appendfilename "appendonly6379.aof"
requirepass 密码
masterauth 密码
# 集群配置
cluster-enabled yes   # 开启集群
cluster-config-file nodes-6379.conf   # 集群的配置文件,自动生成,无需自己创建
cluster-node-timeout 5000  # 集群的超时时间 5s

其他5台也按照上诉进行配置,注意需要修改的地方。

启动6台Redis实例

redis-server /myredis/cluster/redisCluster6379.conf
...

任选一个Redis机器,通过redis-cli命令为6台机器构建集群关系

redis-cli -a 密码 --cluster create --cluster-replicas 1 172.17.0.2:6379 172.17.0.3:6379 172.17.0.4:6379 172.17.0.5:6379 172.17.0.6:6379 172.17.0.7:6379

# --cluster-replicas 1  表示为每个master创建一个slave节点

注意:此处端口我们之所以都写的为6379,因为在docker中redis运行的实际端口我们写的是6379,只是将它的端口映射到本地才变为6379,6380,6381……

执行该命令后,可以看到提示我们Master[0],Master[1],Master[2],并给出了它们对应的槽位,分别为0-5460,5461-10922,10923-16383。并指出172.17.0.6172.017.0.2的从机,即Redis05为Redis01的从机,此外,Redis06为Redis02的从机,Redis04为Redis03的从机。

image-20240518202934221

出现如下提示,便是配置成功。

image-20240518203142301

此时,我们查看\myredis\cluster下的文件。可以看到产生了集群节点的配置文件。

image-20240518203431752

链接进入6379作为切入点,查看并检验集群状态

进入redis客户端。

使用info replication命令查看主从信息。

image-20240518204353618

使用cluster info查看集群信息。

image-20240518204546952

使用cluster nodes查看集群节点。

image-20240518204516622

9.3.2 集群读写

对6379新增两个key,看看效果如何。

image-20240518204951430

可以看到,显示error,报错了!但是它显示MOVED 12706 172.17.0.4:6379,槽位为12706,这个IP对应的是Redis03。我们尝试在Redis03中执行set k1 v1,显示执行成功了。

这是为什么呢?

因为,一定要注意槽位的范围区间,需要路由到对应的槽位。

如何解决?

为了防止路由失效,我们需要在启动redis客户端时加参数-c,即redis-cli -a 密码 -p 端口 -c

我们重启redis01的客户端,然后再执行set k1 v1命令。

image-20240518205748395

可以发现,执行成功了。我们可以理解它帮我我们进行了重定向。

9.3.3 主从容错切换迁移

容错切换迁移

主机Reids01:6379宕机,对应的从机Redis05:6383上位。

Redis停止之前的节点信息:

image-20240519093939563

先停止Redis01,查看从机是否成功上位。

image-20240519094109647

可以看到,在Redis01停止后,且Redis01还没有重启时,Redis05成功上位成为master,但是此时它没有从机。此时整体结构为3主2从。

此时,重启Redis01,查看情况。

image-20240519094700254

可以看到,当Redis01重启重新回来时,它成为了Redis05的从机。

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

手动故障转移,节点从属调整

上面Redis01与Redis05主从关系互换了,和我们原始的设计图不一致了,该如何处理?

重新登陆Redis01机器。进行节点从属关系的调整,调整为Redis01为master,Redis05为slave。

使用命令CLUSTER FAILOVER。该命令为集群的故障调换。

image-20240519100005545

执行该命令后,可以看到,Redis01重新成为了Master,而Redis05成为了Redis01的从机。

当集群中一个主机挂掉了,且该主机下的所有从机也都挂了,那么会有其他主机的从机来顶替这个主机的位置。

9.3.4 主从扩容

当3主3从不够用时,我们将其扩容至4主4从。新增主机Master04,与从机Slave04。端口号分别指定6385,6386。

此时,我们新建两个Redis机器,分别为Redis07:6385和Redis08:6386。并按照上诉进行集群的配置,然后启动。

此时,这两个Redis节点均为Master,且未加入至集群中。

将新增的6385节点(空槽号)作为master节点加入原集群。

在Redis07中执行,redis-cli -a 密码 --cluster add-node 新加入节点的IP地址:端口号 原来节点的IP地址:端口号

如:redis-cli -a 密码 --cluster add-node 172.17.0.8:6379 172.17.0.2:6379

注意:此处端口号都写为了6379,因为它在docker实际的端口我们设置的是6379,只是将其映射到本地时端口变为6385。

172.17.0.8就是将要作为master新增节点,

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

如果当我们实际执行该命令时,出现了错误!

[ERR] Not all 16384 slots are covered by nodes.

问题原因:这个往往是由于主node移除了,但是并没有移除node上面的slot,从而导致了slot总数没有达到16384,其实也就是slots分布不正确。所以在删除节点的时候一定要注意删除的是否是Master主节点。

通过查询找到了解决方案:

使用redis-cli -a 密码 --cluster fix 172.17.0.2:6379来修复集群。

修复完成后再用check命令检查下是否正确,redis-cli -a 密码 --cluster check 172.17.0.2:6379

如果分布不均匀那可以使用下面的方式重新分配slot,redis-cli -a 密码 --cluster reshard 172.17.0.2:6379

然后重新执行加入集群的命令。发现执行成功。

image-20240519103938503

执行redis-cli -a 密码 --cluster check 172.17.0.2:6379检查集群节点情况。

image-20240519105118510

可以发现,新加入的这个Redis节点还没有分配槽位。

通过redis-cli -a 密码 --cluster reshard IP地址:端口号重新分配槽位。如:redis-cli -a 密码 --cluster reshard 172.17.0.2:6379

image-20240519105639805

执行该命令后,询问我们想要移动多少槽位。此时,我们填入4096即可。(16386/4=4096)。

输入并回车后,会询问我们这些分配出来的槽位移动至哪个节点,输入节点号。

image-20240519105920995

此时,我们输入新节点Redis07的节点号c3418ca6b4fe5c4f00434b87f6069becf972d993即可。此时,又会提示我们:

image-20240519110309665

在这时,我们输入all,并回车即可。在这个过程中会有提示Do you want to proceed with the proposed reshard plan (yes/no)? ,此时输入yes即可。

执行完毕后,此时我们再使用命令redis-cli -a 密码 --cluster check 172.17.0.2:6379来查看节点信息。

image-20240519110517183

可以看到,槽位已经分配成功了。

但是,如下图,我们可以看出,新节点Redis07的槽位并不是连续的,而之前原来的节点槽位都是连续的,这是为什么呢?

image-20240519110740861

这是因为,在给Redis07分配槽位时,并不是将集群中所有槽位打破,重新给每个节点分配槽位。而是将集群中原来节点的槽位各自匀出来一些给新的节点。所以这三个槽位区间来自原来3个集群中的节点。

为主节点6385分配从节点6386

通过命令:redis-cli -a 密码 --cluster add-node 新slave主机的IP:新slave端口 新master主机的IP:新master端口 --cluster-slave --cluster-master-id 新主机节点ID进行分配

如:redis-cli -a 密码 --cluster add-node 172.17.0.9:6379 172.17.0.8:6379 --cluster-slave --cluster-master-id c3418ca6b4fe5c4f00434b87f6069becf972d993

执行该命令后,出现如下提示,说明执行成功。

image-20240519111441734

通过redis-cli -a 密码 --cluster check 172.17.0.2:6379命令,查看节点情况。

image-20240519111651986

可以看到,每个主机都被分配了一个从机。

至此,4主4从就配置好了。

9.3.5 主从缩容

当前我们为4主4从,我们想从4主4从缩容到3主3从,该如何操作呢?

主要步骤:

  • 先清除从节点
  • 从主节点中清出来的槽号分配给其他主节点
  • 再删除主节点
  • 恢复成3主3从

目的:从集群中删除主节点Redis07/172.17.0.8:6385和从节点Redis08/172.17.0.9:6386

详细步骤:

步骤1 查询从节点ID

使用命令redis-cli -a 密码 --cluster check 172.17.0.9:6379查询集群情况,获取从节点Redis08的节点ID为b7594734c85ddcbbef103b06e63dec0c35161160

步骤2 删除从节点

使用命令redis-cli -a 密码 --cluster del-node ip:端口 从集群中所指定的Redis。如redis-cli -a 密码 --cluster del-node 172.17.0.9:6379 b7594734c85ddcbbef103b06e63dec0c35161160,执行该命令后,使用redis-cli -a 密码 --cluster check 172.17.0.2:6379查询集群情况。

image-20240519135229779

发现,主机Redis07的从机Redis08已经清除了。

步骤3 重新分配主节点槽位

为了清除主节点Redis07,首先将Redis07的槽位重新分配给Redis01(在此案例中,我们是将槽位直接全部给了Redis01,可以有其他方案)。通过redis-cli -a 密码 --cluster reshard IP:端口命令进行槽位的重新划分,如redis-cli -a 密码 --cluster reshard 172.17.0.2:6379

image-20240519135834910

因为Redis07的槽位只有4096,将Redis07的所有槽位重新分配给其他节点,所以全部就是4096,这里输入4096即可。提示输入接收该槽位的机器的ID时,写入Redis01的ID即可01e7b393f31fe9b4f04ecb3e8bcac69b8ca8ccfe。然后会询问槽位的来源,我们输入Redis07的ID即可,然后再输入一个done

image-20240519140446451

后面,会询问是否接受该访问,输入yes继续执行即可完成。

再次通过redis-cli -a 密码 --cluster check 172.17.0.2:6379命令查询集群情况。

image-20240519141005655

可以看到Redis07的槽位全部给了Redis01了。且Redis07从Master变成了一个Slave,挂在了主机Redis01下面。

步骤4 删除主节点

使用命令redis-cli -a 密码 --cluster del-node ip:端口 节点ID删除节点。如:redis-cli -a 密码 --cluster del-node 172.17.0.8:6379 c3418ca6b4fe5c4f00434b87f6069becf972d993。执行该命令后,再次查询集群信息。

image-20240519141416909

从4主4从已经变为了3主3从,成功完成了缩容。

9.4 集群常用命令

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

image-20240519142911652

如何解决呢?

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

image-20240519143134925

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

常用命令

  • cluster-require-full-coverage:在配置文件中进行配置,集群是否完整才能对外提供服务

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

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

  • CLUSTER COUNTKEYSINSLOT 槽位数字编号

    1:槽位被占用,0:槽位没被占用

  • CLUSTER KEYSLOT 键名称:该键应该存在哪个槽位上


10. SpringBoot整合Redis

10.1 总体概述

image-20240519163236685

jedis,lettuce,RedisTemplate三者的联系

  • Jedis是最初代的,最老牌的,是Redis官网推荐的一个面向java客户端,库文件实现了对各类API进行封装调用。爆出来一些问题,线程池不安全等问题。

  • Lettuce是一个Redis的Java驱动包,可以理解为是对Jedis本身的一个优化。

  • RedisTemplate又是Lettuce的升级。(推荐使用)

10.2 本地连接Redis常见问题

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

10.3 集成Jedis

集成步骤:

1. 新建SpringBoot项目,redis-study

2. 改POM文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

3. 写YAML配置文件

spring:
  application:
    name: redis-study

server:
  port: 8080

4. 业务类编写

public class JedisDemo {
    public static void main(String[] args) {
        // 1. 通过IP和端口好,获得Connection
        Jedis jedis = new Jedis("localhost", 6387);
        // 2. 指定访问Redis服务的密码
        jedis.auth("密码");
        // 3. 获得了Jedis客户端,可以像JDBC一样访问Redis服务了
        System.out.println(jedis.ping());  // 返回PONG,说明连接成功

        // 相当于执行命令 Keys *
        Set<String> keys = jedis.keys("*");
        System.out.println(keys);

        // 尝试5中常用数据类型
        // string
        jedis.set("k2","spring");
        String k2 = jedis.get("k2");
        System.out.println(k2);  // spring
        // list
        jedis.lpush("mylist","l1","l2","l3","l4","l5","l6");
        List<String> mylist = jedis.lrange("mylist", 0, -1);
        System.out.println(mylist);  // [l6, l5, l4, l3, l2, l1]
        // hash
        Map<String,String> map = new HashMap<>();
        map.put("name","zhangsan");
        map.put("gender","man");
        jedis.hset("myhash",map);
        Map<String, String> hashMap = jedis.hgetAll("myhash");
        System.out.println(hashMap);  // {gender=man, name=zhangsan}
        // set
        jedis.sadd("myset","1","1","2","3","4");
        Set<String> myset = jedis.smembers("myset");
        System.out.println(myset);  // [1, 2, 3, 4]
        // zset
        jedis.zadd("myzset",2,"5");
        jedis.zadd("myzset",3,"8");
        jedis.zadd("myzset",1,"4");
        List<String> myzset = jedis.zrange("myzset", 0, -1);
        List<String> myzset1 = jedis.zrevrange("myzset", 0, -1);
        System.out.println(myzset);  // [4, 5, 8]
        System.out.println(myzset1);  // [8, 5, 4]

        // 设置过期时间
        jedis.expire("myset",3600);
        // 获取过期时间
        long myttl = jedis.ttl("myset");
        System.out.println(myttl);  // 3600
    }
}

10.4 集成lettuce

Jedis 是直连 redis server,会有线程安全问题。除非使用连接池,为每个 Jedis实例增加物理连接。

Lettuce是基于Netty的,连接实例可以在多个线程间并发访问,Lettuce还支持异步连接方式,提高网络等待和磁盘IO效率。

集成步骤:

1. 修改POM文件

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

2. 编写业务类

public class LettuceDemo {
    public static void main(String[] args) {
        // 1. 使用构建器,链式编程来build RedisURI
        RedisURI uri = RedisURI.builder().redis("localhost", 6387)
                        .withAuthentication("default","密码")
                        .build();
        // 2. 创建连接客户端
        RedisClient redisClient = RedisClient.create(uri);
        StatefulRedisConnection<String, String> connection = redisClient.connect();
        // 3. 通过connection创建操作的command
        // sync表示同步
        RedisCommands<String, String> commands = connection.sync();

        // 4. 业务操作
        // 获取所有的key
        List<String> keys = commands.keys("*");
        System.out.println(keys);

        // string
        commands.set("lkey","ltest");
        String lkey = commands.get("lkey");
        System.out.println(lkey);
        // list
        commands.lpush("llist","1","a","b");
        List<String> llist = commands.lrange("llist", 0, -1);
        System.out.println(llist);
        // hash
        Map<String,String> map = new HashMap<>();
        map.put("class","grade-1");
        map.put("num","20");
        commands.hset("classinfo",map);
        Map<String, String> classinfo = commands.hgetall("classinfo");
        System.out.println(classinfo);
        // set
        commands.sadd("lset","1","a","b");
        Set<String> lset = commands.smembers("lset");
        System.out.println(lset);
        // zset
        commands.zadd("lzset",1,"a");
        commands.zadd("lzset",4,"c");
        commands.zadd("lzset",3,"b");
        List<String> lzset = commands.zrange("lzset", 0, -1);
        List<String> lzset1 = commands.zrevrange("lzset", 0, -1);
        System.out.println(lzset);
        System.out.println(lzset1);

        // 设置过期时间
        commands.expire("classinfo",3600);
        Long classinfo1 = commands.ttl("classinfo");
        System.out.println(classinfo1);

        // 5. 关闭释放资源
        connection.close();
        redisClient.close();
    }
}

10.5 集成RedisTemplate

10.5.1 连接单机

1. 修改POM文件,新增依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.2.5</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<!--swagger3-->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.2.0</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2. 修改yaml配置文件

spring:
  application:
    name: redis-study

  data:
    redis:
      database: 0
      host: localhost
      port: 6387
      password: 密码
      lettuce:
        pool:
          max-active: 8
          max-wait: -1ms
          max-idle: 8
          min-idle: 0

server:
  port: 8080

# 日志相关配置
logging:
  level:
    root: info
    cn:
      codewei: info

3. 编写代码

配置类-RedisConfig

@Configuration
public class RedisConfig {

}

配置类-SwaggerConfig

@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI docsOpenApi()
    {
        return new OpenAPI()
                .info(new Info().title("redisStudy")
                        .description("通用设计rest")
                        .version("v1.0"))
                .externalDocs(new ExternalDocumentation()
                        .description("www.codewei.cn")
                        .url("https://yiyan.baidu.com/"));
    }
}

Service

@Service
@Slf4j
public class OrderService {
    @Resource
    private RedisTemplate redisTemplate;

    private static final String ORDER_PREFIX = "order:";

    public void addOrder() {
        int keyId = ThreadLocalRandom.current().nextInt(1000)+1;
        String serialNo = UUID.randomUUID().toString();

        String key = ORDER_PREFIX + keyId;
        String value = "京东订单" + serialNo;

        redisTemplate.opsForValue().set(key,value);
        log.info("key:{},value:{}",key,value);
    }
    public String getOrderById(Integer orderId) {
        return String.valueOf(redisTemplate.opsForValue().get(ORDER_PREFIX + orderId));
    }
}

Controller

@RestController
@Slf4j
@Tag(name = "订单接口")
public class OrderController {
    @Resource
    private OrderService orderService;

    @Operation(summary = "新增订单接口")
    @RequestMapping(value = "/order/addOrder",method = RequestMethod.POST)
    public String addOrder(){
        orderService.addOrder();
        return "ok";
    }

    @Operation(summary = "获取订单接口")
    @RequestMapping(value = "/order/getOrder/{id}",method = RequestMethod.GET)
    public String getOrder(@PathVariable("id") Integer id){
        String orderById = orderService.getOrderById(id);
        return orderById;
    }
}

4. 测试

访问http://localhost:8080/swagger-ui/index.html进入到swagger页面。

通过swagger访问addOrder,进行测试。

发送请求后,后台日志输入:key:order:26,value:京东订单29f79fee-4da8-4e4d-8308-2af91b93d071

此时,我们访问getOrder,携带上面返回来的26作为参数。成功访问,返回结果京东订单29f79fee-4da8-4e4d-8308-2af91b93d071

但是,我们在Redis客户端中查看到,其key并不是order:26

image-20240519212513738

这是由Redis序列还问题引起的。

键和值都是通过Spring提供的Serializer序列化到数据库的。RedisTemplate默认使用的是org.springframework.data.redis.serializer.JdkSerializationRedisSerializer 。StringRedisTemplate默认使用的是StringRedisSerializer。Key被序列化成上图这样,线上通过key去查对应的value非常不方便。

解决方案一

使用StringRedisTemplate来替换我们之前使用的RedisTemplate

解决方案二

在RedisConfig中进行配置

@Configuration
public class RedisConfig {
    /**
     * redis序列化的工具配置类,下面这个请一定开启配置
     * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
     * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法
     * this.redisTemplate.opsForSet(); //提供了操作set的所有方法
     * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法
     * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法
     */
    @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;
    }
}

此时,我们重启项目,重新执行addOrder进行测试。

image-20240519214215563

发现key已经成功被存入了,没有发生乱码。

10.5.2 连接集群

1. 修改redis配置

因为我们使用的是Docker,通过本地映射的端口,redis可能无法找到服务,所以我们需要在redis中增加如下配置

cluster-announce-ip 192.168.1.105   # 本地的IP
cluster-announce-port 6379   # 映射到本地的端口 这个参数用于指定 Redis 集群节点对外提供服务的端口号。
cluster-announce-bus-port 6379  # redis的端口 这个参数用于指定 Redis 集群节点之间通信所使用的端口号。

2. 启动集群6台Redis实例

image-20240520095355122

2. 编写yaml文件

spring:
  application:
    name: redis-study

  data:
    redis:
#      database: 0
#      host: localhost
#      port: 6387
      password: 密码
      # --------- 集群配置 ----------
      cluster:
        max-redirects: 3 # 获取失败 最大重定向次数
        nodes: localhost:6379,localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384
      # ----------------------------
      lettuce:
        pool:
          max-active: 8
          max-wait: -1ms
          max-idle: 8
          min-idle: 0

server:
  port: 8080

logging:
  level:
    root: info
    cn:
      codewei: info

3. 此时,启动项目,业务类无需更改,直接通过微服务访问Redis集群

访问http://localhost:8080/swagger-ui/index.html进入到swagger页面进行测试。

两次请求addOrder接口进行测试。访问接口后,正常访问,后台日志打印key:order:817,value:京东订单6996cd22-8b1f-4a42-b9ca-1c836cf7b280key:order:518,value:京东订单4693250f-b71b-4e89-8b71-d180fc25ee87

image-20240520105220842

在Redis中可以正常取到存储的数据。并且我们可以看到两次的数据存放到了不通的Redis主机中。

4. 模拟master:6379宕机

先对Redis集群以命令方式进行验证读写命令,看从机6383是否成功上位。

经过测试,可以看到6383成功上位。读写命令正常。

但是,我们通过Swagger进行测试的时候,无法正常访问Redis了。

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

image-20240520111036222

image-20240520111042321

导致原因:Redis默认的连接池采用 Lettuce,当Redis 集群节点发生变化后,Letture默认是不会刷新节点拓扑。

解决方案:

1.在maven引入的spring-boot-starter-data-redis包中排除lettuce,并且引入采用jedis依赖(不推荐)

2.重写连接工厂实例(极度不推荐)

@Bean
public DefaultClientResources lettuceClientResources() {
    return DefaultClientResources.create();
}
@Bean
public LettuceConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties, ClientResources clientResources) {
    ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
            .enablePeriodicRefresh(Duration.ofSeconds(30)) //按照周期刷新拓扑
            .enableAllAdaptiveRefreshTriggers() //根据事件刷新拓扑
            .build();
    ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
            //redis命令超时时间,超时后才会使用新的拓扑信息重新建立连接
            .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(10)))
            .topologyRefreshOptions(topologyRefreshOptions)
            .build();
    LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
            .clientResources(clientResources)
            .clientOptions(clusterClientOptions)
            .build();
    RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
    clusterConfig.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
    clusterConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
    LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(clusterConfig, clientConfiguration);
    return lettuceConnectionFactory;

}

3.刷新节点集群拓扑动态感应,在yaml中增加如下配置

#支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
spring.redis.lettuce.cluster.refresh.adaptive=true
#定时刷新
spring.redis.lettuce.cluster.refresh.period=2000
spring.redis.cluster.nodes=localhost:6379,localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384
```# Redis7 学习笔记

![image-20240418195131769](https://image0219.oss-cn-hangzhou.aliyuncs.com/images/image-20240418195131769.png)

---

## 1. 简介

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

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

中文文档:https://www.redisio.com/

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

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

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

Redis能做什么?

- 分布式缓存

  ![image-20240422133437554](https://image0219.oss-cn-hangzhou.aliyuncs.com/images/image-20240422133437554.png)

- 内存存储和持久化(RDB+AOF),redis支持异步将内存中的数据写到硬盘上,同时不影响继续服务。

- 高可用架构搭配(单机、主从、哨兵、集群)

- 缓存穿透、击穿、雪崩

- 分布式锁

- 队列

- 排行榜+点赞等

![image-20240422132247602](https://image0219.oss-cn-hangzhou.aliyuncs.com/images/image-20240422132247602.png)

优势:

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

![image-20240422154003050](https://image0219.oss-cn-hangzhou.aliyuncs.com/images/image-20240422154003050.png)

Redis版本号第二位如果是奇数,则为非稳定版本,如2.7、2.9等。如果第二位为偶数,则为稳定版本。

**Redis新特性**

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

![image-20240422154854655](https://image0219.oss-cn-hangzhou.aliyuncs.com/images/image-20240422154854655.png)

- Redis Functions

![image-20240422155118245](https://image0219.oss-cn-hangzhou.aliyuncs.com/images/image-20240422155118245.png)

- Client-eviction

![image-20240422155212134](https://image0219.oss-cn-hangzhou.aliyuncs.com/images/image-20240422155212134.png)

- multi-part AOF

![](https://image0219.oss-cn-hangzhou.aliyuncs.com/images/image-20240422155249766.png)

- ACL v2

​	![image-20240422155321281](https://image0219.oss-cn-hangzhou.aliyuncs.com/images/image-20240422155332944.png)

- listpack

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

- 新增命令

  新增ZMPOP,BZMPOP,LMPOP,BLMPOP等新命令,对于EXPIRE和SET命令,新增了更多的命令参数选项。例如,ZMPOP的格式如下:`ZMPOP numkeys key [key ...] MIN|MAX [COUNT count]`,而BZMPOP是ZMPOP的阻塞版本。

> 在Linux环境下安装Redis必须先具备gcc编译环境
>
> 查看gcc:`gcc -v`
>
> 如果没有,则执行命令`yum -y install gcc-c++`进行安装

**安装步骤**

- 下载获得redis-7.2.4.tar.gz后将它放入我们的Linux目录/opt
- /opt目录下解压redis
- 进入redis目录
- 在redis-7.2.4目录下执行make命令,`make && make install`
- 查看默认安装目录:/usr/local/bin
  - redis-benchmark:性能测试工具,服务启动后运行该命令,看性能怎么样
  - redis-check-aof:修复有问题的AOF文件
  - redis-check-dump:修复有问题的dump.rdb文件
  - redis-cli:客户端,操作入口
  - redis-sentinel:redis集群使用
  - redis-server:redis服务器启动命令

- 将默认的redis.conf拷贝到自己定义好的一个路径下,比如/myredis/redis7.conf
  - 配置文件,改完后确保生效,记得重启
  - 默认`daemonize no`改为`daemonize yes`,后台启动
  - 默认`protected-mode yes`改为`protected-mod no`
  - 默认`bind 127.0.0.1`直接注释掉
  - 添加redis密码,`requirepass xxx`

- 修改/myredis目录下redis.conf配置文件做初始化设置
- 启动服务,`redis-server /opt/redis-7.2.4/myredis-redis7.conf`
- 连接服务,`redis-cli -a xxx -p 6379`

`quit`退出redis-cli客户端。

关闭redis-server服务器

- 单实例关闭:`redis-cli -a xxx shutdown`
- 多实例关闭,指定端口关闭:`redis-cli -p 6379 shutdown`
- 在redis-cli内部执行`shutdown`命令关闭服务器,然后执行`quit`退出客户端

**卸载步骤**

- 停止redis服务
- 删除`/usr/local/bin`目录下与redis相关的文件

---

## 2. Redis十大类型

### 2.1 总体概述

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

![image-20240507125009561](https://image0219.oss-cn-hangzhou.aliyuncs.com/images/image-20240507125009561.png)

命令查询手册:

- https://redis.io/commands/
- https://www.redis.net.cn/order/

### 2.2 key操作命令

- `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`:通杀全部库

### 2.3 大小写和帮助命令

> 命令不区分大小写,但是Key是区分大小写的

帮助命令查询:

```shell
help @string
help @list
help @hash
...

2.4 string字符串类型

string是redis最基本的类型,一个key对应一个value。

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

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

单值单value。

set key value语法

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

参数:

  • EX –-设置指定的过期时间,以秒为单位(正整数)。
  • PX milliseconds – 设置指定的过期时间,以毫秒为单位(正整数)。
  • EXAT timestamp-seconds – 设置密钥过期的指定 Unix 时间,以秒为单位(正整数)。
  • PXAT timestamp-milliseconds – 设置密钥过期的指定 Unix 时间,以毫秒为单位(正整数)。
  • NX– 仅当密钥尚不存在时才设置该密钥。
  • XX– 仅当密钥已存在时才设置该密钥。
  • KEEPTTL– 保留与密钥关联的生存时间。
  • GET– 返回存储在 key 处的旧字符串,如果 key 不存在则返回 nil。SET如果存储在 key 中的值不是字符串,则会返回错误并中止。

get key获取值

同时设置/获取多个键值

MSET key value [key value ...]
MGET key [key ...]
mset/mget/msetnx

获取指定区间范围内的值

getrange/setrange

类似于java中的substring,如

set k1 helloworld

getrange k1 0 4
# hello

setrange k1 1 123
get k1
# h123oworld

数值增减

递增数值

INCR key

增加指定的整数

INCRBY key 3  # 设置步长为3

递减数值

DECR key

减少指定的整数

DECRBY key 3   # 设置步长为3

获取字符串长度和内容追加

STRLEN key   # 获取字符串长度

APPEND key value    # 内容追加

分布式锁

setnx key value

setex(set with expire)键秒值/setnx(set if not exist)

setex:设置带过期时间的key,动态设置。

setex 键 秒值 真实值

setnx:只有在 key 不存在时设置 key 的值。

image-20240507141700985

getset(先get再set)

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

2.5 list列表类型

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

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

单key多value,有序有重复

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

left、right都可以插入添加;

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

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

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

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

常用命令

  • lpush/rpush/lrange
  • lpop/rpop
  • lindex:按照索引下标获得元素(从上到下)
  • llen:获取列表中元素的个数
  • lrem key 数字N 给定值v1:删除N个值等于v1的元素
  • ltrim key 开始index 结束index:截取指定范围的值后再赋值给key
  • rpoplpush 源列表 目的列表
  • lset key index value
  • linsert key before/after 已有值 插入新值

如:

lpush l1 1 2 3 4 5

rpush  l1 1 2 3

lrange l1 0 -1
# 5 4 3 2 1 1 2 3

2.6 hash哈希表类型

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

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

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

常用命令

  • hset/hget/hmset/hmget/hgetall/hdel
  • hlen
  • hexists key 在key里面的某个值的key
  • hkeys/hvals
  • hincrby/hincrbyfloat,增数值增加,小数值增加
  • hsetnx,不存在则新建赋值,存在则不生效

如:

hset user:001 id 11 name codewei age 24 gender 男
hget user:001 id
hgetall user:001

hincrby user:001 age 1

2.7 set集合类型

单值多value,无序无重复

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

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

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

常用命令

  • SADD key member [member ...]:添加元素

  • SMEMBERS key:遍历集合中所有的元素

  • SISMEMBERS key member:判断元素是否在集合中

  • SREM key member [member ...]:删除元素

  • scard:获取集合里面的元素个数

  • SRANDMEMBER key [数字]:从集合中随机展现设置的数字个数元素,元素不删除

  • SPOP key [数字]:从集合中随机弹出一个元素,出一个删一个

  • smove key1 key2 在key1里已存在的某个值:将key1里面已存在的某个值赋值给key2

  • 集合运算

    • A、B

    • 差集运算A-BSDIFF key [key ...]

    • 并集运算A∪BSUNION key [key ...]

    • 交集运算A∩B

      SINTER key [key ...]

      SINTERCARD numkeys key [key ...] [LIMIT limit],它不返回结果集,而返回结果的基数,返回由所有给定集合的交集产生的集合的基数。

如:

sadd s1 1 2 3 1 2 4 5 6

smembers s1
# 1 2 3 4 5 6 会自动去重

SINTER a1 a2   # a 1 2
sintercard 2 a1 a2  # 3

2.8 zset有序集合类型

有序,无重复

Redis zset(Sorted set) 和 set 一样也是string类型元素的集合,且不允许重复的成员。

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

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

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

之前set是k1 v1 v2 v3,现在zset是,k1 score1 v1 score2 v2。

常用操作

  • 向有序集合中加入一个元素和该元素的分数
  • ZADD key score member [score member ...]:添加元素
  • ZRANGE key start stop [WITHSCORES]:按照元素分数从小到大的顺序,返回索引从start到stop之间的所有元素
  • zrevrange:逆序遍历输出
  • ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]:获取指定分数范围的元素,(不包含,limit 作用是返回限制, limit 开始下标步 多少步
  • ZSCORE key member:获取元素的分数
  • ZCARD key:获取集合中元素的数量
  • zrem key 某score下对应的value值:作用是删除元素
  • ZINCRBY key increment member:增加某个元素的分数
  • ZCOUNT key min max:获得指定分数范围内的元素个数
  • ZMPOP:从键名列表中的第一个非空排序集中弹出一个或多个元素,它们是成员分数对
  • zrank key values:获得下标值
  • zrevrank key values:逆序获得下标值

2.9 bitmap位图类型

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

image-20240514093909379

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

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

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

位图可以用于状态统计。

常用命令

image-20240508091847185

  • setbit key offset value

    setbit键,偏移位,只能0或1

    bitmap的偏移量是从0开始算的

  • getbit key offset

  • strlen

    统计字节数占用多少

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

  • bitcount

    统计全部键里面有多少个1

  • bitop

    连续2天都签到的用户

    加入某个网站或者系统,它的用户有1000W,做个用户id和位置的映射

    比如0号位对应用户id:uid-092iok-lkj

    比如1号位对应用户id:uid-7388c-xxx

    image-20240508093937191

如:

setbit b1 0 1
setbit b1 5 1

getbit b1 0   # 1
getbit b1 1   # 0
bitcount b1 0 10  # 2

# bitop演示
hset uid:map 0 uid-092iok-lkj
hset uid:map 1 uid-7388c-xxx

setbit 0508 1 1

setbit 0509 0 1
setbit 0509 1 1

setbit 0510 0 1
setbit 0510 1 1

setbit 0511 0 1
setbit 0511 1 1

# 统计0508和0509都签到的人数
bitop and k 0508 0509
bitcount k   # 1
# 统计0509和0510都签到的人数
bitop and k 0509 0510
bitcount k   # 2
# 统计连续4天都签到的人数
bitop and k 0508 0509 0510 0511
bitcount k  # 1

2.10 HyperLoglog基数统计类型

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

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

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

可以用于统计网站或者文章的UV。UV:Unique Visitor,独立访客,一般理解为客户端IP。需要考虑去重。

可以用于用户搜索网站关键词的数量统计

统计用户每天搜索不同词条的个数

去重统计功能的基数估计算法就是HyperLoglog

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

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

常用命令

  • PFADD key element [element ...]:添加指定元素到HyperLoglog中。
  • PFCOUNT key [key ...]:返回给定HyperLoglog的基数估算值。
  • PFMERGE destkey sourcekey [sourcekey ...]:将多个HyperLoglog和并为一个HyperLoglog。

如:

PFADD webview:0508 129.211.75.68 127.22.71.66 129.28.73.43
PFCOUNT webview:0508 # 3

PFADD webview:0509 129.211.75.68 127.27.89.73 129.59.27.32
PFADD webview:0510 129.57.32.12 127.75.54.87 129.28.73.43

PFMERGE webview:3 webview:0508 webview:0509 webview:0510
PFCOUNT webview:3  # 7

2.11 GEO地理空间类型

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

  • 添加地理位置的坐标。

  • 获取地理位置的坐标。

  • 计算两个位置之间的距离。

根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。

GEO的底层数据结构为zset

地球上的地理位置是使用二维的经纬度表示,经度范围 (-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.精准度的问题,我们知道地球不是平面坐标系,而是一个圆球,这种矩形计算在长距离计算时会有很大误差

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

主要分为3步:

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

image-20240508140817502

常用命令

  • GEOADD 多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中

    GEOADD key longitude latitude member [longitude latitude member ...]

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

    GEOPOS key member

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

    GEODIST key member member m[km|ft|mi]

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

    GEORADIUS key longitude latitude num m[km|ft|mi] [withdist] [withcoord] [count num] [withhash] [desc]

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

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

    count num:限定返回的记录数

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

  • GEORADIUSBYMEMBER 跟GEORADIUS类似

    GEORADIUS key member num m[km|ft|mi] [withdist] [withcoord] [count num] [withhash] [desc]

  • GEOHASH返回一个或多个位置元素的 Geohash 表示

    GEOHASH key member [member ...]

    geohash算法生成的base32编码值

    3维变2维变1维

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

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

如:

GEOADD city 116.418067 39.886373 天坛 117.022878 36.665813 趵突泉

GEOPOS city 天坛

GEODIST city 天坛 趵突泉 km  # 362.08

GEORADIUS city 117.0324 36.680518 10 km withdist withcoord  count 10 withhash desc
# 趵突泉 1.8431 {"latitude": 36.66581248952775, "longitude": 117.02287763357162} 4065929170479734

GEORADIUSBYMEMBER city 天坛 1000 km  # 趵突泉 天安门

2.12 Stream流类型

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

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

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

Redis消息队列的两种方案:

  • List实现消息队列,其实就是点对点的模式,对于一对多力不从心

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

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

    image-20240514093857124

  • Pub/Sub

    image-20240514093852207

Stream流类型就是Redis版的MQ消息中间件+阻塞队列

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

Stream结构

image-20240514093842793

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

名称介绍
Message Content消息内容
Consumer group消费组,通过XGROUP CREATE 命令创建,同一个消费组可以有多个消费者
Last_delivered_id游标,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。
Consumer消费者,消费组中的消费者
Pending_ids消费者会有一个状态变量,用于记录被当前消费已读取但未ack的消息Id,如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack它就开始减少。这个pending_ids变量在Redis官方被称之为 PEL(Pending Entries List),记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符),它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理

ACK:签收确认

队列相关指令

  • XADD:添加消息到队列末尾

    消息ID必须要比上个ID大,默认用*自动生成规矩,*用于在XADD命令中,让系统自动生成id,像是mysql中的自增主键

    XADD key *|id field value [field value ...]

  • XTRIM:限制Stream的长度,如果已经超长会进行截取

    MAXLEN是允许的最大长度,对流进行修剪限制长度

    MINID为允许的最小ID,从某个ID值开始比该ID值小的将会被抛弃

    XTRIM key MAXLEN num

    XTRIM key MINID messageid

  • XDEL:删除消息

    XDEL key messageid

  • XLEN:获取Stream中的消息长度

    XLEN key

  • XRANGE:获取消息列表(可以指定范围),忽略删除的消息

    XRANGE key start end [COUNT count]-表示最小值,+表示最大值

  • XREVRANGE:和XRANGE相比区别在于反向获取,ID从大到小

    XREVRANGE key end start [COUNT count]

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

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

    COUNT表示最多读取多少条消息,BLOCK是否以阻塞的方式读取消息,默认不阻塞,如果milliseconds设置为0,表示永远阻塞

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

    0-0表示从最小的ID开始获取消息,当不指定count,将会返回stream中所有的消息,也可以使用0,或者00,或者000

如:

xadd mystream * id 1001 cname 张三
xadd mystream * id 1002 cname 李四
xadd mystream * id 1003 cname 王五  # 1715218524218-0  消息ID

xrange mystream - + count 2
# 1715218507959-0 张三 1001
# 1715218522389-0 李四 1002
xrange mystream 1715218507960 + count 2 
# 1715218522389-0 李四 1002
# 1715218524218-0 王五 1003

xdel mystream 1715218522389-0

xlen mystream # 2

xtrim mystream MAXLEN 1  # 截取messgeid最大的1个消息,其他消息抛弃
xtrim mystream minid 1715219718393  # 抛弃掉比 1715219718393 messgeid小的消息

xread count 2 streams mystream $  # nil
xread count 2 streams mystream 0-0
# [{"id": "1715219718393-0", "fields": {"cname": "王五", "id": "1003"}}, {"id": "1715219719939-0", "fields": {"cname": "张三", "id": "1001"}}]

# 读取消息 - 阻塞
xread count 1 block 0 streams mystream $
# 此时该redis客户端在监听最新的消息,然后我们新开启一个redis客户端,再新redis客户端中向该stream中插入一条消息
xadd mystream * id 1006 cname 七七
# 此时,我们之前的客户端就会监听到该消息
# 1) 1) "mystream"
#   2) 1) 1) "1715221058147-0"
#         2) 1) "id"
#            2) "1006"
#            3) "cname"
#            4) "七七"
# (17.94s)

生成的消息ID:1715218524218-0,-前表示毫秒时间戳,-后表示在这个时间戳下的第几条消息

消费组相关指令

  • XGROUP CREATE:创建消费者组

    XGROUP CREATE key groupName 0|$

    0:表示从stream头部开始消费

    $:表示从stream尾部开始消费

  • XREADGROUP GROUP:读取消费者组中的消息

    XREADGROUP group groupName consumer [COUNT count] [BLOCK milliseconds] [NOACK] streams key [key ...] id [id ...] >

    “>”,表示从第一条尚未被消费的消息开始读取

    注意:不同消费组的消费者可以消费同一条消息

    stream中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费者内其他消费者读取了,即同一个消费组里的消费者不能消费同一条消息。

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

  • XACK:ack消息,消息被标记为“已处理”

    XACK key gropuName messageId

  • XGROUP SETID:设置消费组组最后递送消息的ID

  • XGROUP DELCONSUMER:删除消费组组

  • XPENDING:打印待处理消息的详细信息

    XPENDING key groupName

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

    XPENDING key groupName - + count consumer

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

  • XCLAIM:转移消息的归属权(长期未被处理/无法处理的消息,转交给其他消费组组进行处理)

  • XINFO:打印Stream\Consumer\Group的详细信息

    • XINFO GROUPS:打印消费者组的详细信息
    • XINFO STREAM key:打印Stream的详细信息

重点问题

问题基于 Stream 实现的消息队列,如何保证消费者在发生故障或宕机再次重启后,仍然可以读取未处理完的消息?
1Streams 会自动使用内部队列(也称为 PENDING List)留存消费组里每个消费者读取的消息保底措施,直到消费者使用 XACK 命令通知 Streams“消息已经处理完成”。
2消费确认增加了消息的可靠性,一般在业务处理完成之后,需要执行 XACK 命令确认消息已经被消费完成

image-20240509131504075

如:

xgroup create mystream groupA $
xgroup create mystream groupB 0

xreadgroup group groupA consumer1 streams mystream > 
# [{"id": "1715231921890-0", "fields": {"cname": "丹丹", "id": "1006"}}, {"id": "1715231923752-0", "fields": {"cname": "溜溜", "id": "1007"}}]
xreadgroup group groupB consumer1 streams mystream >
# [{"id": "1715231921890-0", "fields": {"cname": "丹丹", "id": "1006"}}, {"id": "1715231923752-0", "fields": {"cname": "溜溜", "id": "1007"}}]
xreadgroup group groupB consumer2 streams mystream >
# nil

# 让组内的多个消费者共同分担读取消息,所以,我们通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的
xgroup create mystream groupC 0
xreadgroup group groupC consumer1 count 1 streams mystream >
xreadgroup group groupC consumer2 count 1 streams mystream >
xreadgroup group groupC consumer3 count 1 streams mystream >

xpending mystream groupA consumer1
# 2 1715231921890-0 1715231923752-0 {"consumer1": 2}
xpending mystream groupA - + 10 consumer1
# 1715231921890-0 consumer1 957752 1
# 1715231923752-0 consumer1 957752 1

xack mystream groupA 1715231921890-0

四个特殊符号

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

2.13 bitfield位域类型

了解即可。

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

说白了就是通过bitfield命令我们可以一次性对多个比特位域进行操作。

作用:

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

常用命令

  • BITFIELD key [GET type offset]
  • BITFIELD key [SET type offset value]
  • BITFIELD key [INCRBY type offset increment]
  • 溢出控制OVERFLOW [WRAP|SAT|FAIL]

3. Redis持久化

将内存中的数据写入到硬盘中。

image-20240509143538010

3.1 RDB

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

在指定的时间间隔,执行数据集的时间点快照。

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

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

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

Rdb保存的是dump.rdb文件

image-20240509144035817

redis6与redis7 RDB配置文件对比

redis7

# Unless specified otherwise, by default Redis will save the DB:
#   * After 3600 seconds (an hour) if at least 1 change was performed
#   * After 300 seconds (5 minutes) if at least 100 changes were performed
#   * After 60 seconds if at least 10000 changes were performed

redis6

#   In the example below the behavior will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
3.1.1 自动触发

按照redis.conf里配置的save <seconds> <changes>

本次案例:

触发间隔时长5秒钟以上且有2次修改就触发RDB

redis.conf中修改(如果使用了自己的自定义配置文件,则在自定义配置文件中修改,如我们使用myredis.conf)

save 5 2

修改dump文件的保存路径

dir /myredis/dumpfiles

注意:我们指定的这个文件夹不能不存在,需要我们手动创建好,mkdir -p /myredis/dumpfiles

修改dump文件的文件名称

dbfilename dump6379.rdb

修改配置文件后,重启redis-server。

验证我们的配置是否正确且是否已经生效,在redis客户端执行命令:

CONFIG GET dir  # /myredis/dumpfiles

触发备份

image-20240509152947406

目前,dumpfiles目录下文件为空。

我们执行如下命令后

set k1 v1
set k2 v2

再次查看目录下文件,发现备份文件已经存在

image-20240509153119356

如何恢复?

注意:

  • 执行flushdbflushall也会产生dump.rdb文件,但是里面是空的,无意义。
  • 当我们执行shutdown命令时,也会生成dump.rdb文件保存一次快照。

将备份文件dump.rdb移动到redis配置的dumpfiles目录并启动服务即可。

注意不要让flushdb、flushall和shutdown等命令产生的rdb文件覆盖我们生成的rdb文件!注意备份迁移!

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

3.1.2 手动触发

我们可以使用SAVE或者BGSAVE命令手动触发数据备份。

SAVE

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

image-20240509165149985

BGSAVE(默认)

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

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

fork是什么?

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

image-20240509165327466

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

优点

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

缺点

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

RDB总结

  • 适合大规模的数据恢复
  • 按照业务定时备份
  • 对数据完整性和一致性要求不高
  • RDB 文件在内存中的加载速度要比 AOF 快得多
  • 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失从当前至最近一次快照期间的数据,快照之间的数据会丢失。
  • 内存数据的全量同步,如果数据量太大会导致I/0严重影响服务器性能
  • RDB依赖于主进程的fork,在更大的数据集中,这可能会导致服务请求的瞬间延迟。fork的时候内存中的数据被克隆了一份,大致2倍的膨胀性,需要考虑。

RDB文件修复命令

redis-check-rdb 文件路径

哪些情况下会产生RDB快照?

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

RDB快照禁用

  • 方法一:动态所有停止RDB保存规则的方法:redis-cli config set save "”
  • 方法二(推荐):在配置文件中修改,save ""

RDB优化参数

  • 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,此选项是禁用的。

3.2 AOF

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

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

AOF保存的是appendonly.aof文件。

AOF持久化工作流程

image-20240512192117947

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

AOF缓冲区三种写回策略

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

image-20240512192416696

redis6到redis7,AOF功能发生了很大的变化。

开启AOF:在redis的配置文件中,进行配置

appendonly yes  # 默认为no

配置写回策略

# appendfsync always
appendfsync everysec   # 默认为everysec
# appendfsync no

AOF文件保存路径

  • redis6中,AOF保存文件的位置和RDB保存文件的位置一样,都是通过redis.conf配置文件的 dir 配置。
  • redis7中,新增了一个配置项appenddirname,最终AOF的文件保存在dir配置路径加上appendirname之后的路径。

如:

dir /myredis/dumpfiles
appenddirname "appendonlydir"

那么,AOF文件就会存储在/myredis/dumpfiles/appendonlydir目录下。

我们之前配置的dir/myredis/dumpfiles,因为配置的路径中有/dumpfiles,此时我们在该目录下再存储aof文件就不是很合适。所以,我们在此处将dir修改为/myredis。最终配置为:

dir /myredis
appenddirname "appendonlydir"

AOF文件名称

  • redis6时,通过appendfilename进行配置,只有一个AOF文件,所以只有一个文件名称。
appendfilename appendonly.aof
  • redis7时,引入了新特性Multi Part AOF。存在三个文件:base基本文件、incr增量文件和manifest清单文件。
# - appendonly.aof.1.base.rdb as a base file.
# - appendonly.aof.1.incr.aof, appendonly.aof.2.incr.aof as incremental files.
# - appendonly.aof.manifest as a manifest file.

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

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

MANIFEST:用来跟踪、管理这些AOF。

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

所以,综上所诉,在redis7中,所有AOF文件的中,几种类型文件的名称前缀为appendfilename来定义。如,会存在appendonly.aof.1.base.rdbappendonly.aof.1.incr.aofappendonly.aof.2.incr.aofappendonly.aof.manifest

其实在redis7中,对于aof文件,只需要配置appenddirnameappendfilename即可。

所有配置完成后,重启Redis,进行测试。

1. 正常恢复

我们首先执行命令set m1 v1set m2 v2

可以看到在/myredis/appendonlydir目录下存在:

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

我们关闭redis,然后再重新开启,看数据是否可以正常恢复。

此时,会产生有个疑问,我们之前配置了RDB,这时候同时存在RDB和AOF,那么这两种方式会同时存在呢?还是会产生错误?

如果可以正常恢复,那么是由RDB恢复的,还是由AOF恢复的呢?后面我们会进行验证。

为了避遍RDB的影响,我们先将RDB文件删除,为了先验证AOF是否能恢复,然后重启Redis。重启后,执行keys *,发现m1m2依然存在,说明恢复成功。

image-20240512202342271

此时,我们不进行任何操作,我们复制一份appendonlydir目录,命名为appendonlydir.bak

此时,我们执行flushdb命令。然后关闭redis,同样的删除RDB文件,然后重启Redis,我们发现,redis中是空的,说明flushdb命令也相当于是写操作,会被记录下来。

flushdb是写操作!

然后,关闭redis,我们将appendonlydir删除,将appendonlydir.bak重命名为appendonlydir。然后重启redis,可以发现数据重新恢复为了appendonlydir所记录的版本。

此时,在appendonlydir目录下,三个文件如下,测试一下,当我们执行set k2 v2命令时,哪个文件会发生变化呢?

image-20240512203615156

显而易见,只有appendonly.aof.1.incr.aof文件的大小发生了改变。

image-20240512203707330

说明,在我们发生写操作时,是incr文件在真正记录我们的写操作行为。

2. 异常恢复

前面我们进行的案例全部都是AOF文件正常的情况。但是存在极端情况,可能在写入AOF文件时,可能内容刚写了一半,突然redis宕机了,这就会导致AOF文件错误。那么该如何修复AOF文件,最后完成数据的恢复呢?

故意乱写正常的AOF文件,模拟网络闪断文件写error。

我们编辑/myredis/appendonlydir/appendonly.aof.1.incr.aof文件,在文件最后随便写入一行,然后保存文件。

image-20240512204843493

此时,我们关闭redis,然后重启。

image-20240512205052080

我们可以发现,redis客户端无法连接redis服务器,说明redis服务器没有正常启动。说明AOF文件存在问题,redis服务器就无法正常启动!

此时,我们可以在/usr/local/bin目录下通过redis-check-aof --fix AOF文件名称命令修复AOF文件。

redis-check-aof --fix /myredis/appendonlydir/appendonly.aof.1.incr.aof

image-20240512205721539

此时,我们再重新启动redis。

image-20240512205816082

可以发现,redis客户端成功连接了redis服务器。

优点

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

缺点

  • 相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb
  • aof运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同
  • 对于相同的数据集,AOF 文件通常比等效的 RDB 文件大。
  • AOF 可能比 RDB 慢,具体取决于确切的 fsync 策略。一般来说,将 fsync 设置为每秒一次的性能仍然非常高,并且禁用 fsync 后,即使在高负载下,它也应该与 RDB 一样快。即使在巨大的写入负载的情况下,RDB仍然能够对最大延迟提供更多的保证。

​ Redis < 7.0时

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

3.3 AOF重写机制

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

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

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

触发机制

  • 自动触发
  • 手动触发

官方默认配置

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

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

  • 根据上次重写后的aof大小,判断当前aof大小是不是增长了100%,也就是1倍

  • 重写时满足的文件大小64mb

自动触发

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

手动触发

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

案例实验

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

比如有个key ,一开始我们set k1 v1,然后改成set k1 v2,最后改成set k1 v3

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

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

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

案例准备工作:

首先在配置文件中开启AOF

appendonly yes

在配置文件中,将重写峰值修改为1k,也就意味着,当aof文件增长1倍,且文件大小达到1k时,触发重写。

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

关闭混合持久化

aof-use-rdb-preamble no  # 默认为yes

删除之前的全部AOF和RDB文件,清楚干扰项

自动触发验证

完成上述配置,重启redis服务器,执行命令set k1 v1

查看三大AOF文件。

image-20240513100555124

image-20240513100523077

此时,我们在执行set k1 11111111111111111111111,多执行几次后。使得appendonly.aof.1.incr.aof文件达到1k。

image-20240513100825318

可以看到,在appendonly.aof.1.incr.aof文件达到了1k之后,触发了重写,base和incr的文件名称都发生了变化,从1变为了2。

再次查看,appendonly.aof.2.incr.aof,发现只保留了最小指令集。

image-20240513101112919

手动触发验证

我们再执行set k2 v2

然后执行bgrewriteaof命令手动触发重写。

bgrewriteaof
#Background append only file rewriting started

此时,我们再查看三个aof文件。

image-20240513102857019

可以发现,aof文件从2变为了3。

重写原理

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

AOF优化配置项

image-20240513173336172

3.4 RDB与AOF混合持久化

前面我们有讲到一个问题:RDB和AOF是否可以共存呢? 如果可以共存,那么恢复的时候是通过哪种方式进行恢复呢?

官方回答,RDB和AOF可以共存。如果AOF开启,Redis则会优先加载AOF

同时开启RDB和AOF时,只会加载AOF文件,不会加载RDB文件。

数据恢复顺序和加载流程

image-20240513173932425

建议同时开启RDB和AOF两种持久化方法,在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整。RDB用于备份数据库(AOF在不断变化不好备份),留着rdb作为一个万一的手段。

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

RDB与AOF混合方式

1.开启混合方式设置

设置aof-use-rdb-preamble的值为yes

aof-use-rdb-preamble yes

2.RDB+AOF的混合方式

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

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

3.5 纯缓存模式

纯缓存模式就是不使用持久化,同时关闭AOF和RDB。有利于系统的提升。

禁用RDB,在配置文件中进行配置:

save ""

禁用rdb持久化模式下,我们仍然可以使用命令save、bgsave生成rdb文件

禁用AOF,在配置文件中进行配置:

appendonly no

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


4. Redis事务

数据库的事务:在一次和数据库的连接会话中,所有执行的SQL,要么一起成功,要么一起失败。

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

Redis事务的作用:一个队列中,一次性、顺序性、排他性的执行一系列命令。

性质说明
单独的隔离操作Redis的事务仅仅是保证事务里的操作会被连续独占的执行,redis命令执行是单线程架构,在执行完事务内所有指令前是不可能再去同时执行其他客户端的请求的
没有隔离级别的概念因为事务提交前任何指令都不会被实际执行,也就不存在”事务内的查询要看到事务里的更新,在事务外查询不能看到”这种问题了
不保证原子性Redis的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力
排它性Redis会保证一个事务内的命令依次执行,而不会被其它命令插入

常用命令

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

案例测试

1. 正常执行

顺序执行以下命令:multiset k1 v1set k2 v2set k3 v3exec

结果正常。

2. 放弃事务

先执行命令:set count 3

顺序执行以下命令:multiset k1 v11set k2 v22incr countdiscard

此时,执行get count,结果仍然为3。说明整个事务中所有命令全部没有生效。

3. 一条命令存在问题,全不生效

假设有n条命令,其中有一条命令出错了,那么整个事务全部都不会生效。

顺序执行以下命令:multiset k1 v111set k2 v222set k3exec

此时,会提示:EXECABORT Transaction discarded because of previous errors.

说明整个事务中的命令全部都没有生效了。因为存在错误命令set k3。此时再执行get k1命令,发现值还是原来的值。

4. 对的命令正确执行,错误的命令停止

当只有执行才能判断命令是错误时,这种情况下,错误的命令不生效,其他正确的命令依然生效。如,我们先执行命令:set test abc。此时只有执行了incr test命令后才会知道该命令是错误的,不像set k3这种命令,不执行也能知道命令是错误的。

所以我们先执行了set test abc

然后依次执行:multiset k1 v11set k2 v22set k3 v33incr countincr testexec

此时,我们再执行get k1,发现值已经更新为了v11。说明正确的命令依然生效。

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

5. watch监控

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

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

watch

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

执行命令:set k1 abcset balance 100watch balancemultiset k1 abc2set balance 110exec

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

为了测试命令加塞,我们需要在启动一个redis客户端2。

首先在客户端1中执行命令,set k1 abc3set balance 120watch balancemultiset k1 abc4

然后在客户端2中执行命令,set balance 150

然后继续在客户端1中执行命令set balance 200exec

此时,执行exec命令后,返回结果为null。此时我们再执行命令get k1,返回结果为abc3,说明值没有改变。执行命令get balance,返回结果为150,说明客户端1执行的命令没有生效。

如果watch监控的key的值被事务之外的其他命令所改变,那么该事务所有命令全部不生效。

unwatch

一旦执行了exec之前执行unwatch,那么监控锁都会被取消掉了。

但是,在执行unwatch之间,被监控的值已经被事务之外的其他命令所修改,也会导致事务的失败,即使unwatch是在exec之前。

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

小总结

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

5. Redis管道

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

问题由来

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

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

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

image-20240513215633195

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

解决方法

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

image-20240513215347321

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

总结:管道就是批处理命令变种优化措施,类似Redis的原生批命令(mget和mset)。

案例演示

我们新建一个cmd.txt文件

set k100 v100
set k200 v200
hset k300 name zhangsan
hset k300 gender male
lpush list 1 2 3 4 5

执行命令cat cmd.txt | redis-cli -a 密码 --pipe

[root@51f78296b791 myredis]# cat cmd.txt | redis-cli -a 密码 --pipe
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
All data transferred. Waiting for the last reply...
Last reply received from server.
errors: 0, replies: 5

这样,就通过管道将命令批量执行了。

Pipeline与原生批量命令对比

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

Pipeline与事务对比

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

使用Pipeline注意事项

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

6. 发布订阅

了解即可。

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

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

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

image-20240514091726796

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

image-20240514091808179

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

image-20240514092152127

常用命令

  • SUBSCRIBE channel [channel ...]:订阅给定的一个或多个频道的信息。

    推荐先执行订阅后再发布,订阅成功之前发布的消息是收不到的。

    订阅的客户端每次可以收到一个 3 个参数的消息:消息的种类、始发频道的名称、实际的消息内容。

  • PUBLISH channel message:发布消息到指定的频道。

  • PSUBSCRIBE pattern [pattern ...]:按照模式批量订阅,订阅一个或多个符合给定模式(支持*号?号之类的)的频道。

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

  • PUBSUB CHANNELS:由活跃频道组成的列表。

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

  • PUBSUB NUMPAT:只统计使用PSUBSCRIBE命令执行的,返回客户端订阅的唯一模式的数量。

  • UNSUBSCRIBE [channel [channel ...]]:取消订阅。

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

不建议使用,专业的事情交给专业的人来做,发布订阅交给消息中间件来做,如RabbitMQ、Kafka等。


7. Redis主从复制

7.1 介绍

主从复制,master以写为主,Slave以读为主。

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

image-20240514095144194

作用:

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

配从库,不配主库

权限细节

master如果配置了requirepass参数,需要密码登陆。那么slave就要配置masterauth来设置校验密码,否则的话master会拒绝slave的访问请求。

基本操作命令

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

7.2 案例实操

7.2.1 架构说明

一个主机Master,两个从机Slave。(使用3台虚拟机或者3个docker容器)

image-20240515104058888

拷贝多个redis.conf配置文件(redis6379.conf,redis6380.conf,redis6381.conf)

注意:三边网络ping通,注意防火墙配置。

修改配置文件细节(以redis6379.conf为例)

  • 开启后台运行,daemonize yes

  • 注释掉bind 127.0.0.1

  • protected-mode no

  • 指定端口port 6379

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

  • pid文件(进程文件)名字,pidfile /var/run/redis_6379.pid

  • log日志文件名字,logfile "/myredis/6379.log"

  • Redis密码设置requirepass 密码

  • RDB文件名称dbfilename dump6379.rdb

  • (非必选,可以不开启)aof文件,appendonly yes开启aof,appendfilename "appendonly.aof"AOF文件名称,appenddirname appendonlydirAOF文件目录名称

  • 从机访问主机的通行密码masterauth(从机需要配置,主机不用),masterauth 密码

注意:我们使用的是docker,我们在启动docker时进行了端口映射 本地端口:主机端口,因为在启动容器时,设置的主机端口全为6379,所以端口号不用改变,使用6379即可,在本地连接时,使用映射到本地的端口即可。

7.2.2 一仆二主

方案一:通过配置文件进行配置

在从库6380和6381的配置文件中配置

replicaof 主库IP 主库端口号
replicaof 172.17.0.2 6379

先启动主库,然后再启动两个从库。注意:指定我们刚刚配置的配置文件进行启动redis-server /xxx/xxx.conf

查看主库的日志6379.log,可以发现从库已经成功连上了

image-20240515152134609

在从机使用replication info命令查看主从关系。

# Replication
role:slave
master_host:172.17.0.2
master_port:6379
master_link_status:up
master_last_io_seconds_ago:8
master_sync_in_progress:0
slave_read_repl_offset:616
slave_repl_offset:616
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:d0ba3161ea132d101ef02629e40eb52c14c39ecf
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:616
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:616

在主机使用该命令进行查看

# Replication
role:master
connected_slaves:2
slave0:ip=172.17.0.3,port=6379,state=online,offset=1554,lag=1
slave1:ip=172.17.0.4,port=6379,state=online,offset=1554,lag=1
master_failover_state:no-failover
master_replid:d0ba3161ea132d101ef02629e40eb52c14c39ecf
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:1554
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:1554

在redis6379主机上进行一次写入操作set k1 v1

此时,在redis6380以及redis6381上执行get k1,同样可以成功获取。

这样一主二从就成功配置好了。

问题:

  • 从机可以执行写命令吗?

    答:不可以,READONLY You can’t write against a read only replica.

  • slave是从头开始复制还是从切入点开始复制?也就是主机启动,执行set k1 v1set k2 v2set k3 v3。那么,当主机执行完这些命令时,再启动从机,是否之前的数据也会复制过来吗?

    答:从头开始复制。之前的数据也会复制过来。首次是全量,后续是主机写一次,从机便跟随主机也写一次。

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

    答:从机数据不会丢,数据可以正常使用。但是不会上位。

  • 主机宕机后,重启后主从关系还在吗?从机还能否顺利复制?

    答:主机重启后,主从关系依然存在。从机依然可以顺利复制。

  • 某台从机宕机后,主机继续,从机重启后它能跟上大部队吗?

    答:从机重启后,会自动同步主机上的数据。

方案二:通过slaveof命令进行配置

首先,将从机关机,删除配置文件中的replicaof配置项,此时3个redis都是主机状态,各自独立。

注意:不能删除masterauth配置,不然会连接不上!当删除后,使用该命令连接时,在日志文件中可以看到MASTER aborted replication with an error: NOAUTH Authentication required.

在从机上执行命令slaveof 主库IP 主库端口,以此绑定主机。

如:

replicaof 172.17.0.2 6379

此时,在主机上使用info replication进行查看,可以看到已经有一个从机连上了。

# Replication
role:master
connected_slaves:1
slave0:ip=172.17.0.3,port=6379,state=online,offset=3844,lag=1
master_failover_state:no-failover
master_replid:14544813fe80d644be723228b35ce10d2eaf97c7
master_replid2:d0ba3161ea132d101ef02629e40eb52c14c39ecf
master_repl_offset:3844
second_repl_offset:3063
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:3063
repl_backlog_histlen:782

注意:使用该命令进行主从配置时,如果从机重启后,那么关系还在吗?

答:主机重启时,主从关系依然存在。但是,从机重启后,该从机的主从关系就不在了。

配置VS命令

  • 配置:持久稳定
  • 命令:当次生效,重启失效
7.2.3 薪火相传

上一个slave可以是下一个slave的master,slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master,可以有效减轻主master的写压力。如下图所示,如果很多从机都直接连到master主机上,那么会对主机的性能产生很大的影响。所以可以让从机可变为主机,然后连接其他从机。

image-20240515162608955

比如:redis02绑定主机redis01,redis03绑定从机redis02。此时redis02依然也是不能进行写操作的。

slaveof 新主库IP 新主库端口中途变更绑定的主机,会清除之前的数据,重新建立拷贝最新的。

7.2.4 反客为主

通过SLAVEOF no one命令,断开绑定的主机,执行该命令的redis,将成为独立的,由slave变为master。

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

7.3 原理及工作流程

  • slave启动,同步初请

    slave启动成功连接到master后会发送一个sync命令

    slave首次全新连接master,一次完全同步(全量复制)将被自动执行,slave自身原有数据会被master数据覆盖清除

  • 首次连接,全量复制

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

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

  • 心跳持续,保持通信

    master发出PING包的周期,默认是10秒。

    通过repl-ping-replica-period 10进行配置。

  • 进入平稳,增量复制

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

  • 从机下线,重连续传

    master会检查backlog里面的offset,master和slave都会保存一个复制的offset还有一个masterId,offset是保存在backlog中的。Master只会把已经复制的offset后面的数据复制给Slave。

7.4 缺点

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

master挂了如何办?

默认情况下,不会在slave节点中自动重选一个master。那每次都要人工干预?这样,无人值守安装变成刚需。


8. Redis哨兵监控

8.1 介绍

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

作用:

  • 监控redis运行状态,包括master和slave

  • 当maste宕机,能自动将slave切换成新master

image-20240515203450651

功能:

  • 主从监控:监控主从redis库运行是否正常
  • 消息通知:哨兵可以将故障转移的结果发送给客户端
  • 故障转移:如果Master异常,则会进行主从切换,将其中一个Slave作为新Master
  • 配置中心:客户端通过连接哨兵来获得当前Redis服务的主节点地址

8.2 案例实操

8.2.1 架构说明

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

1主2从:用于数据读取和存放

哨兵一定要配集群,否则就不要用,最少保证3个哨兵,防止一台机器宕机,哨兵选举失败。

哨兵数量为奇数,可以更好的进行投票选举。

image-20240515204636034

8.2.2 实操

前面我们启动了Redis01、Redis02、Redis03,这三台分别作为Master、Slave1和Slave2。端口分别为6379,6380,6381。

注意:哨兵的默认端口为26379。

此时,我们再启动三个Redis,分别为Redis04,Redis05,Redis06,分别作为Sentinel01,Sentinel02,Sentinel03。端口分别为26379,26380,26381。

根据前面所描述,配置好一主二从。将Redis01作为Master。

注意!!Redis01,也就是现在的Master主机,也要配置好masterauth,因为如果该主机宕机了,他可能就变为了从机,其他机器变为了主机,此时他就要去访问复制那个主机,所以也要配置好访问的密码才能正常访问。

在Redis04,Redis05,Redis06三台机器上面,进行如下配置(以Redis04为例):

  • 查看Redis目录下的sentinel.conf配置文件。

    之前提到过的进行配置,如:

    bind服务监听地址注释掉

    daemonize ye允许后台运行

    protected-mode no关闭安全保护模式

    port端口

    logfile /myredis/sentinel26379.log日志文件路径

    pidfile /var/run/redis-sentinel26379.pidpid文件路径

    dir /myredis工作目录

    此外,哨兵的相关配置,如

    sentinel monitor <master-name> <ip> <redis-port> <quorum>配置哨兵监控的master主机。quorum表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数。(客观下线)

    sentinel auth-pass <master-name> <password>配置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>:客户端重新配置主节点参数脚本。

  • 对于上述配置,我们直接新建一个sentinel26379.conf配置文件,进行配置

bind 0.0.0.0
daemonize yes
protected-mode no
port 26379
logfile "/myredis/sentinel26379.log"
pidfile /var/run/redis-sentinel26379.pid
dir /myredis
sentinel monitor mymaster 172.17.0.2 6379 2
sentinel auth-pass mymaster 密码

其他哨兵主机也按上述配置进行修改。

上诉配置中,sentinel monitor <master-name> <ip> <redis-port> <quorum>中的quorum代表什么意思呢?

quorum:确认客观下线的最少的哨兵数量。

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

完成上诉配置后,启动1主2从Redis01,Redis02,Redis03。

然后启动3个哨兵Redis04,Redis05,Redis06。

redis-server /myredis/sentinel26379.conf --sentinel
redis-server /myredis/sentinel26380.conf --sentinel
redis-server /myredis/sentinel26381.conf --sentinel
或
redis-sentinel /myredis/sentinel26379.conf
redis-sentinel /myredis/sentinel26380.conf
redis-sentinel /myredis/sentinel26381.conf

启动哨兵后,测试一次主从复制。一切正常。

此时,1主2从+3个哨兵,配置成功!

哨兵功能演示

我们自己手动关闭6379服务器,模拟master挂了。

当6379master主机宕机后,哨兵便开始选举。在哨兵的日志文件中,可以看到选举信息。

image-20240516134521706

  • 此时,两个从机数据是否依然存在且可用?

    答:数据依然存在且可用。

  • 是否会从两台从机上选出新的主机master?

    答:会从从机上选出新的主机master。在我们的测试中,redis02,6380成为了新的master主机。此时,6380可以进行写操作了。6381仍然是slave。但是6381绑定的主机变为了6380主机,且在配置文件中也会动态的改变,如redis03 6381的配置文件:(下图中的端口号并没有错误,因为我们使用的是docker,进行了端口映射,在docker中端口为6379,映射到本地为6380)

image-20240516134539088

  • 之前宕机的master机器重启回来,是会成为从机?还是继续是主机master呢?会不会有双master冲突?

    答:会变为从机,其配置文件也会动态的变化,来绑定redis02,也就是6380。

对比redis01宕机后,配置文件的变化

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

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

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

当在一开始,我们刚停掉Reids01,使用Redis02继续执行Get命令,获取数据时,可能出现broken pipe错误。再继续执行get命令又会恢复正常。

了解broken pipe

名称描述
认识broken pipepipe是管道的意思,管道里面是数据流,通常是从文件或网络套接字读取的数据。当该管道从另一端突然关闭时,会发生数据突然中断,即是broken,对于socket来说,可能是网络被拔出或另一端的进程崩溃
解决问题其实当该异常产生的时候,对于服务端来说,并没有多少影响。因为可能是某个客户端突然中止了进程导致了该错误
总结 Broken Pipe这个异常是客户端读取超时关闭了连接,这时候服务器端再向客户端已经断开的连接写数据时就发生了broken pipe异常!

8.3 哨兵流程及选举原理

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

运行流程,故障切换

  • 三个哨兵监控一主二从,正常运行

  • SDown主观下线(Subjectively Down)

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

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

  • ODown客观下线(Objectively Down)

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

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

    当主节点被判断客观下线以后,各个哨兵节点会进行协商,先选举出一个领导者哨兵节点(兵王)并由该领导者节点,也即被选举出的兵王进行failover(故障迁移)。那么哨兵领导者,兵王如何选出来的?是通过Raft算法

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

主观下线SDown

所谓主观下线(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)状态。

客观下线ODown

sentinel monitor <master-name> <ip> <redis-port> <quorum>四个参数含义:

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

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

选取领导者哨兵节点Leader:Raft算法

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

image-20240516135820230

由兵王开始推动故障切换流程并选出一个新master,三个步骤:

1.新主登基

  • 某个Slave被选中成为新Master

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

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

    如果上诉一样,再判断复制偏移位置offset最大的从节点。

    如果上诉一样,最后判断最小Run ID的从节点(字典顺序,ASCII码)。

    image-20240516140244276

2.群臣俯首

执行slaveof no one命令让选出来的从节点成为新的主节点,并通过slaveof命令让其他节点成为其从节点。

Sentinel leader会对选举出的新master执行slaveof no one操作,将其提升为master节点。

Sentinel leader向其它slave发送命令,让剩余的slave成为新的master节点的slave。

3.旧主拜服

将之前已下线的老master设置为新选出的新master的从节点,当老master重新上线后,它会成为新master的从节点。

Sentinel leader会让原来的master降级为slave并恢复正常工作。

总结:

  • 原master宕机
  • 哨兵判断主观下线
  • 哨兵通过投票,判断客观下线
  • 哨兵根据Raft算法选取领导者哨兵Leader
  • 领导者哨兵Leader发动Redis选举,根据优先权、偏移量、RUN ID选取新的Master,并设定slave。

8.4 哨兵使用建议

  • 哨兵节点的数量应为多个,哨兵本身应该集群,保证高可用

    支持读写分离、支持数据的高可用、支持海量数据的读写存储操作

  • 哨兵节点的数量应该是奇数

  • 各个哨兵节点的配置应一致

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

  • 哨兵集群+主从复制,并不能保证数据零丢失,所以引出Redis集群


9. Redis集群分片

9.1 介绍

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

的一部分,这就是Redis的集群,其作用是提供在多个Redis节点间共享数据的程序集。

image-20240516221838619

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

Redis集群可以支持多个Master。

Redis集群作用

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

9.2 集群算法-分片-槽位

9.2.1 介绍

集群的槽位slot

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

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

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

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

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

image-20240518155200790

集群的分片

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

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

槽位与分片的优势

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

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

9.2.2 槽位映射的3种解决方案

1.哈希取余分区

image-20240518160424325

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

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

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

2.一致性哈希算法分区

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

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

3大步骤:

  • 算法构建一致性哈希环

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

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

    image-20240518160843846

  • redis服务器IP节点映射

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

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

    image-20240518160953693

  • 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-20240518161100306

优点:

  • 一致性哈希算法的容错性

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

    image-20240518161522393

  • 一致性哈希算法的扩展性

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

    image-20240518161407721

缺点:

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

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

    image-20240518161325700

小总结:

为了在节点数目发生改变时尽可能少的迁移数据,将所有的存储节点排列在收尾相接的Hash环上,每个key在计算Hash后会顺时针找到临近的存储节点存放。而当有节点加入或退出时仅影响该节点在Hash环上顺时针相邻的后续节点。

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

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

3.哈希槽分区(建议使用)

为什么会出现?

答:因为一致性哈希算法的数据倾斜问题。

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

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

image-20240518161840583

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

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

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

哈希槽计算

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

9.2.3 经典面试题

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

答:

说明一

正常的心跳数据包带有节点的完整配置,可以用幂等方式用旧的节点替换旧节点,以便更新旧的配置。

这意味着它们包含原始节点的插槽配置,该节点使用2k的空间和16k的插槽,但是会使用8k的空间(使用65k的插槽)。

同时,由于其他设计折衷,Redis集群不太可能扩展到1000个以上的主节点。

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

说明二(人话)

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

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

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

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

  • redis的集群主节点数量基本不可能超过1000个。(因为太多的Redis主节点可能会导致数据失真)

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

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

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

9.2.4 额外说明

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

比如客户端将输入写入到Redis01,此时Redis01要将输入给从机Redis02,但是!此时,数据刚写入到Redis01,还没来得及讲输入写入到从机,Redis01宕机了,那么此时就会造成了Redis02丢失数据。

9.3 案例实操

9.3.1 集群配置

删除之前Docker中的Redis容器。

重新新建6台Redis机器。Redis01,Redis02,Redis03,Redis04,Redis05,Redis06。映射到本地的端口分别为6379,6380,6381,6382,6383,6384。并在每台Redis机器上进行目录/myredis/cluster

image-20240518180448805

注意:

上诉图,只是一个理想的设计,比如这里我们指定Master01点从机是Slave01,但是Master01点从机到底是哪台,是由集群其内部分配的。

修改配置文件

以Redis01为例。

/myredis/cluster下新建文件redisCluster6379.conf文件。

bind 0.0.0.0
daemonize yes
protected-mode no
port 6381
logfile "/myredis/cluster/cluster6379.log"
pidfile /myredis/cluster6379.pid
dir /myredis/cluster
dbfilename dump6379.rdb
appendonly yes
appendfilename "appendonly6379.aof"
requirepass 密码
masterauth 密码
# 集群配置
cluster-enabled yes   # 开启集群
cluster-config-file nodes-6379.conf   # 集群的配置文件,自动生成,无需自己创建
cluster-node-timeout 5000  # 集群的超时时间 5s

其他5台也按照上诉进行配置,注意需要修改的地方。

启动6台Redis实例

redis-server /myredis/cluster/redisCluster6379.conf
...

任选一个Redis机器,通过redis-cli命令为6台机器构建集群关系

redis-cli -a 密码 --cluster create --cluster-replicas 1 172.17.0.2:6379 172.17.0.3:6379 172.17.0.4:6379 172.17.0.5:6379 172.17.0.6:6379 172.17.0.7:6379

# --cluster-replicas 1  表示为每个master创建一个slave节点

注意:此处端口我们之所以都写的为6379,因为在docker中redis运行的实际端口我们写的是6379,只是将它的端口映射到本地才变为6379,6380,6381……

执行该命令后,可以看到提示我们Master[0],Master[1],Master[2],并给出了它们对应的槽位,分别为0-5460,5461-10922,10923-16383。并指出172.17.0.6172.017.0.2的从机,即Redis05为Redis01的从机,此外,Redis06为Redis02的从机,Redis04为Redis03的从机。

image-20240518202934221

出现如下提示,便是配置成功。

image-20240518203142301

此时,我们查看\myredis\cluster下的文件。可以看到产生了集群节点的配置文件。

image-20240518203431752

链接进入6379作为切入点,查看并检验集群状态

进入redis客户端。

使用info replication命令查看主从信息。

image-20240518204353618

使用cluster info查看集群信息。

image-20240518204546952

使用cluster nodes查看集群节点。

image-20240518204516622

9.3.2 集群读写

对6379新增两个key,看看效果如何。

image-20240518204951430

可以看到,显示error,报错了!但是它显示MOVED 12706 172.17.0.4:6379,槽位为12706,这个IP对应的是Redis03。我们尝试在Redis03中执行set k1 v1,显示执行成功了。

这是为什么呢?

因为,一定要注意槽位的范围区间,需要路由到对应的槽位。

如何解决?

为了防止路由失效,我们需要在启动redis客户端时加参数-c,即redis-cli -a 密码 -p 端口 -c

我们重启redis01的客户端,然后再执行set k1 v1命令。

image-20240518205748395

可以发现,执行成功了。我们可以理解它帮我我们进行了重定向。

9.3.3 主从容错切换迁移

容错切换迁移

主机Reids01:6379宕机,对应的从机Redis05:6383上位。

Redis停止之前的节点信息:

image-20240519093939563

先停止Redis01,查看从机是否成功上位。

image-20240519094109647

可以看到,在Redis01停止后,且Redis01还没有重启时,Redis05成功上位成为master,但是此时它没有从机。此时整体结构为3主2从。

此时,重启Redis01,查看情况。

image-20240519094700254

可以看到,当Redis01重启重新回来时,它成为了Redis05的从机。

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

手动故障转移,节点从属调整

上面Redis01与Redis05主从关系互换了,和我们原始的设计图不一致了,该如何处理?

重新登陆Redis01机器。进行节点从属关系的调整,调整为Redis01为master,Redis05为slave。

使用命令CLUSTER FAILOVER。该命令为集群的故障调换。

image-20240519100005545

执行该命令后,可以看到,Redis01重新成为了Master,而Redis05成为了Redis01的从机。

当集群中一个主机挂掉了,且该主机下的所有从机也都挂了,那么会有其他主机的从机来顶替这个主机的位置。

9.3.4 主从扩容

当3主3从不够用时,我们将其扩容至4主4从。新增主机Master04,与从机Slave04。端口号分别指定6385,6386。

此时,我们新建两个Redis机器,分别为Redis07:6385和Redis08:6386。并按照上诉进行集群的配置,然后启动。

此时,这两个Redis节点均为Master,且未加入至集群中。

将新增的6385节点(空槽号)作为master节点加入原集群。

在Redis07中执行,redis-cli -a 密码 --cluster add-node 新加入节点的IP地址:端口号 原来节点的IP地址:端口号

如:redis-cli -a 密码 --cluster add-node 172.17.0.8:6379 172.17.0.2:6379

注意:此处端口号都写为了6379,因为它在docker实际的端口我们设置的是6379,只是将其映射到本地时端口变为6385。

172.17.0.8就是将要作为master新增节点,

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

如果当我们实际执行该命令时,出现了错误!

[ERR] Not all 16384 slots are covered by nodes.

问题原因:这个往往是由于主node移除了,但是并没有移除node上面的slot,从而导致了slot总数没有达到16384,其实也就是slots分布不正确。所以在删除节点的时候一定要注意删除的是否是Master主节点。

通过查询找到了解决方案:

使用redis-cli -a 密码 --cluster fix 172.17.0.2:6379来修复集群。

修复完成后再用check命令检查下是否正确,redis-cli -a 密码 --cluster check 172.17.0.2:6379

如果分布不均匀那可以使用下面的方式重新分配slot,redis-cli -a 密码 --cluster reshard 172.17.0.2:6379

然后重新执行加入集群的命令。发现执行成功。

image-20240519103938503

执行redis-cli -a 密码 --cluster check 172.17.0.2:6379检查集群节点情况。

image-20240519105118510

可以发现,新加入的这个Redis节点还没有分配槽位。

通过redis-cli -a 密码 --cluster reshard IP地址:端口号重新分配槽位。如:redis-cli -a 密码 --cluster reshard 172.17.0.2:6379

image-20240519105639805

执行该命令后,询问我们想要移动多少槽位。此时,我们填入4096即可。(16386/4=4096)。

输入并回车后,会询问我们这些分配出来的槽位移动至哪个节点,输入节点号。

image-20240519105920995

此时,我们输入新节点Redis07的节点号c3418ca6b4fe5c4f00434b87f6069becf972d993即可。此时,又会提示我们:

image-20240519110309665

在这时,我们输入all,并回车即可。在这个过程中会有提示Do you want to proceed with the proposed reshard plan (yes/no)? ,此时输入yes即可。

执行完毕后,此时我们再使用命令redis-cli -a 密码 --cluster check 172.17.0.2:6379来查看节点信息。

image-20240519110517183

可以看到,槽位已经分配成功了。

但是,如下图,我们可以看出,新节点Redis07的槽位并不是连续的,而之前原来的节点槽位都是连续的,这是为什么呢?

image-20240519110740861

这是因为,在给Redis07分配槽位时,并不是将集群中所有槽位打破,重新给每个节点分配槽位。而是将集群中原来节点的槽位各自匀出来一些给新的节点。所以这三个槽位区间来自原来3个集群中的节点。

为主节点6385分配从节点6386

通过命令:redis-cli -a 密码 --cluster add-node 新slave主机的IP:新slave端口 新master主机的IP:新master端口 --cluster-slave --cluster-master-id 新主机节点ID进行分配

如:redis-cli -a 密码 --cluster add-node 172.17.0.9:6379 172.17.0.8:6379 --cluster-slave --cluster-master-id c3418ca6b4fe5c4f00434b87f6069becf972d993

执行该命令后,出现如下提示,说明执行成功。

image-20240519111441734

通过redis-cli -a 密码 --cluster check 172.17.0.2:6379命令,查看节点情况。

image-20240519111651986

可以看到,每个主机都被分配了一个从机。

至此,4主4从就配置好了。

9.3.5 主从缩容

当前我们为4主4从,我们想从4主4从缩容到3主3从,该如何操作呢?

主要步骤:

  • 先清除从节点
  • 从主节点中清出来的槽号分配给其他主节点
  • 再删除主节点
  • 恢复成3主3从

目的:从集群中删除主节点Redis07/172.17.0.8:6385和从节点Redis08/172.17.0.9:6386

详细步骤:

步骤1 查询从节点ID

使用命令redis-cli -a 密码 --cluster check 172.17.0.9:6379查询集群情况,获取从节点Redis08的节点ID为b7594734c85ddcbbef103b06e63dec0c35161160

步骤2 删除从节点

使用命令redis-cli -a 密码 --cluster del-node ip:端口 从集群中所指定的Redis。如redis-cli -a 密码 --cluster del-node 172.17.0.9:6379 b7594734c85ddcbbef103b06e63dec0c35161160,执行该命令后,使用redis-cli -a 密码 --cluster check 172.17.0.2:6379查询集群情况。

image-20240519135229779

发现,主机Redis07的从机Redis08已经清除了。

步骤3 重新分配主节点槽位

为了清除主节点Redis07,首先将Redis07的槽位重新分配给Redis01(在此案例中,我们是将槽位直接全部给了Redis01,可以有其他方案)。通过redis-cli -a 密码 --cluster reshard IP:端口命令进行槽位的重新划分,如redis-cli -a 密码 --cluster reshard 172.17.0.2:6379

image-20240519135834910

因为Redis07的槽位只有4096,将Redis07的所有槽位重新分配给其他节点,所以全部就是4096,这里输入4096即可。提示输入接收该槽位的机器的ID时,写入Redis01的ID即可01e7b393f31fe9b4f04ecb3e8bcac69b8ca8ccfe。然后会询问槽位的来源,我们输入Redis07的ID即可,然后再输入一个done

image-20240519140446451

后面,会询问是否接受该访问,输入yes继续执行即可完成。

再次通过redis-cli -a 密码 --cluster check 172.17.0.2:6379命令查询集群情况。

image-20240519141005655

可以看到Redis07的槽位全部给了Redis01了。且Redis07从Master变成了一个Slave,挂在了主机Redis01下面。

步骤4 删除主节点

使用命令redis-cli -a 密码 --cluster del-node ip:端口 节点ID删除节点。如:redis-cli -a 密码 --cluster del-node 172.17.0.8:6379 c3418ca6b4fe5c4f00434b87f6069becf972d993。执行该命令后,再次查询集群信息。

image-20240519141416909

从4主4从已经变为了3主3从,成功完成了缩容。

9.4 集群常用命令

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

image-20240519142911652

如何解决呢?

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

image-20240519143134925

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

常用命令

  • cluster-require-full-coverage:在配置文件中进行配置,集群是否完整才能对外提供服务

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

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

  • CLUSTER COUNTKEYSINSLOT 槽位数字编号

    1:槽位被占用,0:槽位没被占用

  • CLUSTER KEYSLOT 键名称:该键应该存在哪个槽位上


10. SpringBoot整合Redis

10.1 总体概述

image-20240519163236685

jedis,lettuce,RedisTemplate三者的联系

  • Jedis是最初代的,最老牌的,是Redis官网推荐的一个面向java客户端,库文件实现了对各类API进行封装调用。爆出来一些问题,线程池不安全等问题。

  • Lettuce是一个Redis的Java驱动包,可以理解为是对Jedis本身的一个优化。

  • RedisTemplate又是Lettuce的升级。(推荐使用)

10.2 本地连接Redis常见问题

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

10.3 集成Jedis

集成步骤:

1. 新建SpringBoot项目,redis-study

2. 改POM文件

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

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

3. 写YAML配置文件

spring:
  application:
    name: redis-study

server:
  port: 8080

4. 业务类编写

public class JedisDemo {
    public static void main(String[] args) {
        // 1. 通过IP和端口好,获得Connection
        Jedis jedis = new Jedis("localhost", 6387);
        // 2. 指定访问Redis服务的密码
        jedis.auth("密码");
        // 3. 获得了Jedis客户端,可以像JDBC一样访问Redis服务了
        System.out.println(jedis.ping());  // 返回PONG,说明连接成功

        // 相当于执行命令 Keys *
        Set<String> keys = jedis.keys("*");
        System.out.println(keys);

        // 尝试5中常用数据类型
        // string
        jedis.set("k2","spring");
        String k2 = jedis.get("k2");
        System.out.println(k2);  // spring
        // list
        jedis.lpush("mylist","l1","l2","l3","l4","l5","l6");
        List<String> mylist = jedis.lrange("mylist", 0, -1);
        System.out.println(mylist);  // [l6, l5, l4, l3, l2, l1]
        // hash
        Map<String,String> map = new HashMap<>();
        map.put("name","zhangsan");
        map.put("gender","man");
        jedis.hset("myhash",map);
        Map<String, String> hashMap = jedis.hgetAll("myhash");
        System.out.println(hashMap);  // {gender=man, name=zhangsan}
        // set
        jedis.sadd("myset","1","1","2","3","4");
        Set<String> myset = jedis.smembers("myset");
        System.out.println(myset);  // [1, 2, 3, 4]
        // zset
        jedis.zadd("myzset",2,"5");
        jedis.zadd("myzset",3,"8");
        jedis.zadd("myzset",1,"4");
        List<String> myzset = jedis.zrange("myzset", 0, -1);
        List<String> myzset1 = jedis.zrevrange("myzset", 0, -1);
        System.out.println(myzset);  // [4, 5, 8]
        System.out.println(myzset1);  // [8, 5, 4]

        // 设置过期时间
        jedis.expire("myset",3600);
        // 获取过期时间
        long myttl = jedis.ttl("myset");
        System.out.println(myttl);  // 3600
    }
}

10.4 集成lettuce

Jedis 是直连 redis server,会有线程安全问题。除非使用连接池,为每个 Jedis实例增加物理连接。

Lettuce是基于Netty的,连接实例可以在多个线程间并发访问,Lettuce还支持异步连接方式,提高网络等待和磁盘IO效率。

集成步骤:

1. 修改POM文件

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

2. 编写业务类

public class LettuceDemo {
    public static void main(String[] args) {
        // 1. 使用构建器,链式编程来build RedisURI
        RedisURI uri = RedisURI.builder().redis("localhost", 6387)
                        .withAuthentication("default","密码")
                        .build();
        // 2. 创建连接客户端
        RedisClient redisClient = RedisClient.create(uri);
        StatefulRedisConnection<String, String> connection = redisClient.connect();
        // 3. 通过connection创建操作的command
        // sync表示同步
        RedisCommands<String, String> commands = connection.sync();

        // 4. 业务操作
        // 获取所有的key
        List<String> keys = commands.keys("*");
        System.out.println(keys);

        // string
        commands.set("lkey","ltest");
        String lkey = commands.get("lkey");
        System.out.println(lkey);
        // list
        commands.lpush("llist","1","a","b");
        List<String> llist = commands.lrange("llist", 0, -1);
        System.out.println(llist);
        // hash
        Map<String,String> map = new HashMap<>();
        map.put("class","grade-1");
        map.put("num","20");
        commands.hset("classinfo",map);
        Map<String, String> classinfo = commands.hgetall("classinfo");
        System.out.println(classinfo);
        // set
        commands.sadd("lset","1","a","b");
        Set<String> lset = commands.smembers("lset");
        System.out.println(lset);
        // zset
        commands.zadd("lzset",1,"a");
        commands.zadd("lzset",4,"c");
        commands.zadd("lzset",3,"b");
        List<String> lzset = commands.zrange("lzset", 0, -1);
        List<String> lzset1 = commands.zrevrange("lzset", 0, -1);
        System.out.println(lzset);
        System.out.println(lzset1);

        // 设置过期时间
        commands.expire("classinfo",3600);
        Long classinfo1 = commands.ttl("classinfo");
        System.out.println(classinfo1);

        // 5. 关闭释放资源
        connection.close();
        redisClient.close();
    }
}

10.5 集成RedisTemplate

10.5.1 连接单机

1. 修改POM文件,新增依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.2.5</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<!--swagger3-->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.2.0</version>
</dependency>
<dependency>
    <groupId>log4j</groupId>
    <artifactId>log4j</artifactId>
    <version>1.2.17</version>
</dependency>

2. 修改yaml配置文件

spring:
  application:
    name: redis-study

  data:
    redis:
      database: 0
      host: localhost
      port: 6387
      password: 密码
      lettuce:
        pool:
          max-active: 8
          max-wait: -1ms
          max-idle: 8
          min-idle: 0

server:
  port: 8080

# 日志相关配置
logging:
  level:
    root: info
    cn:
      codewei: info

3. 编写代码

配置类-RedisConfig

@Configuration
public class RedisConfig {

}

配置类-SwaggerConfig

@Configuration
public class SwaggerConfig {
    @Bean
    public OpenAPI docsOpenApi()
    {
        return new OpenAPI()
                .info(new Info().title("redisStudy")
                        .description("通用设计rest")
                        .version("v1.0"))
                .externalDocs(new ExternalDocumentation()
                        .description("www.codewei.cn")
                        .url("https://yiyan.baidu.com/"));
    }
}

Service

@Service
@Slf4j
public class OrderService {
    @Resource
    private RedisTemplate redisTemplate;

    private static final String ORDER_PREFIX = "order:";

    public void addOrder() {
        int keyId = ThreadLocalRandom.current().nextInt(1000)+1;
        String serialNo = UUID.randomUUID().toString();

        String key = ORDER_PREFIX + keyId;
        String value = "京东订单" + serialNo;

        redisTemplate.opsForValue().set(key,value);
        log.info("key:{},value:{}",key,value);
    }
    public String getOrderById(Integer orderId) {
        return String.valueOf(redisTemplate.opsForValue().get(ORDER_PREFIX + orderId));
    }
}

Controller

@RestController
@Slf4j
@Tag(name = "订单接口")
public class OrderController {
    @Resource
    private OrderService orderService;

    @Operation(summary = "新增订单接口")
    @RequestMapping(value = "/order/addOrder",method = RequestMethod.POST)
    public String addOrder(){
        orderService.addOrder();
        return "ok";
    }

    @Operation(summary = "获取订单接口")
    @RequestMapping(value = "/order/getOrder/{id}",method = RequestMethod.GET)
    public String getOrder(@PathVariable("id") Integer id){
        String orderById = orderService.getOrderById(id);
        return orderById;
    }
}

4. 测试

访问http://localhost:8080/swagger-ui/index.html进入到swagger页面。

通过swagger访问addOrder,进行测试。

发送请求后,后台日志输入:key:order:26,value:京东订单29f79fee-4da8-4e4d-8308-2af91b93d071

此时,我们访问getOrder,携带上面返回来的26作为参数。成功访问,返回结果京东订单29f79fee-4da8-4e4d-8308-2af91b93d071

但是,我们在Redis客户端中查看到,其key并不是order:26

image-20240519212513738

这是由Redis序列还问题引起的。

键和值都是通过Spring提供的Serializer序列化到数据库的。RedisTemplate默认使用的是org.springframework.data.redis.serializer.JdkSerializationRedisSerializer 。StringRedisTemplate默认使用的是StringRedisSerializer。Key被序列化成上图这样,线上通过key去查对应的value非常不方便。

解决方案一

使用StringRedisTemplate来替换我们之前使用的RedisTemplate

解决方案二

在RedisConfig中进行配置

@Configuration
public class RedisConfig {
    /**
     * redis序列化的工具配置类,下面这个请一定开启配置
     * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
     * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法
     * this.redisTemplate.opsForSet(); //提供了操作set的所有方法
     * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法
     * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法
     */
    @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;
    }
}

此时,我们重启项目,重新执行addOrder进行测试。

image-20240519214215563

发现key已经成功被存入了,没有发生乱码。

10.5.2 连接集群

1. 修改redis配置

因为我们使用的是Docker,通过本地映射的端口,redis可能无法找到服务,所以我们需要在redis中增加如下配置

cluster-announce-ip 192.168.1.105   # 本地的IP
cluster-announce-port 6379   # 映射到本地的端口 这个参数用于指定 Redis 集群节点对外提供服务的端口号。
cluster-announce-bus-port 6379  # redis的端口 这个参数用于指定 Redis 集群节点之间通信所使用的端口号。

2. 启动集群6台Redis实例

image-20240520095355122

2. 编写yaml文件

spring:
  application:
    name: redis-study

  data:
    redis:
#      database: 0
#      host: localhost
#      port: 6387
      password: 密码
      # --------- 集群配置 ----------
      cluster:
        max-redirects: 3 # 获取失败 最大重定向次数
        nodes: localhost:6379,localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384
      # ----------------------------
      lettuce:
        pool:
          max-active: 8
          max-wait: -1ms
          max-idle: 8
          min-idle: 0

server:
  port: 8080

logging:
  level:
    root: info
    cn:
      codewei: info

3. 此时,启动项目,业务类无需更改,直接通过微服务访问Redis集群

访问http://localhost:8080/swagger-ui/index.html进入到swagger页面进行测试。

两次请求addOrder接口进行测试。访问接口后,正常访问,后台日志打印key:order:817,value:京东订单6996cd22-8b1f-4a42-b9ca-1c836cf7b280key:order:518,value:京东订单4693250f-b71b-4e89-8b71-d180fc25ee87

image-20240520105220842

在Redis中可以正常取到存储的数据。并且我们可以看到两次的数据存放到了不通的Redis主机中。

4. 模拟master:6379宕机

先对Redis集群以命令方式进行验证读写命令,看从机6383是否成功上位。

经过测试,可以看到6383成功上位。读写命令正常。

但是,我们通过Swagger进行测试的时候,无法正常访问Redis了。

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

image-20240520111036222

image-20240520111042321

导致原因:Redis默认的连接池采用 Lettuce,当Redis 集群节点发生变化后,Letture默认是不会刷新节点拓扑。

解决方案:

1.在maven引入的spring-boot-starter-data-redis包中排除lettuce,并且引入采用jedis依赖(不推荐)

2.重写连接工厂实例(极度不推荐)

@Bean
public DefaultClientResources lettuceClientResources() {
    return DefaultClientResources.create();
}
@Bean
public LettuceConnectionFactory lettuceConnectionFactory(RedisProperties redisProperties, ClientResources clientResources) {
    ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
            .enablePeriodicRefresh(Duration.ofSeconds(30)) //按照周期刷新拓扑
            .enableAllAdaptiveRefreshTriggers() //根据事件刷新拓扑
            .build();
    ClusterClientOptions clusterClientOptions = ClusterClientOptions.builder()
            //redis命令超时时间,超时后才会使用新的拓扑信息重新建立连接
            .timeoutOptions(TimeoutOptions.enabled(Duration.ofSeconds(10)))
            .topologyRefreshOptions(topologyRefreshOptions)
            .build();
    LettuceClientConfiguration clientConfiguration = LettuceClientConfiguration.builder()
            .clientResources(clientResources)
            .clientOptions(clusterClientOptions)
            .build();
    RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(redisProperties.getCluster().getNodes());
    clusterConfig.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
    clusterConfig.setPassword(RedisPassword.of(redisProperties.getPassword()));
    LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(clusterConfig, clientConfiguration);
    return lettuceConnectionFactory;

}

3.刷新节点集群拓扑动态感应,在yaml中增加如下配置

#支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
spring.redis.lettuce.cluster.refresh.adaptive=true
#定时刷新
spring.redis.lettuce.cluster.refresh.period=2000
spring.redis.cluster.nodes=localhost:6379,localhost:6380,localhost:6381,localhost:6382,localhost:6383,localhost:6384
  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mango1698

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值