Redis

Redis简介

NoSQL简介

​ 目前市场主流数据存储都是使用关系型数据库。每次操作关系型数据库时都是I/O操作,I/O操作是主要影响程序执行性能原因之一,连接数据库关闭数据库都是消耗性能的过程。尽量减少对数据库的操作,能够明显的提升程序运行效率。

​ 针对上面的问题,市场上就出现了各种NoSQL(Not Only SQL,不仅仅可以使用关系型数据库)数据库,它们的宣传口号:不是什么样的场景都必须使用关系型数据库,一些特定的场景使用NoSQL数据库更好。

常见NoSQL数据库:

​ memcached :键值对,内存型数据库,所有数据都在内存中。

​ Redis:和Memcached类似,还具备持久化能力。

​ HBase:以列作为存储。

​ MongoDB:以Document做存储。

2.Redis简介

​ Redis是以Key-Value形式进行存储的NoSQL数据库。

​ Redis是使用C语言进行编写的。

​ 平时操作的数据都在内存中,效率特高,读的效率110000/s,写81000/s,所以多把Redis当做缓存工具使用。

​ Redis以solt(槽)作为数据存储单元,每个槽中可以存储N多个键值对。Redis中固定具有16384。理论上可以实现一个槽是一个Redis。每个向Redis存储数据的key都会进行crc16算法得出一个值后对16384取余就是这个key存放的solt位置。

​ 同时通过Redis Sentinel提供高可用,通过Redis Cluster提供自动分区。

主流功能和应用

1、分担MySQL压力,MySQL是存入硬盘,Redis是在内存中使用,日常使用中80%查询,20%写入。

2、分担流程:查找的时候先查找redis,找到直接返回,如果没有就去mysql,mysql返回数据,然后就把mysql写回redis,然后以后找redis就能找到了。

3、比如计数器,排行榜等方面就明显用redis优于mysql

4、内存存储和持久化 RDB和AOF,如果断电之后redis支持异步将内存中的数据写到硬盘上,然后有电就恢复到redis,不麻烦Mysql

5、高可用结构,单机,主从,哨兵,集群。为了避免redis挂了,服务压力过大。

6、缓存穿透,击穿,雪崩

7、分布式锁,跨服务器加锁就是用分布式锁。之前java的锁是针对JVM的,

8、队列list和set结构,消息队列平台,验证码发布

9、….

image-20251125114243582

Redis单机版安装

​ 1.安装依赖C语言依赖

​ redis使用C语言编写,所以需要安装C语言库

1
# yum install -y gcc-c++ automake autoconf libtool make tcl 

​ 2.上传并解压

​ 把redis-5.0.5.tar.gz上传到/usr/local/tmp中

​ 解压文件

1
2
3
# cd /usr/local/tmp

# tar zxf redis-5.0.5.tar.gz

​ 3.编译并安装

​ 进入解压文件夹

1
# cd /usr/local/tmp/redis-5.0.5/

​ 编译

1
# make

​ 安装

1
# make install PREFIX=/usr/local/redis

​ 4.开启守护进程

​ 复制cd /usr/local/tmp/redis-5.0.5/中redis.conf配置文件

1
# cp redis.conf /usr/local/redis/bin/

​ 修改配置文件

1
2
3
# cd /usr/local/redis/bin/

# vim redis.conf

​ 把daemonize的值由no修改为yes

​ 5.修改外部访问

​ 在redis5中需要修改配置文件redis.conf允许外部访问。需要修改两处。

​ 注释掉下面

​ bind 127.0.0.1

1
#bind 127.0.0.1

​ protected-mode yes 改成 no

​ 6.启动并测试

​ 启动redis

​ # ./redis-server redis.conf

​ 重启redis

​ # ./redis-cli shutdown

​ # ./redis-server redis.conf

​ 启动客户端工具

​ #./redis-cli

​ 在redis5中客户端工具对命令会有提供功能。

Redis常用的10大类型

​ Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储,它还支持数据的备份,即master-slave模式的数据备份,同样Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。

​ Redis支持的五大数据类型包括String(字符串 用法: 键 值),Hash(哈希 类似Java中的 map 用法: 键 键值对),List(列表 用法:键 集合 不可以重复),Set(集合 用法:键 集合 可以重复),Zset(sorted set 有序集合 用法: 键 值 值)

image-20251125140449390

String(字符串)

​ string 是 redis 最基本的类型,你可以理解成与 Memcached 一模一样的类型,一个 key 对应一个 value。string 类型是二进制安全的意思是 redis 的 string 可以包含任何数据。比如jpg图片或者序列化的对象。**string 类型是 Redis 最基本的数据类型,string 类型的值最大能存储 512MB。**

应用场景:

​ String是最常用的一种数据类型,普通的key/value存储都可以归为此类,value其实不仅是String,

也可以是数字:比如想知道什么时候封锁一个IP地址(访问超过几次)。

List(列表)

​ Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。底层是双端链表,每个列表最多40亿个元素。

应用场景:

    Redis list的应用场景非常多,也是Redis最重要的数据结构之一。  

​ 我们可以轻松地实现最新消息排行等功能。

   Lists的另一个应用就是消息队列,可以利用Lists的PUSH操作,将任务存在Lists中,然后工作线程再用POP操作将任务取出进行执行。  

Hash(哈希)

​ Redis hash 是一个键值(key=>value)对集合。

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

​ 使用场景:存储、读取、修改用户属性

​ 我们简单举个实例来描述下Hash的应用场景,比如我们要存储一个用户信息对象数据,包含以下信息: 用户ID,为查找的key,

​ 存储的value用户对象包含姓名name,年龄age,生日birthday 等信息, 如果用普通的key/value结构来存储,主要有以下2种存储方式:

​ 第一种方式将用户ID作为查找key,把其他信息封装成一个对象以序列化的方式存储,

​ 如:set u001 “李三,18,20010101”

​ 这种方式的缺点是,增加了序列化/反序列化的开销,并且在需要修改其中一项信息时,需要把整个对象取回,并且修改操作需要对并发进行保护,引入CAS等复杂问题。

​ 第二种方法是这个用户信息对象有多少成员就存成多少个key-value对儿,用用户ID+对应属性的名称作为唯一标识来取得对应属性的值,

​ 如:mset user:001:name “李三 “user:001:age18 user:001:birthday “20010101” 虽然省去了序列化开销和并发问题,但是用户ID为重复存储,如果存在大量这样的数据,内存浪费还是非常可观的。

​ 那么Redis提供的Hash很好的解决了这个问题。

Set(集合)

Redis的Set是string类型的无序集合。

​ 使用场景:1.共同好友、二度好友

​ 2. 利用唯一性,可以统计访问网站的所有独立 IP

  Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。  

  比如在微博应用中,每个人的好友存在一个集合(set)中,这样求两个人的共同好友的操作,可能就只需要用求交集命令即可。  

​ Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实

  实现方式:  

​ set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。

zset(sorted set:有序集合)

 Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复。

使用场景:1.带有权重的元素,比如一个游戏的用户得分排行榜

​ 2.比较复杂的数据结构,一般用到的场景不算太多

GEO地理空间

主要用于存储地理位置信息,并对存储的信息进行操作,包括添加地理位置坐标,获取地理位置坐标,计算两个位置的距离,根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。

HyperLogLog 基数统计

在输入元素的数量或者体积非常大时,计算基数所需要的空间总是固定并且很小。

比如统计今天天猫访问的访问量,这时候就需要记录不重复的元素的统计。基数统计就是不重复元素的统计

bitmap 位图

里面由许许多多的小格子组成,里面只能放1 和 0 ,用来判断Y/N,用来每日签到,点赞,点击率分析,是否点赞等功能。

bitfield 位域

类似于修改java的字节码,实时数据的替换和查找。

stream流

redis版的mq,消息队列

Redis常用命令

Redis命令相关手册有很多,下面为其中比较好用的两个

1.https://www.redis.net.cn/order/

2.http://doc.redisfans.com/text-in

1. Key操作

keys * 查找所有key

type [key name] 返回类型

unlink key 非阻塞删除,真正的删除在后面异步删除

move key dbindex[0-15] redis默认16个库,然后将当前数据库的key移动到给定的数据库db中

select dbindex 切换数据库【0-15】

dbsize 查看当前数据库的数量

flushdb 清空当前库

flushall 通杀全部库

1.1 exists

​ 判断key是否存在。

​ 语法:exists key名称

​ 返回值:存在返回数字,不存在返回0

1.2 expire

​ 设置key的过期时间,单位秒

​ 语法:expire key 秒数

​ 返回值:成功返回1,失败返回0

1.3 ttl

​ 查看key的剩余过期时间

​ 语法:ttl key

​ 返回值:返回剩余时间,如果不过期返回-1 -2表示已经过期

1.4 del

​ 根据key删除键值对。

​ 语法:del key

​ 返回值:被删除key的数量

2. 字符串值(String)

set 和 get

任何一个value值最大一个512M

image-20251125174128969

1
2
3
4
5
6
7
8
set k1 v1 nx  如果不存在k1就创建k1
set k1 v1 xx 如果存在就覆盖创建
set k1 v1 get 先获取老的k1的值返回,并且把k1的值赋值为v1
set k1 v1 ex 10 设置10秒过期时间
ttl k1 查看过期时间
set k1 v1 px 3000 设置3000毫秒
但是如果同一个key修改没有配置过期时间,那么修改覆盖的时候会变成永久不过期,但是现在只想修改值,时间不覆盖
set k1 v1 keepttl

同时设置/获取多个键值

1
2
3
4
mset k1 v1 k2 v2 k3 v3
mget k1 k2 k3
msetnx k1 v1 k4 v4 一半成功一半不成功的话那就是不成功,是一个整体
msetnx k5 v5 k6 v6

获取指定区间范围内的值

1
2
3
4
5
set k1 abcd1234
getrange k1 0 -1 获取部分value
getrange k1 0 3
setrange k1 1 xxyy 那么就变成了 axxyy234

数值增减

1
2
3
4
5
set k1 100
incr k1 类似于i++
incrby k1 3 类似于i+3
decr k1 i--
decrby k1 3

获取字符串长度

1
2
3
set k1 abcd
strlen k1 长度为4
append k1 xxxx

分布式锁

1
2
setnx key value
setex(set with expire)键秒值/setnx(set if not exist)

什么叫分布式锁,三个微服务,同时争抢一个资源,以前是sync、lock,unlock这些是jvm的,找一个redis,然后找一个节点Key,现在如果一个微服务需要拿资源,需要先去redis上,setnx lock uuid,那么键锁成功,然后执行完del lock,相当于lock和unlock。zookeeper也能只不过那个是建node。

image-20251125225724221

1
2
3
4
5
6
set k1 v1 
expire k1 10
这两个是分开的不是原子操作
setex k1 10 v11
setnx k1 10 v11
这样就是原子操作了

getset

1
getset k1 haha 就是先get再set

应用场景

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

是否喜欢的文章 用incre

3 哈希表(Hash)

​ Hash类型的值中包含多组field value。

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

hset / hget / hmset / hmget / hgetall / hdel

1
2
3
4
5
6
7
hset user: 001 id 11 name z3 age 25 放了个user:001 然后value 是 id 11等的KV键值对
hget user:001 id
hget user:001 name
hmset user:001 id 12 name li4 age 26
hmget user:001 id name age 加了个m就是多个取
hgetall user:001
hdel user:001 age

hlen

1
hlen user:001

hexists key 在key里面的某个值的key

1
hexists user:001 name 是否有name这个字段

hkeys / hvals 单独罗列keys 和 vals

1
2
hkeys user:001
hvals user:001

hincrby / hincrbyfloat

1
2
3
4
hset user:001 age 25 score 99.5
hincrby user:001 age 1
hincrby user:001 age 2
hincrbyfloat user:001 score 0.5

hsetnx 值不存在赋值,存在就无效

1
hsetnx user:001 email 123@123.com

应用场景

image-20251126112928804

早期的中小厂设计

4. 列表(List)

单key多value,底层是双端链表,容量40多亿,主要功能有push和pop等。

对两端的操作性能高,通过索引下标的操作中间的节点性能会较差。

lpush/rpush/lrange

1
2
3
4
5
6
lpush list1 1 2 3 4 5
rpush list2 11 22 33 44 55
type list1
lrange list1 0 -1 5 4 3 2 1
lrange list2 0 -1 11 22 33 44 55
但是没有rrange

lpop/rpop

1
2
3
4
5
lrange list1 0 -1   5 4 3 2 1
lpop list1
lrange list1 0 -1 4 3 2 1
rpop kust1
lrange list1 0 -1 4 3 2

lindex 按照索引下标获得元素 从上到下

1
2
lindex list1 0   4
lindex list1 1 3

llen 获取列表中元素的个数

lrem key 数字N 给定值v1 解释(删除N个值等于v1的元素)

1
2
list里面是允许有重复元素的,如果要去重
lrem list1 2 v1 删除list1里面2个v1值

ltrim key 开始index 结束 index 解释 截取指定范围的值后再赋值给key

1
2
3
lrange list1 0 -1  5 4 3 3 2 2 2 1
ltrim list1 4 7
lrange list1 0 -1 2 2 2 1

rpoplpush 原列表 目标列表

image-20251126000050478

1
2
rpoplpush list2 list3  从list2除了14
lrange list3 0 -1 然后14 1 2 3 4

消息队列了可以当做

lset key index value

1
lset list1 1 mysql 在list下标1设mysql

linset key before/after 已有值 插入的新值

1
linsert list1 before mysql java  在mysql前面加

应用场景

微信公众号订阅文章

1、作者发布了文章11和22

2、关注了他们的文章装进list,lpush likearticle 11 22

3、查看自己订阅的文章 lrange likearticle

5 集合(Set)

set和java中集合hashset一样。单值,多value,无重复

sadd key menber / smembers key / sismember key member

1
2
3
sadd set1 1 1 1 2 2 2 3 4 5 这自动去重,只加了5个元素
smembers set1 查看set1中所有元素
sismember set1 x 查看set1是否存在 x

srem set1 删除元素

1
srem set1 1

scard set1 统计元素个数

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

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

smove key1 key2 【key1里的value】在key1里已存在的某个值赋给key2

集合运算 差并交

1
2
3
4
5
6
sadd set1 a b c 1 2
sadd set2 1 2 3 a x
sdiff set1 set2 //只属于a 不属于 b 的
sunion set1 set2 //两个集合合并
sinter set1 set2 //两个集合交集

sintercard

redis7新命令,他不返回结果集,只返回结果的基数。返回由所有给定集合的交集产生的集合的基数

交集的个数其实

1
2
sintercard 2 set1 set2  求2个key 分别是set1和set2,然后求去重后的交集个数
sintercard 2 set1 set2 limit 4 返回交集个数最多返回4否则就返回原来个数

应用场景:你可能认识的人,社交,推荐等

image-20251126124708446

6 有序集合 zset

在set基础之上,每个val值前加一个score分数值,排行榜用这个数据结构很方便的

image-20251126125637560

zadd / zrange / zrevrange / zrangebyscore

1
2
3
4
5
6
7
zadd zset1 60 v1 70 v2 80 v3 90 v4 100 v5
zrange zset1 0 -1 显示所有value
zrange zset1 0 -1 withscores 带着score显示value
zrevrange zets1 0 -1 withscores 反转也就是倒序输出
zrangebyscore zset1 60 90 包含60到90的score的排序
zrangebyscore zset1 (60 90 withscores 不包含60 包含90 的排序
zrangebyscore zset1 (60 90 withscores limit 0 1 不包含60包含90然后显示从第0个开始然后显示1个

zscore / zcard / zrem / zincrby / zcount / zmpop

1
2
3
4
5
6
zscore zset1 v1 获取元素的分数
zcard zset1 获取zset1的元素
zrem zset1 v5 删除元素
zincrby zset1 3 v1 给v1加三分
zcount zset1 60 100 60分到100分有多少个元素
zmpop 1 zset1 min count 1 弹出zset里面1个元素然后弹出最小的一个 结果回返回 zset1 v1 60

zrank / zrevrank

1
2
zrank zset1 v2    0  获得zset1的v2的下标值 zrank是0    正序的0
zrevrank zset1 v2 3 获得zset1的v2的下标值 zrevrank是3 倒序的3

应用场景

热销的商品,热销的主播打赏

image-20251126194035618

7 位图 bitmap

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

钉钉打卡上下班,电影、广告是否被点击播放过,用户是否登陆过,签到统计

一个字节(一个byte) 8位,底层是个String类型的数组,每个二进制位对应一个索引。

Bitmap支持的最大位数是2 的32次方位,用512M内存能存储40多亿数据

setbit / getbit

1
2
3
4
setbit k1 1 1
setbit k1 7 1
get k1 这时候是A 因为实际上bitmap其实是String然后这个对应的Ascll码就是A
getbit k1 0

strlen

1
strlen k1 这时候计算的是字节为单位,不满一个字节算一个字节

bitcount

1
bitcount k1 记录1的个数

bitop

统计连续2天签到用户,做个id和位置的映射

1
2
3
4
5
6
7
8
9
10
11
12
hset uid:map 0 uid1 用户表
hset uid:map 1 uid2
然后做映射表
setbit 20230101 0 1 登录表
setbit 20230101 1 1
setbit 20230101 2 1
setbit 20230102 0 1
setbit 20230102 1 1
现在统计连续两天都登录的用户
bitop and/or/xor/not与或非
bitop and k3 20230101 20230102 把20230101和20230102两天都登录的人数放入k3
bitcount k3 然后就知道连续登录两天的人数了

8 基数统计 hyperloglog

底层是string

统计某个网站的uv,统计某个文章被阅读的uv

uv就是独立访客,一般认为是ip,union vister,同一个ip就是同一个客户

还有用户搜索网站关键词的数量,统计用户每天搜索不同词条个数

在redis,每个hyperloglog健只需要花费 12kb内存,就可以存储2的64次方个不同的元素的基数。32次方就40亿了。

pfadd / pfcount / pfmerge

1
2
3
4
pfadd hll01 1 3 4 5 7 9 在hll01加元素 1 3 4 5 7 9
pfadd hll02 2 4 4 6 8 9 在hll02加元素 2 4 6 8 9
pfmerge distResult hll01 hll02 把两个key的元素加入distResult这个key里面
pfcount disResult 计算disResult里面的元素 1 2 3 4 5 6 7 8 9 一共9个元素

9 地理空间 GEO

地球上的地理位置是二维的经纬度表示,然后确定一个经纬度就可以确定位置经度(-180,180] , 维度(-90,90],但是如果要实现找附近的人,如果用二维坐标,误差会很大,并且范围查找会很慢。

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

主要分三步,将三维的地球转变为二维的坐标,二维变一维,最后将一维点转换二进制base32编码

底层是zset

geoadd 添加经度维度坐标

1
2
3
4
geoadd [key] [经度] [维度] [位置名称] 具体的经纬度百度获取
zrange city 0 -1
如果中文乱码先quit 然后
redis-cli -a [密码] - - raw 这样启动客户端就能避免中文乱码

geopos 返回经纬度

1
geopos city 天安门 故宫 长城 然后就会返回这三个地点的经纬度

grohash 返回坐标的hash表示

小数点后面很多位,所以在写程序的时候非常不好表示,所以用个映射表示对应的经纬度

1
geohash city 天安门 故宫 长城  然后经纬度就变成字符串了

geodis 两个位置的距离

1
geodis city 天安门 长城 km 后面的单位可以更换,返回距离

georaduis 以半径为中心查找附近的xxx

这里返回的是与中心的距离不超过给定最大距离的所有位置元素

1
2
3
4
5
6
7
8
georadius city [自己的经度] [自己的维度] 10 km withdis withcoord count 10 withhash desc 

GEORADIUS city 116.4 39.9 10 km WITHDIST WITHCOORD COUNT 10 WITHHASH DESC
1) 1) "天津" 城市
2) "9.8765" 距离的距离
3) 1) "117.2000" 天津的经度
2) "39.1000" 天津的维度
4) "4018921763923247" 天津的geohash值

image-20251127123133054

georadiusbymember 跟上面类似,但是这个不需要知道经纬度,只要知道地点名称即可

1
GEORADIUSBYMEMBER city "北京" 200 km 10 km WITHDIST WITHCOORD COUNT 10 WITHHASH DESC

10 redis 流 stream

redis的stream就是redis版本的MQ,消息中间件。

MQ 有很多 kafka RabbitMQ rocketmq

redis消息队列有2个方案,用List实现 lpushrpop 如果消息队列及其简单用redis就能解决

通常用作异步队列,通信这块,点对点。但是一对多力不从心

然后就有pub和sub

image-20251127133211608

但是消息无法持久化,网络断开、redis断开,消息就会丢弃,也没有ack机制来保证数据的可靠性。

基于此上面这些痛点,新增了Stream希望能解决这些问题

所以stream流就是消息中间件 + 阻塞队列

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

stream的结构

image-20251127133542574

1、message content 消息内容

2、消费组 同一个消费组有多个消费者

3、last_delivered_id 游标,每个消费组有个游标,任何一个消费者读取了消息会使游标往后移动

4、consumer 在消费组中的消费者

5、pending_ids 消费者会有个状态变量,记录当前消费但是没ack的消息的id,如果客户端没有ack,这个消息id会越来越多,一旦ack才会减少。

命令

xadd 添加消息到队列末尾

消息id必须比上一个id大,默认用星号表示自动生成id,类似于自增主键

1
2
xadd mystream * id 11 cname z3
xadd mystream * id 12 cname l4

image-20251127134603362

xrange

1
2
xrange mystream - + 把消息队列从小到大返回
xrange mystream - + count 1 从小到大返回1条

xrevrange

1
xrevrange mystream + 1 意思是消息队列从大到小倒序返回

xdel

1
xdel mystream [时间] 是按照id删也就是主键删

xlen

1
slen mystream 返回消息个数也就是key的个数

strim

1
2
xtrim mystream maxlen 2 也就是按照最大长度 截取2条信息如果有四条信息,那么只会剩下下面两条
xtrim mystream minid [时间戳] 比这个时间戳小的都会被删,这里的小是先入队的而不是数值上的小

xread

block没写非阻塞,否则是阻塞

1
2
3
4
5
6
7
8
xadd mystream * k2 v2
xadd mystream * k3 v3
xadd mystream * k4 v4
xadd mystream * k5 v5
xread count 2 streams mystream $ 读取2条比最后加入的还后的信息 返回nil
xread count 2 streams mystream 0- 0 或者写000 读取2条比最早加入的后面的两条信息包括最早加入的信息
xread count1 block 0 streams mystream &
无限等待block 0 也可以写block 5000 最多5秒的意思,然后新开一个客户端加消息,然后这边就会返回

image-20251127154423422

消费组的相关指令

首先$ 表示从Stream尾部开始消费

0表示从Stream头部开始消费

创建消费者组的时候必须指定 ID,ID为0表示从头开始消费,为 $ 表示只消费新的消息,队尾新来

xgroup create

1
2
xgroup create mystream groupX $
xgroup create mystream groupA 0

xreadgroup group

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> 表示从第一条尚未被消费的消息开始读取
消息组groupA内的消费者consumer1从mystream消息队列中读取所有信息
但是,不同消费组的消费者可以消费同一条消息
xadd mystream * k2 v2
xadd mystream * k3 v3
xadd mystream * k4 v4
xadd mystream * k5 v5
xgroup create mystream groupX $
xgroup create mystream groupA 0
xgroup create mystream groupB 0

xreadgroup groupA consumer1 streams mystream > 这个conusmer1是动态创建的,不是预先存在的
stream中的消息一旦被消费组里的一个消费者读取了,就不能被该组内的其他消费者读取。但是不同消费组可以消费。
让组内的多个消费者共同分担读取消息,所以我们通常会让每个消费者读取部分消息,从而实现消息读取负载在多个消费者间是均衡分布的
xreadgroup group groupB consumer1 count 1 streams mystream >
xreadgroup group groupB consumer2 count 1 streams mystream > 等等每人读一条

重点问题

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

刚才只是读了但是还没ack,还没签收,Streams会自动使用内部队列也称为PENDINGList 留存消费组里每个消费者读取的消息保底措施,直到消费者使用XACK命令通知Streams 消息已经处理完成

image-20251127184041160

xpending

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
xpending mystream groupc  看看消费组里已读未确认的清单
首先会返回所有消费者读取的消息最小ID
然后返回所有消费者读取的消息最大ID
然后返回每个消费者读取的个数
XPENDING mystream groupC - + 10

1) 1) "1710000000000-0" # 消息 ID
2) "consumer1" # 被哪个消费者读取
3) (integer) 123456 # idle 时间(毫秒,从交付到现在)
4) (integer) 1 # delivery counter(重试次数)

2) 1) "1710000000001-0"
2) "consumer1"
3) (integer) 98765
4) (integer) 1

3) 1) "1710000000002-0"
2) "consumer2"
3) (integer) 45678
4) (integer) 1

xpending mystream groupC - + 2 consumer2 这样看固定消费者读取的消费信息

XACK

1
XACK mystream groupc [时间戳] 这样就确认签收了

11 位域 bitfield

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

Redis持久化策略

​ Redis不仅仅是一个内存型数据库,还具备持久化能力。

持久化两个策略RDB AOF,RDB就是做一个快照,AOF就是把写操作都记录在文档里

1. RDB

以指定的时间间隔执行数据集的时间点快照,即便redis故障,快照文件也不会丢失,数据的可靠性得到保证。这个快照以dump.rdb。保存的是 全量快照

redis6以下,自动触发,每个900s,如果有超过1个key发生变化,就写一份新的RDB文件

每隔300s,如果有超过10个key发生了变化,就写一份新的RDB文件

每隔60s,如果超过10000个key发生了变化,就写一份新的RDB文件

redis7以后,3600s以内,1个修改,300s以内100个修改,60s内10000个修改

rdb的操作分为自动触发和手动触发

这里修改配置文件来自定义

下面是修改配置文件的写法,位置自己找,以后做redis集群需要

1
2
3
save 5 2                修改快照频率这个5秒内如果有累计2次修改以上包括两次就会触发,如果增加k3然后5秒内不会新增,然后5秒后新增k4之后也会保存。
dir /myredis/dumpfiles 修改保存路径
dbfilename dump6379.rdb 修改保存的文件名

在redis-cli内可以通过下面的命令查看redis信息

1
2
3
config get requirepass 查看密码
config get port 查看端口号
config get dir 查看快照保存的路径

rdb模式是默认模式,可以在指定的时间间隔内生成数据快照(snapshot),默认保存到dump.rdb文件中。当redis重启后会自动加载dump.rdb文件中内容到内存中。

恢复

将备份文件移动到redis安装目录并启动服务即可。也就是说将备份文件放到配置里面备份的路径,redis启动的时候会自动恢复

首先如果shutdown就会保存一次,并且调用flushall的时候也会更新到dump.db里面

所以不可以把备份文件和生产redis服务器放在同一台服务器,必须分开各自存储,以防备份文件也没了

手动触发 save / bgsave

redis会forks ,然后就会有个子的和父进程。然后后台子进程会覆盖rdb了。

生产上只允许用bgsave不许用save

因为save会阻塞当前redis服务器,直到持久化工作完成,redis不能执行其他命令。

1
2
3
4
5
6
save
bgsave 这个是在后台进行异步的快照操作,不阻塞。覆盖老的rdb

lastsave 通过这个命令获取最后一次快照的时间,这个时间戳要在Linux命令

date -d @刚刚的时间戳 就能直到时间了

优点和缺点

适合大规模的数据恢复,定时备份,内存加载速度快,对一致性要求不高。

rdb在没有正确关闭的时候会丢失部分数据。fork可能会很耗时如果数据量大。

检查并修复rdb文件

1
2
cd /usr/local/bin  类似于programfile然后里面有redis-check
redis-check-rdb /myredis/dumpfiles/dump6379.rdb

哪些情况会触发RDB快照

1、配置文件中默认的快照配置

2、手动save/bgsave

3、执行flushall / flushdb 命令但里面是空的无意义

4、执行shutdown且没有设置开启AOF持久化

5、主从复制时,主节点自动触发

rdb优化配置项

1
2
3
4
5
6
7
8
save <seconds> <changes>
dbfilename
dir
stop-writes-on-bgsave-error 默认yes 当bgsave error的时候停止写
如果配置no表示你不在乎数据不一致或者有其他的手段发现和控制这种不一致,那么在快照写入失败时,也能确保redis继续接受新的写请求
rdbcompression 默认yes 对于存储到磁盘中的快照,可以设置是否进行压缩存储
rdbchecksum 默认yes 在存储快照后,进行CRC64进行数据校验,会增加10%性能消耗
rdb-del-sync-files 默认no 在没有持久性的情况下删除复制中使用的RDB文件,没必要删

如何禁用快照

1
2
动态的停止RDB redis-cli config set save ""  本次cli禁用
配置文件禁用 在save "" 写个空字符串即可

2 AOF

​ AOF(Append Only File)把Redis执行过的所有写指令记录下来,读操作不记录、redis启动之初会读取该文件重新构建数据。

默认情况下,redis没有开启,要开启需要在配置文件中添加appendonly yes

1
2
3
4
# 默认no
appendonly yes
# aof文件名
appendfilename "appendonly.aof"

AOF工作流程

image-20251127233133875

AOF三种写回策略

Always / everysec / no

1
2
3
appendfsync everysec 在配置文件中,每秒写回,先把日志写到AOF缓冲区,每隔1秒写回aof文件中
Always是同步写回,每个写命令,立刻同步写回
no 操作系统控制的写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时写回

AOF的配置文件说明

Redis7新特性 Multi-part AOF

image-20251127234924610

aof的保存路径

redis6是 AOF 保存文件的位置和 RDB 文件位置是一样的,都是dir中配置的

1
2
3
4
dir /myredis

/myredis/dump.db
/myredis/appendonly.aof

redis7是 有dir + appenddirname

1
2
3
4
5
dir /myredis
appenddirname "appendonlydir"

/myredis/dump.rdb
/myredis/appendonlydir/xxx.aof

aof保存的名称

redis6 只有叫appendonly.aof

redis7 Muti Part AOF设计,多部分组成aof 对外名字还叫appendonly.aof 但是实际上分为了三个文件组成

image-20251128000800062

base 基本文件

incr 增量文件

manifest 清单文件

image-20251128000904887

尝试利用 AOF 恢复流程

写操作继续,然后生成AOF文件到指定目录

恢复1 : 重启redis然后重新加载,结果OK

恢复2:写入数据进redis,然后flushdb+shutdown服务器,新生成了dump和aof,备份新生成的aof.bak,然后删除dump/aof再看恢复,重启redis然后加载,停止服务器,拿出我们的备份修改后再重启服务器

上面都是正常状况

异常恢复 假设内容只写了一小半,没写完整,redis挂了,导致aof文件错误

1
利用命令 redis-check-aof --fix 进行修复

aof重写机制

当aof的文件越来越大,不断记录写命令,AOF恢复要求的时间越来越长。

只保留可以恢复数据的最小指令集,或者手动使用命令来重写

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

手动的话客户端向服务器发送bgrewriteaof 命令

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

重写原理

image-20251128010832943

Redis 7 的 AOF 重写通过 fork 子进程将内存数据转为紧凑命令写入 base 文件,父进程将重写期间的新命令写入新的 incr 文件,最后通过 manifest 清单将 base + incr 组合成逻辑上的完整 AOF,全程无需覆盖或删除任何现有文件。

Redis 7 的 AOF 重写全程在新文件上操作,旧 AOF 文件保留且不被覆盖。
旧文件在不再被 manifest 引用后,由 Redis 后台异步清理,确保高可用与数据安全。

总结配置

image-20251128014335015

3 RDB + AOF 混合

如果开启了aof,会优先加载aof,其次再加载rdb文件来恢复。

开启混合配置

1
2
aof-use-rdb-preamble yes
开启后rdb做全量,aof做增量

这时候产生的文件一部分是rdb,一部分是aof。

如果用纯缓存模式,同时关闭RDB和AOF

1
2
3
在配置里面
save "" 禁用rdb
appendonly no 禁用aof

Redis 事务

事务就是一组命令的集合,要么一起成功,要么一起失败。

在一个队列中,一次性,顺序性,排他性的操作

Redis事务和数据库的事务区别

1、Redis事务是单独的隔离操作:Redis事务仅仅保证事务的操作会被连续独占执行,执行完事务内的所有指令不可能再去同时执行其他客户端的请求。

2、没有隔离级别的概念:因为事务提交前任何指令都不会被实际执行,也就不存在“事务内的查询要看到事务里的更新,在事务外查询不能看到”这种问题

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

4、排他性:Redis会保证一个事务内的命令一次执行,而不会被其他命令插入

但是使用 Redis 事务 确实存在数据丢失或数据不一致的风险,mysql不会只要做好配置。

常用命令

Discard 取消事务,Exec 执行所有事务块内的命令

Multi 标记一个事务块的开始

Unwatch 取消watch命令对所有key的监视

Watch key 监视一个或多个key,如果事务执行之前这些key被其他命令所改动,那么事务将被打断

case1 正常执行

1
2
3
4
5
multi
set k1 v1
set k2 v2
incr count
exec 这里multi之后的命令还没执行只是放入队列中,exec才是最终执行

case2 放弃执行

1
2
3
4
5
multi
set k1 v1
set k2 v2
incr count
discard 这时候队列里的命令全部放弃执行

case3 全体连坐

1
2
3
4
5
6
如果事务出错全部不执行,这个是在执行exec之前才会发生的
multi
set k1 v1
set k2
exec
那么这时候全部命令都不执行

case4 冤头债主

1
2
3
4
5
6
如果事务exec后执行异常,一个命令失败,其他命令还是继续执行
multi
set k1 v1
incr email
exec
这时候eamil执行出错但是k1 v1还是设置成功了

case5 watch监控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Redis使用Watch来提供乐观锁定,类似于CAS。
悲观锁是Synchronized lock unlock ,很悲观,每次拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block知道它拿到锁
乐观锁,顾名思义,就是很乐观,每次拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。乐观锁策略,提交版本必须大于记录当前版本才能执行更新


set k1 v1
set balance 100

watch balance
multi
set k1 v2
set balance 200
exec
这时候很正常执行成功

multi
set k1 v3
set balance 300
这时候另一个客户端
set balance 400
回到原来客户端
exec
这时候整个事务全部不执行,这就是乐观锁的效果

unwatch就是取消watch就好了 UNWATCH 会立即清除当前连接上所有的 WATCH 监控。

Redis管道

处理问题的结果跟事务类似,但是完全不一样。

如何优化频繁命令往返造成的性能瓶颈?Redis每秒钟可以8万次写,10万次读,但是一条命令需要返回一个OK,这次想能不能批处理这时候想到mset,这个mset就类似于管道,把所有命令排成一个流水线,一次性操作。

image-20251128153519036

image-20251128153721949

解决思路

然后就引出了管道的概念,一次发送多条命令给客户端,只有一次相应,鉴赏通信次数,实现降低往返延时时间。

案例

1
2
3
4
5
6
7
8
首先写一个cmd.txt然后里面写着
set k1 v1
hset k3 name z3
hset k3 age 20
lpush list 1 2 3 4
这些命令
然后再linux中使用命令
cat cmd.txt | redis-cli -a [密码] -- pipe 然后就能使用管道了

管道与原生批量命令对比

原生就是mset那些。首先原生批量命令是原子性的,但是管道不是原子性的,其次原生批量命令一次只能执行一种类型,管道可以各种类型的命令,原生是服务端实现的,管道是服务端和客户端共同实现的。

管道与事务

事务具有原子性,管道不具有

管道一次性将多条命令发送到服务端,事务是一条一条发收到exec命令才会执行

执行事务会阻塞其他命令的执行,管道中的命令不会

使用管道注意的事项

管道缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令

使用管道组装的命令不能太多没不然数据量过大,客户端阻塞的时间可能会过久。

Redis 发布订阅

image-20251128193305939

不推荐使用。

Redis主从复制

HA high application 高可用

master以写为主,slave以读为主。

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

能干嘛?

1、读写分离

2、容灾恢复

3、数据备份

4、水平扩容支撑高并发

怎么用

配从库不配主库,一个主库,两个从库

权限细节

单机配置了requirepass,需要密码登录,那么slave就要配置masterauth来设置校验密码,否则master就会拒绝slave的访问请求

1
masterauth [密码]

常用操作命令

1
2
3
4
info replication           可以查看复制节点的主从关系和配置信息
replicaof 主库IP 主库端口 一般写入进redis.conf配置文件内,设置主redis
slaveof 主库IP 主库端口 如果从机跟master断开之后都需要重新连接,可以用命令重新认定新的主redis
slaveof no one 停止当从数据库,自己当主

一主多从搭建

3台虚拟机,都安装redis

1
拷贝多个redis.conf文件,分别为redis6379.conf、redis6380.conf、redis6381.conf

首先保证三边的网络相互ping通,且注意防火墙配置

1
2
3
4
5
6
ifconfig 查看ip
ping 三边ip
防火墙设置白名单
然后在从库redis.conf配置 replicaof 主机ip 主机端口
如果需要 在redis-cli用指令动态换主库 用slaveof 新主库ip 主库端口
如果需要自己当主机 在redis-cli用指令 用slaveof no one

配置从redis为例子

1
2
3
4
5
6
7
8
9
10
11
开启daemonize yes    后台运行yes,大概309行
注释bind 127.0.0.1 绑定ip注释掉
protected-mode no 保护模式关闭
指定端口 port 6379/6380/6381
指定当前工作目录 dir dir /myredis 配置文件路径,用绝对路径
pid文件名字,pidfile 大概341行默认就行但是要知道在哪
log文件名字,logfile 大概354行默认就行但是要知道放在哪和级别 "/myredis/6379.log"
requirepass 本机密码
dump.rdb名字 大概482行dbfilename,dump6379.rdb
aof文件,appendfilename 需要开启appendonly yes可开启
从机访问主机的通行密码masterauth,从机要配置,主机不用大概582行

搭建好之后

1
2
3
4
先启动主,后两台从再开启
redis-server /myredis/redis6379.conf
redis-cli -a [密码] -p 6379
如果出错可以看刚刚的日志文件中查看问题

从机可以写命令吗?不可以

那主机写到了k3然后从机启动了,这时候从机启动,从机会读k1和k2吗?会

主机shutdown,从机会当主机吗?不会,从机不动原地待命

主机shutdown后,重启后,能恢复吗?能

从机shutdown后,master继续,从机恢复能跟上吗?能

薪火相传

上一个slave可以是下一个slave的master,slvae同样可以接受其他slaves的连接和同步请求,那么该slave可以减轻主master的写压力。

中途变更转向:会清除之前的数据,重新建立拷贝最新的。slaveof 新主库ip 新主库端口

这种配置一个主master,然后一个slave,一个slave的slave,这样首先主master写操作,两个slave都能读到,然后这时候两个slave还是不能写操作。

反客为主

slaveof no one然后自己就变成master了

面试题复制原理和工作流程

slave启动,同步初请:首先slave连接到master后会发送一个sync命令,初次连接master,第一次完全同步全量复制将自动执行,slave自身原有的数据会被master数据覆盖清除。

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

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

心跳持续,保持通信: master发出PING包周期,默认是10秒,每十秒中确认是否连接还在。

进入平稳,增量复制:Master继续将新的所有收集到的修改命令自动依次传给slave,完成同步。

从机下线,重连续传:假设有一台机器down了,master会检查backlog里面的偏移量offset,master和slave都会保存一个复制的offset还有一个masterId,offset是保存在backlog中的。Master只会把已经复制的offset后面的数据复制给slave,类似断点续传

复制的缺点

复制延时,信号衰减

image-20251129012204047

master挂了怎么办

默认情况下,不会在slave里面选一个master,那每次都要人工干预?

无人值守安装成了刚需,那么就引出了哨兵和集群。

哨兵(Sentinel)

​ 在redis主从默认是只有主具备写的能力,而从只能读。如果主宕机,整个节点不具备写能力。如果故障了根据 投票数 自动将某一个从库转换为新主库,继续对外服务。

作用:无人值守运维,监控redis运行状态,当master宕机能自动将slave切换成新master

能干嘛?

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

2、消息通知:哨兵可以将故障转移的结果发送给客户端

3、故障转移:如果Master异常,则会进行主从切换,将其中一个Slave作为新Master

4、配置中心:客户端通过连接哨兵来获得当前Redis服务的主节点地址

Redis Sentinel架构

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

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

6台机器。

image-20251129082634420

实战步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/myredis目录下新建或者拷贝sentinel.conf文件,名字绝不能错
先看看/opt目录下默认的sentinel.conf文件的内容
重点参数
bind 服务监听地址,用于客户端连接,默认本机地址
daemonize 是否以后台daemon方式运行
protected-mode 安全保护模式
port 端口 默认端口26379
logfile 日志文件路径
pidfile pid文件路径 用于存储进程 ID(PID)的文本文件
dir 工作目录
sentinel monitor <master-name> <ip> <redis-port> <quorum投票数>
设置要监控的主机,quorum:确认客观下线的最少的哨兵数量,在案例中3票2票觉得挂机了才下线以及故障转移。
sentinel auth-pass <master-name> <password>
master设置了密码,连接master服务的密码


sentinel文件通用配置
bind 0.0.0.0
daemonize yes
protected-mode no
port 26379
logfile "/myredis/sentinel26379.log"
pidfile /var/run/redis-sentinel26379.pid
dir /myredis
sentinel monitor mymaster 192.168.111.169 6379 2
sentinel auth-pass mymasster 111111

先启动一主二从3个redis实例,测试正常的主从复制
==============下面是哨兵部分===============
在启动3个哨兵,完成监控
启动哨兵 redis-sentinel sentinel26379.conf --sentinel还有其他两个都启动
启动3个哨兵监控后再测主从复制
原有的master挂了
这时候添加哨兵后,两台从机数据都能读到,之前是不行的
这里有个小问题就是当主机挂了之后不久获取数据的时候会出现Broken Pipe问题或者出现ServerClosed问题

那么这时候是80当master还是81 ?当主机挂了,哨兵会投票选举。
首先是主观下线。单个sentinel认为节点崩溃了,然后投票客观认为下线了、然后故障迁移,首先选举一个Leader sentinel来执行故障迁移,从slaves选一个提升为新master,通知其他slaves复制新master,更新配置通知客户端。
如果原master恢复了,那就自动变成slave。所以不会有双master冲突

后序6379可能变成从机,那么这时候需要设置访问新主机的密码masterauth
对比配置文件
文件的内容在运行期间会被sentinel进行动态修改
主从切换后,主从监视三个配置文件的内容都会发生改变。

一些其他的参数

image-20251129084916925

哨兵运行流程和选举原理

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

详细

1、三个哨兵监控一主二从,正常运行中

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

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

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

默认30秒

4、选一个领导者哨兵,然后由该哨兵来进行故障迁移(switch master 添加slave)

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

image-20251130175258863

5、由领导者来推动切换流程,并选出一个新master

​ 首先新master

​ 某个Slave被选中成为Master,选出新Master的规则,先看优先级数字越小越高,谁高谁当master,再看复制偏移量(谁复制的多),谁大谁当,最后看run id ,谁最小谁当Run ID 是 Redis 实例启动时自动生成的唯一标识符

​ 然后slave切换master

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

​ 最后旧master认新master

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

redis配置注意事项

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

​ 哨兵节点的数量应为奇数。

​ 各个哨兵的配置应该一致

​ 如果哨兵布置在Docker容器里面,要注意端口的正确映射

​ 哨兵集群 + 主从复制,并不能保证数据零丢失

那么由于不能保证数据零丢失,那么就引出下面的集群了

集群(Cluster)

先回顾哨兵 + 主从

image-20251130223726144

这里有个Bug就是当主机挂了重新选举的时候,写操作被中断,一瞬间写操作数据流失、所以单点高并发只有一台主机,所以单个Master难以承担,因此需要多个复制集进行集群。

image-20251130231500637

现在就变成集群了。Redis集群是共享数据的,M1有数据,M2,M3也会有,写的时候全局数据共享形成程序集。

能干嘛

1、Redis集群支持多个Master,每个Master又可以挂载多个Slave

2、由于集群自带Sentinel的故障转移机制,内置了高可用支持,所以 无需再去使用哨兵功能

3、客户端与Redis的节点连接,不再需要连接集群中所有的节点,只需要任意连接集群中的一个可用节点即可。

4、 槽位Slot 负责分配到各个物理服务节点,由对应的集群来负责维护节点、插槽和数据之间的关系

集群算法-分片-槽位slot

redis集群的槽位slot

​ Redis 集群没有使用一致性hash,而是用hash槽的概念,槽位最多16384,集群要1000个以内。

那么进行写操作的时候,会对每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽。

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

image-20251201014649059

分片

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

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

那这两个概念的引入有什么优势

1、方便找

2、方便扩容和缩容:在增加master或者减少master的过程中只是运了一部分槽给新增的或者减少的master而已,并不会停机,这就是高可用

slot槽位的映射

1、哈希取余分区 (小厂):直接hash(key) % N个机器数,简单粗暴,直接有效。但是原来规划好的节点,进行扩容或者缩容就比较麻烦了,因为分母写死了,映射关系需要重新计算,此时取余计算的结果发生很大变化,全部数据非常混乱,重新洗牌变更。

2、一致性哈希算法分区 (中厂)

​ 为了解决分母数量变动取余不行的问题。当服务器个数变动时,尽量减少映射的影响。

​ 1、算法构建一致性哈希环

image-20251201020858141

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

他也是按照取模的方法。前面是对服务器数量取模,而一致性hash算法是对2的32次方取模,整个空间 按顺时针方向组织

​ 2、服务器IP节点映射

将集群中各个IP节点映射到环上的某一位置。具体可以用IP或者主机名作为关键字进行哈希

​ 3、key落到服务器的落键规则

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

image-20251201022033786

优点: 容错性!扩展性!

容错性

image-20251201022414716

这里C服务器挂了,C节点继续往前走到了D。这时候A\B\D是不受影响的,如果一台服务器不可用,受影响的数据仅仅是此服务器到其换空间中前一台服务器之间的数据(也就是逆时针走遇到第一台服务器),其他的不会受到影响。简单说就是C挂了,收到影响的只是B\C之间的数据,且这些数据会转移到D进行存储

扩展性

image-20251201022755334

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

缺点:一致性hash算法的数据倾斜问题

当服务节点太少的时候,容易因为节点分布不均匀而造成的数据倾斜问题。

image-20251201023011700

3、哈希槽分区 (大厂)

是什么?CRC16(KEY) mod 16384 = HASH_SLOT

为了解决数据倾斜问题。哈希槽其实是2的16次方也就是16384的数组

能干什么?

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

image-20251201023759548

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

多少hash槽?

一个集群只能有16384个槽,编号从0 开始,这些槽会分配给集群中的所有主节点,分配策略没有要求。

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

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

1、如果槽位是65536,发送心跳消息的消息头达8K,发送的心跳包过于庞大。

因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位是65536,这个ping消息的消息头太大了,浪费带宽。这个ping消息是以bitmap也就是01的map形式传递

  • 如果有 65536 个槽,那么位图长度为:
    65536÷8=8192 字节=8 KB
  • 16384 个槽 对应的位图长度为:
    16384÷8=2048 字节=2 KB

2、redis的集群主节点数量基本不会超过1000个

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

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

Redis主节点的配置信息中他所负责的哈希槽是通过一张bitmap的形式来存储的,在传输过程中会对bitmap进行压缩,但是如果Bitmap的填充率 slots / N 很高的话,bitmap的压缩率就很低。如果节点数很少,哈希槽数量很多的话,bitmap的压缩率就很低。也就是说尽量让槽位和节点数接近是最大效率的,那么节点数由于不会超过1000那么16384就刚好了。

Redis不保证强一致性,也就是在特定情况下,还是会丢失一些写命令

集群环境搭建

1、3主3从redis集群配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ip + 端口 这时候让一主一从在一台服务器上,也就是这时候模拟3台服务器形成的集群。

首先 mkdir -p /myredis/cluster
vim /myredis/cluster/redisCluster6381.conf
vim /myredis/cluster/redisCluster6382.conf

bind 0.0.0.0
daemonize yes
protected-mode no
port 6381
logfile "/myredis/cluster/cluster6381.log"
pidfile /myredis/cluster6381.pid
dir /myredis/cluster
appendonly yes
appendfilename "appendonly6381.aof"
requirepass 密码
masterauth 密码

cluster-enabled yes
cluster-config-file nodes-6381.conf
cluster-node-timeout 5000
其他配置也类似

然后启动6台redis主机实例
redis-server /myredis/cluster/redisCluster6381.conf
。。。

然后通过redis-cli命令为6台机器构建集群关系
构建主从关系命令
redis-cli -a 密码 --cluster create --cluster-replicas 1 ip:端口(整个集群都写上)
然后这时候会显示一连串的,谁是主谁是从,然后回答yes就好了

然后进入客户端
redis-cli -a 密码 -p 端口
然后运行
cluster nodes 查看节点关系
info replication 查看当前节点的关系
cluster info 查看节点关系

2、3主3从读写

1
2
3
4
5
6
7
8
9
10
11
12
这时候写
set k1 v1 出错了
set k2 v2 成功了
那么这时候部分支持,部分不支持,因为这时候是不对的,因为槽位的限制,需要 路由到位
那么为了防止路由失效增加参数 -c
redis-cli -a 密码 -p 端口 -c
set k1 v1
这时候成功了,但是是重定向到其他redis了。
然后去其他的redis
get k1 那么就找到v1了

通过cluster keyslot (key)这样就能知道key属于哪个槽位

3、主从容错切换迁移

1
2
3
4
5
6
7
8
9
10
11
12
主6381宕机,从机会不会上位
然后把6381手动shutdown
然后看对应从机什么情况
这时候cluster nodes 发现slave升级了。这时候从机 会上位
那这时候原主机恢复了,会如何?这时候原主机就变成从机了,跟哨兵效果一样

集群不保证数据一致性,一定会有数据丢失的情况。会丢但是不多。

手动故障转移 或者 节点从属调整该如何处理?
这时候登入slave机器,然后用命令
cluster failover
这样就把从机恢复成为主机了

4、主从扩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
新启动两个redis实例,然后利用redis-server启动两个实例,这时候两个都是master
然后将其中一台作为master加入原有集群
redis-cli -a 密码 --cluster add-node 自己实际IP:端口 集群里master的IP:集群里master的端口

然后这时候新加入的节点没有被分配slot
然后这时候需要重新分配slot
然后这时候需要用命令重新分配槽号
redis-cli -a 密码 --cluster reshard 新加入的ip:新加入的端口号
然后会问你移多少,要用16384/4然后来计算移动多少
然后会问新加入的master的节点号
然后会问你有什么意见然后 all

分配好后查看分配的slot发现
以前是连续的分区,现在是之前3家各自匀出来一部分空的,现在不是连续的分区了

然后为新的主节点分配从节点
redis-cli -a 密码 --cluster add-node 新slaveip:新slave端口 新masterip:新master端口 --cluster-slave --cluster-master-id 新主机节点ID
然后就主从扩容成功了

5、主从缩容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
首先清除从节点
然后请出来的槽号重新分配给其他master
然后再删除要删除的master
最后恢复3主3从

首先检查集群的情况,获得要删除从节点的节点ID cluster nodes
然后将从节点删除
redis-cli -a 密码 --cluster del-node 从机ip:从机端口 从机节点ID
现在删除了
redis-cli -a 密码 --cluster check 其他masterip:其他master端口
然后清空要删除的master的槽位,全部给其他master
redis-cli -a 密码 --cluster reshard 分配给其他master的ip:其他master的端口
然后找到接受槽位的节点id待会儿要用,然后找到要删除的节点id,然后输入done就分配槽位了

这时候分配的master有两台slave了,之前把slave删除,然后master就变成slave了,之前把slave全分给新master,不然就要写三次了,现在一锅端

然后将变成slave的master删除
redis-cli -a 密码 --cluster del-node 从机ip:从机端口 从机节点ID
这时候就两个都删除干净了

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
在一个redis里面
set k1 v1
set k2 v2
然后
mget k1 k2 k3
然后这时候是拿不到的,不在同一个slot槽位下的键值无法使用mset\mget等多键操作。
# 正确方式(启用集群模式)
redis-cli -c -p 6381 -a 密码
一个一个拿可以,但是如果键在多个槽位就不能一次性拿出

可以通过{}来定义同一个组的概念,使key中{} 内相同内容的键值对放到一个slot槽位去
mset k1{z} v1 k2{z} v2 k3{z} v3
首先这个key是k1{z},然后槽位是根据{z}来决定的,所以说这三个key会在同一个槽位

2、Redis集群里面有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。

集群的每个节点负责一部分hash槽。

3、常用命令

1
2
3
4
5
6
集群是否完整才能对外提供服务  cluster-require-full-coverage
在配置里面这个默认YES,需要集群完整性,才可以对外提供服务,通常在集群中,如果任何一个挂了,对外提供的数据不完整,整个集群是不完整的,redis默认在这种情况下,是不会对外提供服务的。
如果诉求是,集群不完整也需要对外提供服务,需要将参数设置为No。

CLUSTER COUNTKEYSINSLOT 槽位数字编号 查看槽位情况如果是1槽位被占用,0,槽位没占用
CLUSTER KEYSLOT 键名称 该键应该存在哪个槽位上

SpringBoot集成Redis

之前java连接mysql用的驱动包是jdbc,那么java连接redis也有自己的一套驱动包。

总体概述

jedis-lettuce-RedisTemplate三者的关系

jedis 相当于jdbc,最初的驱动包

lettuce 对jedis优化

最后集成RedisTemplate

本地Java连接Redis常见问题

1
2
3
4
5
bind配置注释掉  否则相当于windows系统连接linux系统的
保护模式设置为no
Linux系统的防火墙设置
redis服务器的IP地址和密码是否正确
忘记写访问redis的服务端口号和auth密码

集成Jedis

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

也就是之前用的所有十大类型和命令,可以通过java程序进行调用了

1
2
3
4
5
6
约定大于配置大于编码,编码放在最后
建Module
改POM
写YML
主启动
业务类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//这里是springboot然后这里用来了解Jedis的所以这里不用@Component,用最原始的人工控制
public class JedisDemo{
public static void main(String[] args){
//1 connection获得,通过指定ip和端口号
Jedis jedis = new Jedis("192.168.111.185",6379);
//以前在linux端用redis-cli -a 密码 -p 端口号来连接客户端,这里也类似,已经准备好对应的构造方法了
//2 指定访问服务器的密码
jedis.auth("11111");

//3 获得了jedis客户端,可以像jdbc一样访问redis
System.out.println(jedis.ping());


//keys
Set<String> keys = jedis.keys("*");

//string
jedis.set("k3","v3");
jedis.get("k3");

//list
jedis.lpush("list","1","2","3");
jedis.lrange("list",0,-1);

//设置过期时间
jedis.expire("k3",20L);
jedis.ttl("k3");
}
}

集成lettuce

为什么出现这个?

如果在高并发的环境下,连接jedis每次都new一个客户端,绝对机器承担不住,这样就想到了池化技术。

jedis也支持池化技术,但是lettuce更好的封装所以以后就用lettuce但是被springboot集成了,所以以后就用springboot了。

为什么用lettuce:主要是lettuce底层使用的是Netty,jedis客户端连接Redis服务器的时候,每个线程都要自己创建jedis实例去连接Redis客户端,当有很多个线程的时候,不仅开销大需要反复的创建关闭一个Redis连接,而且也是线程不安全的(一个线程通过Jedis实例更改Redis服务器中的数据之后会影响另一个线程)。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
步骤跟上面一样就是改POM文件然后用lettuce的业务类就可以了
//这里是springboot然后这里用来了解Jedis的所以这里不用@Component,用最原始的人工控制
public class LettuceDemo{
public static void main(String[] args){
// 1 使用构建器链式编程来builder我们RedisURI
RedisURI uri = RedisURI.builder()
.redis("192.168.111.185").
withPort(6379).
withAuthentication("default","11111")
.build();
//2 创建连接客户端
RedisClient redisClient = RedisClient.create(uri);
StatefulRedisConnection conn = redisClient.connect();

//3 通过conn创建操作的command
RedisCommands commands = conn.sync();

//======各种业务逻辑=======
List keys = commands.keys("*");
commands.set("k5","v5");
//跟jedis类似

//4 各种关闭释放资源
conn.close();
redisClient.shutdown();
}
}

集成RedisTemplate

连接单机

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
首先boot整合redis基础
建Module
改POM
springboot和redis整合了那么需要引入包
spring-boot-starter-data-redis 这是 Spring Boot 官方提供的 Redis 集成 starter,用于简化 Redis 的配置和使用。
commons-pool2 Apache 提供的 通用对象池库
springfox-swagger2 SpringFox 是一个为 Spring Boot 应用自动生成 REST API 文档的工具,swagger2 模块用于生成符合 Swagger 2.0 规范 的 API 描述。
springfox-swagger-ui 提供 Swagger UI 可视化界面,将 swagger2 生成的 JSON 文档渲染成交互式网页。

写YML
server.port=7777
spring.application.name=redis7_study
# ======= logging 日志相关=============
logging.level.root=info
logging.level.com.atguigu.redis7=info
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.sss} [%thread] %-51evel %logger- %msg%n
logging.file.name=D:/mylogs2023/redis7_study.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.sss} [%thread]%-5level %logger- %msg%n
#=====swagger=======
spring.swagger2.enabled=true
#在springboot2.6.X结合swagger2.9.x会提示documentationPluginsBootstrapper空指针异常,
#原因是在springboot2.6,X中将SpringMVc默认路径匹配策路从AntPathMatcher更改为PathPatternParser,
#导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

#=======redis单机=========
spring.redis.database=0
#修改为自己真实IP
spring.redis.host=192.168.111.185
spring.redis.port=6379
spring.redis.password=11111
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0

主启动
@SpringBootApplication
业务类
配置类
RedisConfig
Swagger
service
OrderService
controller
OrderController
测试

OrderService

image-20251202233318478

Controller

image-20251202233654676

但是存入的key和读取的key都乱码,需要注意序列化问题

1
2
3
4
5
6
7
8
key和value 都是通过Spring提供的Serializer序列化传到数据库的
RedisTemplate是通过JDK来序列化的
StringRedisTemplate是通过StringRedisSerializer序列化的
所以我们使用StringRedisTemplate(这个是RedisTemplate的子类,子类更完善)

第一种解决方法,直接使用StringRedisTemplate,然后加入的没有序列化问题了,但是在redis客户端查看key的时候是乱码,如果想要redis客户端里查看也是好的话,需要根据添加命令--raw来开启客户端

第二种解决方案,在RedisConfi里面配置RedisTemplate,然后手动控制序列化方式

连接集群

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
启动redis集群6台实例
配置yml文件
server.port=7777

spring.application.name=redis7_study

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

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

# ========================swagger=====================
spring.swagger2.enabled=true
#在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常,
#原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser,
# 导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
spring.mvc.pathmatch.matching-strategy=ant_path_matcher


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

直接通过微服务访问redis集群
如果通过客户端访问
redis-cli -a 密码 -p 端口 -c --raw
问题来了,一个主机意外down了,现在6台机器只剩5台,然后一台成功上位,在服务器里尝试是可以的,但是在客户端里面尝试的时候出错了,报错显示找不到服务器并且超时了。
SpringBoot客户端没有动态感知到Redis集群的最新集群消息。
原因是SpringBoot 2.X 版本,Redis默认的连接池采用Lettuce。当Redis集群节点发生变化后,Lettuce默认是不会刷新节点拓扑
解决方案:
1、用jedis,但是这样不优雅了。
2、重写连接工厂实例,但是这样更难了
3、刷新节点集群拓扑动态感应(官网推荐)
要么调用RedisClusterClient.reloadPartitions
要么修改配置,定时拓扑刷新默认是关闭的,所以需要改配置
改写YML
#支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
spring.redis.lettuce.cluster.refresh.adaptive=true
#定时刷新
spring.redis.lettuce.cluster.refresh.period=2000