NoSQL适用场景

  • 对数据高并发的读写
  • 海量数据的读写
  • 对数据高可扩展性的

NoSQL不适用场景

  • 需要事务支持
  • 基于sql的结构化查询存储,处理复杂的关系,需要即席查询。
  • (用不着sql的和用了sql也不行的情况,请考虑用NoSql)

Mwmcache

  • 很早出现的NoSql数据库
  • 数据都在内存中,一般不持久化
  • 支持简单的key-value模式,支持类型单一
  • 一般是作为缓存数据库辅助持久化的数据库

Redis

  • 几乎覆盖了Memcached的绝大部分功能
  • 数据都在内存中,支持持久化,主要用作备份恢复
  • 除了支持简单的key-value模式,还支持多种数据结构的存储,比如 list、set、hash、zset等。
  • 一般是作为缓存数据库辅助持久化的数据库

MongoDB

  • 高性能、开源、模式自由(schema free)的文档型数据库
  • 数据都在内存中, 如果内存不足,把不常用的数据保存到硬盘
  • 虽然是key-value模式,但是对value(尤其是json)提供了丰富的查询功能
  • 支持二进制数据及大型对象
  • 可以根据数据的特点替代RDBMS ,成为独立的数据库。或者配合RDBMS,存储特定的数据。

Redis概述和安装

概述

  • Redis是一个开源的 key-value 存储系统。
  • 和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set –有序集合和hash(哈希类型)。
  • 这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。
  • 在此基础上,Redis支持各种不同方式的排序。
  • 与memcached一样,为了保证效率,数据都是缓存在内存中。
  • 区别的是Redis会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件。
  • 并且在此基础上实现了master-slave(主从)同步。

安装

安装版本: redis-7.0.12.tar.gz (Linux环境下安装)

步骤

  1. 下载最新版的gcc编译器

    yum install centos-release-scl scl-utils-build

    yum install -y devtoolset-8-toolchain

    scl enable devtoolset-8 bash

  2. 测试

    gcc -version

  3. 下载 redis-7.0.12.tar.gz 后放 /opt 目录

    cd 当前目录
    mv redis-7.0.12.tar.gz /opt
  4. 解压 redis-7.0.12.tar.gz

    tar -zwvf redis-7.0.12.tar.gz
  5. 解压完成后进入目录

    cd redis-7.0.12
  6. 在 redis-7.0.12 目录下再次执行 make 命令(只是编译好)

  7. 如果没有准备好 C 语言编译环境,make 会报错 Jemalloc/jemalloc.h 表示没有这个文件

    解决:运行 make distclean

  8. 在redis-6.2.1目录下再次执行make命令(只是编译好)

    image-20230718095249546
  9. 跳过 make test 继续执行:make install

    image-20230718095342582

安装目录

默认安装到:/usr/local/bin

image-20230718100021517

  • redis-benchmark:性能测试工具,可以在自己本子运行,看看自己本子性能如何
  • redis-check-aof:修复有问题的AOF文件,rdb和aof后面讲
  • redis-check-dump:修复有问题的dump.rdb文件
  • redis-sentinel:Redis集群使用
  • redis-server:Redis服务器启动命令
  • redis-cli:客户端,操作入口

前台启动(不推荐)

前台启动,命令行窗口不能关闭,否则服务器停止

image-20230718100851716

后台启动(推荐)

  1. 备份 redis.conf

    拷贝一份 redis.conf 到其他目录

    cd /opt/redis-7.0.12
    cp redis.conf /myredis/redis7.conf
    1. 后台启动设置 daemonize no 改成 daemonize yes

      修改 redis.conf (128行) 文件将里面的 daemonize no 改成 darmonize yes ,让服务在后台启动

      1. 默认protected-mode yes 改为 protected-mode no
      2. 默认bind 127.0.0.1 改为 直接 注释掉(默认bind 127.0.0.1只能本机访问)或改成本机地址IP地址,否则影响远程IP连接
      3. 添加 redis 密码:改为 requirepass 自己设置密码(先要打开注释)
  2. Redis启动

    redis-server /myredis/redis.conf

    redis-cli -a 密码 进入redis页面

  3. 用客户端访问:redis-cli

    image-20230718101256564

  4. 多个端口可以:redis-cli-p6379

  5. 测试验证:ping

    image-20230718101354992

  6. 关闭Redis

    • 单实例关闭:redis-cli shutdown

      image-20230718101500924

    • 也可以进入终端后再关闭

      image-20230718101451815

    • 多实例关闭,指定端口关闭:redis-cli -p6379 shutdown

Redis介绍

默认16个数据库,类似数组下标从0开始,初始默认使用0号库

  • 使用命令 select <dbid>来切换数据库。如: select 8
  • 统一密码管理,所有库同样密码。
  • dbsize:查看当前数据库的key的数量
  • flushdb:清空当前库
  • flushall:通杀全部库

多路复用是指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行(比如使用线程池)

串行 vs 多线程+锁(memcached) vs 单线程+多路IO复用(Redis)

常用五大数据类型

键(key)

keys * :查看当前库所有key (匹配:keys *1)

exists key :判断某个key是否存在

type key :查看key是什么类型

del key :删除指定的key数据

unlink key :根据value选择非阻塞删除,仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作。

expire key 10 :为给定的key设置过期时间(10秒)

ttl key :查看还有多少秒过期,-1表示永不过期,-2表示已过期

select 0~15:切换数据库0~15,默认数据库是0号数据库

dbsize:查看当前数据库的key的数量

flushdb:清空当前数据库

flushall:通杀全部数据库

字符串(String)

  • String是Redis最基本的类型,可以理解成与Memcached一模一样的类型,一个key对应一个value。

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

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

常用命令

set <key> <value> :添加键值对

*NX :当数据库中 key 不存在时,可以将key-value添加数据库

*XX:当数据库中 key 存在时,可以将key-value添加数据库,与NX参数互斥

*EX:key 的超时秒数

*PX:key 的超时毫秒数,与 EX 互斥

image-20230718154349073

get <key> :查询对应键值

append <key> <value> :将给定的 <value> 追加到原值的末尾

strlen <key> :获得值的长度

setnx <key> <value> :只有在 key 不存在时,设置 key 的值

incr <key> :将 key 中存储的数字值增1,只能对数字操作,如果为空,新增值为1

decr <key> :将 key 中存储的数字值减1,只能对数字值操作,如果为空,新增值为 -1

incrby / decrby <key> <步长> :将 key 中存储的数字值增减,自定义步长。

原子性:

所谓原子操作是指 不会被线程调度机制打断的操作;

这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

mset <key1> <value1> <key2> <value2> ... :同时设置一个或多个 key-value 对

mget <key1> <key2> <key3> ... :同时获取一个或多个 value

msetnx <key1> <value1> <key2> <value2> ... :同时设置一个或多个 key-value对,当且仅当所有给定 key 都不存在

原子性:有一个失败则都失败

getrange <key> <起始位置> <结束位置> :获得值的范围,类似java中的 substring,前包、后包

setrange <key> <起始位置> <value> :用 <value> 覆写 <key> 所存储的字符串值,从 <起始位置> 开始(从索引0开始

setex <key> <过期时间> <value> :设置键值的同时,设置过期时间,单位秒。

getset <key> <value> :以新换旧,设置了新值同时获得旧值。

数据结构

String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

image-20230718153747348

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

列表(List)

单键多值

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

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

常用命令

lpush / rpush <key> <value1> <value2> <value3> ... :从左边 / 右边插入一个或多个值。

lpop / rpop <key> :从 左边 / 右边吐出一个值。 值在键在,值光键亡。

rpoplpush <key1> <key2> :从 <key1> 列表右边吐出一个值,插到<key2>列表左边。

lrange <key> <start> <stop> :按照索引下表获得元素(从左到右)

例如:lrange k1 0 -1 :0表示左边第一个,-1表示右边第一个。(0 -1 表示获取所有)

lindex <key> <index> :按照索引下标获得元素(从左到右)

llen <key> :获得列表长度

linsert <key> before / after <value> <newvalue> :在 <value> 的前面 / 后面插入 <newvalue> 插入值

lrem <key> <n> <value> 从左边删除 n 个 value (从左到右)

lset <key> <index> <value> :将列表 key 下标为 index 的值替换成 value

数据结构

List的数据结构为快速链表 quickList。

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。

它将所有的元素紧挨着一起存储,分配的是一块连续的内存。

当数据量比较多的时候才会改成quicklist。

因为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是 int类型的数据,结构上还需要两个额外的指针 prev 和 next。

image-20230718191446098

Redis将链表和 ziplist 结合起来组成了quicklist。也就是将多个 ziplist 使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

集合(Set)

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

Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的**复杂度都是O(1)**。

一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变

常用命令

sadd <key> <value1> <value2> ... :将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略

smembers <key> :取出该集合的所有值。

sismember <key> <value> :判断集合 <key> 是否为含有该 <value> 值,有1,没有0

scard <key> :返回该集合的元素个数。

srem <key> <value1> <value2> ... :删除集合中的某个元素

spop <key> :随机从该集合中吐出一个值。(删除)

srandmember <key> <n> :随机从该集合中取出 n 个值,不会从集合中删除。

smove <source> <destination> value :把集合中一个值从一个集合移动到另一个集合(source表示集合1,destination表示集合2,value表示集合1中的值)

sinter <key1> <key2> :返回两个集合的 交集 元素。

sunion <key1> <key2> :返回两个集合的 并集 元素。

sdiff <key1> <key2> :返回两个集合的 差集元素 (key1 中的,不包含 key2 中的)

sintercard numkeys key [key...] [LIMIT limit] :Redis7 新命令,它不返回结果集,而只返回结果的基数。返回有所有给定集合的交际产生的集合的基数。(这个numkeys指的是比较的有几个key,如果是两个key则 sintercard 2 k1 k2 ,返回两个集合交集的个数,后面可追加 limit 数字 表示限制(显示) ”数字“ 个)

数据结构

Set数据结构是dict字典,字典是用哈希表实现的。

Java中 HashSet 的内部实现使用的是 HashMap ,只不过所有的value都指向同一个对象。Redis的 set 结构也是一样,它的内部也使用hash结构,所有的 value 都指向同一个内部值。

哈希(Hash)

Redis hash 是一个键值对集合。

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

类似Java里面的Map<String,Object>

用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储

主要有以下2种存储方式:

image-20230718195712561

常用命令

hset <key> <field> <value> :给 <key> 集合中的 <field> 键赋值 <value>

hget <key1> <field> :从 <key1> 集合 <field>键 取出 其value

hmset <key1> <field> <value1> <field2> <value2> ... :批量设置 hash 的值

hexists <key1> <field> :查看哈希表 key 中,给定域 field 是否存在

hkeys <key> :列出该 hash 集合的所有 field键

hvals <key> :列出该 hash 集合的所有 value值

hincrby <key> <field> <increment> :为 哈希表 key 中的域 field 的值加上增量(加减) 1 -1(increment表示增量的值)

hsetnx <key> <field> <value> :将哈希表 key 中的域 field 的值设置为 value,当且仅当域 field 不存在

数据结构

Hash 类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当 field-value 长度较短且个数较少时,使用ziplist,否则使用 hashtable。

有序集合 Zset(shorted set)

Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。

不同之处是有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。

因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。

访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

常用命令

zadd <key> <score1> <value1> <score2> <value2> ... :将一个或多个 member 元素及其 score 值加入到有序集 key 当中

zrange <key> <start> <stop> [WITHSCORES] :返回有序集 key 中,下标注 <start> <stop> 之间的元素,带 WITHSCORES,可以让分数一起和值返回到结果集。

zrangebyscore key min max [withscores] [limit offset count] :返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max)的成员。有序集成员按 score 值递增(从小到大)次序排列。limit offset count 表示从 offset开始走count步。zrangebyscore key (min max ... :表示不包括min值,从小到大排列。

zrevrangebyscore key max min [withscores] [limit offset count] :同上,改为从大到小排列。

zincrby <key> <increment> <value> :为元素的 score 加上增量

zrem <key> <value> :删除该集合下,指定的元素

zcount <key> <min> <max> :统计该集合,分数区间内的元素个数

zmpop numkeys key [key ...] <MIN | MAX> [COUNT count] :从键名列表中的第一个非空排序集中弹出一个或多个元素,它们是成员分数对。zmpop 1 zset1 MIN :弹出scores最小的那个数。

zscore <key> <value> :获取元素的分数(score)

zcard key :获取集合中元素的数量

zincrby <key> <increment> <value> :增加某个元素的分数

zcount <key> min max :获得指定分数范围内的元素个数

zrank <key> <value> :返回该值在集合中的排名,从 0 开始

zrevrank <key> <value> :逆序返回在集合中的排名

数据结构

SortedSet (zset) 是Redis提供的一个非常特别的数据结构,一方面它等价于 Java的数据结构 Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于 TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层使用了两个数据结构

(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。

(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

新数据类型

位图(Bitmaps)

Redis提供了Bitmaps这个“数据类型”可以实现对位的操作:

  • Bitmaps本身不是一种数据类型, 实际上它就是字符串(key-value) , 但是它可以对字符串的位进行操作。

  • Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

常用命令

  • setbit <key> <offset> <value> :设置 Bitmaps 中某个偏移量的值(0或1),*offset:偏移量从 0 开始

  • getbit <key> <offset> :获取 Bitmaps 中某个偏移量的值,例如获取user中的第7位的值,getbit user 6

  • bitcount <key> [start end] :统计字符串从 start 字节到 end 字节比特值为 1 的数量

    bitcount bit1 0 -1 :统计 bit1 中所有字节比特值为 1 的数量

  • bitop and(or/not/xor) <destkey> [key...] :bitop 是一个复合操作,它可以做多个 Bitmaps 的 and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在 destkey 中。
    案例:

    • 两天访问网站的 userid=1,2,5,9。

      setbit unique:users:20231104 1 1
      setbit unique:users:20231104 2 1
      setbit unique:users:20231104 5 1
      setbit unique:users:20231104 9 1
      
      setbit unique:users:20231105 0 1
      setbit unique:users:20231105 1 1
      setbit unique:users:20231105 4 1
      setbit unique:users:20231105 9 1
    • 计算出两天都访问过网站的用户数量

      bitop and unique:users:and:20231103_04 unique:users:20231103 unique:users:20231104

      image-20230719101105719

    • 计算出任意一天都访问过网站的用户数量(例如月活跃),可以使用 or 求并集

      image-20230719101155859

Bitmaps 与 set 对比

假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表

set 和 Bitmaps 存储一天活跃用户对比:

数据类型 每个用户id占用空间 需要存储的用户量 全部内存量
集合类型 64位 50000000 64位*50000000 = 400MB
Bitmaps 1位 100000000 1位*100000000 = 12.5MB

很明显, 这种情况下使用Bitmaps能节省很多的内存空间, 尤其是随着时间推移节省的内存还是非常可观的

set 和 Bitmaps 存储独立用户空间对比

数据类型 一天 一个月 一年
集合类型 400MB 12GB 144GB
Bitmaps 12.5MB 375MB 4.5GB

但Bitmaps并不是万金油, 假如该网站每天的独立访问用户很少, 例如只有10万(大量的僵尸用户) , 那么两者的对比如下表所示, 很显然, 这时候使用Bitmaps就不太合适了, 因为基本上大部分位都是0。

set 和 Bitmaps 存储一天活跃用户对比(独立用户比较少)

数据类型 每个userid占用空间 需要存储的用户量 全部内存量
集合类型 64位 100000 64位*100000 = 800KB
Bitmaps 1位 100000000 1位*100000000 = 12.5MB

基数统计(HyperLogLog)

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

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

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

什么是基数?

比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 基数(不重复元素)为5。 基数估计就是在误差可接受的范围内,快速计算基数。

常用命令

  • pfadd <key> <element> [element ...] :添加指定元素到 HyperLogLog 中,将所有元素添加到指定HyperLogLog数据结构中。如果执行命令后HLL估计的近似基数发生变化,则返回1,否则返回0。

    实例:

    image-20230719102449105
  • pfcount <key> [key...] :计算 HLL 的近似技术,可以计算多个 HLL,比如用 HLL 存储每天的 UV,计算一周的 UV 可以使用 7 天的 UV 合并计算即可(去重)

    实例:

    image-20230719102259725

  • pfmerge <destkey> <sourcekey> [sourcekey...] :将一个或多个 HLL 合并后的结果存储在另一个 HLL 中,比如每月用户可以使用每天的活跃用户来合并计算可得

    实例:

    image-20230719102551778

地理空间(Geospatial)

Redis 3.2 中增加了对GEO类型的支持。GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置,查询,范围查询,距离查询,经纬度Hash等常见操作。

常用命令

  • geoadd <key> <longitude> <latitude> <member> [longitude latitude member ...] :添加地理位置(经度、纬度、名称)

    实例:

    image-20230719103012365

    两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。当坐标位置超出指定范围时,该命令将会返回一个错误。

    已经添加的数据,是无法再次往里面添加的。

  • geopos <key> <member> [member...] :获得指定地区的坐标值

    实例:

    image-20230719103108542
  • geodist <key> <member1> <member2> [m | km | ft | mi] :获取两个位置之间的直线距离

    实例:

    image-20230719103217152

    单位:

    m 表示单位为米[默认值]。

    km 表示单位为千米。

    mi 表示单位为英里。

    ft 表示单位为英尺。

    如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位

  • georadius <key> <longitude> <latitude> radius m|km|ft|mi :以给定的经纬度为中心,找出某一半径内的元素
    经度 纬度 距离 单位

    实例:

    image-20230719103428037
  • geohash <key> [member [member...]] :返回坐标的geohash表示

    image-20230719105819439

image-20230719103550333

流(Stream)

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

Redis 消息队列 的两种方案

  • List 实现消息队列

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

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

image-20230309213659075

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

Pub/sub

image-20230309213958521

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

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

redis 版的 MQ消息中间件 + 阻塞队列

底层结构和原理

Stream 结构:

image-20230309214654127

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

image-20230309214754029

常用命令

队列相关指令

命令 作用
xadd 添加消息到队列末尾
xtrim 删除消息
xdel 删除消息
xlen 获取 stream 中的消息长度
xrange 获取消息列表(可以指定范围),忽略删除的消息
xrevrange 和 xranger 相比,区别在于 反向获取,ID 从大到小
xread 获取消息(阻塞/非阻塞),返回大于指定ID 的消息

消费组相关指令

命令 作用
xgroup create 创建消费组
xreadgroup group 读取消费组中的消息
xack ack 消息,消息被标记为 “已处理”
xgroup setid 设置消费组最后递送消息的 id
xgroup delconsumer 删除消费组
xpending 打印待处理消息的详细信息
xclaim 转移消息的归属权(长期未被处理/无法处理的消息,转交为其他消费组进行处理)
xinfo 打印 stream / consumer /group 的详细消息
xinfo groups 打印消费者组的详细消息
xinfo stream 打印 stream 的详细消息

四个特殊符号

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

命令使用

  • xadd <key> <id> <field> <value> [field value...] :使用 xadd 向队列添加消息,如果指定的队列不存在,则创建一个队列。

    • key :队列名称,如果不存在就创建
    • id :消息 id,我们使用 * 表示由 redis 生成,可以自定义,但是要自己保证递增性。
    • field vlaue :记录。
    redis> XADD mystream * name Sara surname OConnor
     	"1601372323627-0"
      
    redis> XADD mystream * field1 value1 field2 value2 field3 value3
      	"1601372323627-1"
      
    redis> XLEN mystream
      	(integer) 2
      
    redis> XRANGE mystream - +
        1) 1) "1601372323627-0"
           2) 1) "name"
              2) "Sara"
              3) "surname"
              4) "OConnor"
        2) 1) "1601372323627-1"
           2) 1) "field1"
              2) "value1"
              3) "field2"
              4) "value2"
              5) "field3"
              6) "value3"
  • xtrim <key> maxlen [~] count :使用 xtrim 对流进行修剪,限制长度

    • key :队列名称
    • maxlen :长度
    • count :数量
    127.0.0.1:6379> XADD mystream * field1 A field2 B field3 C field4 D
    	"1601372434568-0"
    
    127.0.0.1:6379> XTRIM mystream MAXLEN 2
    	(integer) 0
    	
    127.0.0.1:6379> XRANGE mystream - +
        1) 1) "1601372434568-0"
           2) 1) "field1"
              2) "A"
              3) "field2"
              4) "B"
              5) "field3"
              6) "C"
              7) "field4"
              8) "D"
  • xdel <key> id [id ...] :使用 xdel 删除消息

    • key :队列名称
    • id :消息 id
    > XADD mystream * a 1
    	1538561698944-0
    	
    > XADD mystream * b 2
    	1538561700640-0
    	
    > XADD mystream * c 3
    	1538561701744-0
    	
    > XDEL mystream 1538561700640-0
    	(integer) 1
    	
    127.0.0.1:6379> XRANGE mystream - +
        1) 1) 1538561698944-0
           2) 1) "a"
              2) "1"
        2) 1) 1538561701744-0
           2) 1) "c"
              2) "3"
  • xlen <key> :使用 xlen 获取流包含的元素数量,即消息长度

    redis> XADD mystream * item 1
    	"1601372563177-0"
    redis> XADD mystream * item 2
    	"1601372563178-0"
    redis> XADD mystream * item 3
    	"1601372563178-1"
    	
    redis> XLEN mystream
    	(integer) 3
  • xrange <key> <start> <end> [COUNT count] :使用 xrange 获取消息列表

    • key :队列名
    • start :开始值,- 表示最小值
    • end :结束值,+ 表示最大值
    • count :数量
    redis> XADD writers * name Virginia surname Woolf
    	"1601372577811-0"
    	
    redis> XADD writers * name Jane surname Austen
    	"1601372577811-1"
    	
    redis> XADD writers * name Toni surname Morrison
    	"1601372577811-2"
    	
    redis> XADD writers * name Agatha surname Christie
    	"1601372577812-0"
    	
    redis> XADD writers * name Ngozi surname Adichie
    	"1601372577812-1"
    	
    redis> XLEN writers
    	(integer) 5
    	
    redis> XRANGE writers - + COUNT 2
        1) 1) "1601372577811-0"
           2) 1) "name"
              2) "Virginia"
              3) "surname"
              4) "Woolf"
        2) 1) "1601372577811-1"
           2) 1) "name"
              2) "Jane"
              3) "surname"
              4) "Austen"
  • xrevrange <key> <end> <start> [COUNT count] :使用 xrevrange 获取消息列表,会自动过滤已经删除的消息

    • key :队列名
    • end :结束值,+ 表示最大值
    • start :开始值,- 表示最小值
    • count :数量
    redis> XADD writers * name Virginia surname Woolf
    	"1601372731458-0"
    	
    redis> XADD writers * name Jane surname Austen
    	"1601372731459-0"
    	
    redis> XADD writers * name Toni surname Morrison
    	"1601372731459-1"
    	
    redis> XADD writers * name Agatha surname Christie
    	"1601372731459-2"
    	
    redis> XADD writers * name Ngozi surname Adichie
    	"1601372731459-3"
    	
    redis> XLEN writers
    	(integer) 5
    
    redis> XREVRANGE writers + - COUNT 1
    1) 1) "1601372731459-3"
       2) 1) "name"
          2) "Ngozi"
          3) "surname"
          4) "Adichie"
  • xread [COUNT count] [BLOCK milliseconds] stream <key> [key ...] <id> [id...] :使用 xread 以阻塞或非阻塞方式获取消息列表。

    • count :数量
    • milliseconds :可选,阻塞毫秒数,没有设置就是非阻塞模式
    • key :队列名
    • id :消息 id
    # 从 Stream 头部读取两条消息
    > XREAD COUNT 2 STREAMS mystream writers 0-0 0-0
        1) 1) "mystream"
           2) 1) 1) 1526984818136-0
                 2) 1) "duration"
                    2) "1532"
                    3) "event-id"
                    4) "5"
                    5) "user-id"
                    6) "7782813"
              2) 1) 1526999352406-0
                 2) 1) "duration"
                    2) "812"
                    3) "event-id"
                    4) "9"
                    5) "user-id"
                    6) "388234"
        2) 1) "writers"
           2) 1) 1) 1526985676425-0
                 2) 1) "name"
                    2) "Virginia"
                    3) "surname"
                    4) "Woolf"
              2) 1) 1526985685298-0
                 2) 1) "name"
                    2) "Jane"
                    3) "surname"
                    4) "Austen"

    非阻塞

    image-20230309222228782

    image-20230309222258355

    阻塞

    redis-cli 连接第二个 客户端

    image-20230309222343107

  • xgroup [create key groupname id-or-$] [setid <key> <groupname> id-or-$] [destroy <key> <groupname>] [delconsumer <key> <groupname> <consumername>] :使用 xgroup create 创建消费者组

    • key :队列名称,如果不存在就创建
    • groupname :组名
    • $ :表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略

    从头开始消费:

    xgroup create mystream consumer-group-name 0-0

    从尾部开始消费:

    xgroup create mystream consumer-group-name $

  • xreadgroup GROUP <group> <consumer> [COUNT count] [BLOCK milliseconds] [NOACK] streams <key> [key...] id [id...] :使用 xreadgroup group 读取消费组中的消息

    • group :消费组名
    • consumer :消费者名
    • count :读取数量
    • milliseconds :阻塞毫秒数
    • key :队列名
    • id :消息 id

    例:xreadgroup GROUP consumer-group-name consumer-name COUNT 1 STREAMS mystream >

位域(bitfield)

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

  1. 位域修改
  2. 溢出控制

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

命令:

image-20230309223404208

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

Redis持久化(RDB + AOF)

RDB

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

备份执行:

  • Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。

  • 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。

  • RDB的缺点是:最后一次持久化后的数据可能丢失。

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

RDB持久化流程

image-20230720082913468

Fork

Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。

在Linux程序中,fork ( ) 会产生一个和父进程完全相同的子进程,但子进程在此后多会 exec 系统调用,出于效率考虑,Linux中引入了写时复制技术

一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

配置

使用 vim /myredis/redis7.conf 进入redis7.conf 界面,

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

    image-20230310152748384

  • redis 6.2 以及 redis7.0.0

    image-20230310171018738

自动触发:

  • 在 redis7.conf 中配置 save

  • 修改 dump 文件的保存位置

    • 默认

      image-20230310171505245
    • 自定义修改为 /myredis/dum6379.rdb ,可以在redis里面使用 config get dir 获取目录

      image-20230310171801493 image-20230310172126770
  • 修改 dump 文件名称

    image-20230310172237300

  • 触发备份

    image-20230720090553799

  • 恢复文件

    • 将备份文件 (dump.rdb) 移动到 redis 安装目录并启动服务即可
    • 备份成功后故意 用 flushdb 清空 redis,看是否可以恢复数据
    • 执行 flushall/flushdb 命令也会产生 dump.rdb 文件。但里面是空的。无意义
  • 物理恢复文件,一定要 服务和备份 分机隔离

    image-20230310173458197

注意:

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

手动触发:

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

  • save

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

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

      image-20230310174335493

      image-20230310174355231

  • bgsave(默认,推荐)

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

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

      image-20230310174957853

      image-20230310175023057

  • lastsave

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

检查和修复 dump.rdb 文件

image-20230310181547571

总结

image-20230310182755358

触发RDB快照的情况

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

禁用快照

  1. 动态所有停止 RDB 保存规则的方法:redis-cli config set save ""

  2. 快照禁用:

    image-20230719170049534

RDB优化参数

  • save <seconds> <changes>

  • dbfilename

  • dir

  • stop-writes-on bgsave-error (使用默认即可)

    image-20230719170530068

    image-20230719170542522

  • rdbcompression (使用默认即可)

    image-20230719170615191

  • rdbchecksum (使用默认即可)

    image-20230719170659221

  • rdb-del-sync-files

    image-20230719170749286

AOF

官网介绍

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

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

AOF保存的是 appendonly.aof 文件

AOF持久化工作流程

image-20230720092859059

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

三种写回策略

  1. Always :同步写回,每个写命令执行完立刻同步地将日志写回磁盘

  2. everysec :每秒写回,每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔1秒把缓冲区中的内容写入磁盘(默认)

  3. no :写入aof文件,不等待磁盘同步

    image-20230720093202694
配置项 写回时机 优点 缺点
Always 同步写回 可靠性高,数据基本不丢失 每个写命令都要落盘,性能影响大
Everysec 每秒写回 性能适中 宕机时丢失1秒内的数据
No 操作系统控制的写回 性能好 宕机时丢失数据较多

文件保存说明

Multi-part AOF

image-20230720094517427

开启 appendonly yes

image-20230720094855849

aof文件-保存路径

redis6

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

image-20230720095055936

RDB文件保存位置:/myredis/dump.rdb

AOF文件保存位置:/myredis/appendonly.aof

reids7

image-20230720095146319

RDB文件保存位置:/myredis/dump.rdb

AOF文件保存位置:myredis/appendonlydir/xxx.aof

总结

现在设置为 dir /myredis ,表示如果是redis6,继续实行 /myredis/xxx

如果是redis7,则实施appendirname "appendonlydir" ,即 dir + appenddirname

image-20230720095631735

aof文件-保存名称

redis6 :有且只有一个

image-20230720095950853

redis7:Multi Part AOF 的设计

image-20230720100338560

  • base 基本文件
    • 表示基础AOF,它一般由子进程通过重写产生,该文件最多只有一个。
  • incr 增量文件
    • 表示增量AOF,它一般会在AOFRW开始执行时被创建,该文件可能存在多个。(写操作会写入这个文件)
  • manifest 清单文件
    • 表示历史AOF,它由BASE和INCR AOF变化而来,每次AOFRW成功完成时,本次AOFRW之前对应 的BASE和INCR AOF都将变为HISTORY,HISTORY类型的AOF会被Redis自动删除。

redis7注意:

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

redis7配置项:

image-20230720100523052

配置文件说明

正常恢复

  • 将 appendonly 设置为 appendonly yes
  • 生成 AOF 文件到指定目录

恢复一:当重启 redis 重新加载的时候用的是 appendonlydir 文件

image-20230720102450401

恢复二:

  1. 写入数据进 redis,然后 flushdb + shutdown 服务器

  2. 新生成了 dump 和 aof

  3. 备份新生成的 .aof 为 .aof.bak ,然后删除 dump/aof 再看恢复

    image-20230310204747742
  4. 重启 redis 重新加载,发现数据库为空

  5. 停止服务器 ,将 .aof.bak 修改为 .aof 之后在重新启动服务器,发现数据恢复了

    image-20230310204820165

    表示,数据存储在 .aof 文件中

异常恢复

  • 故意乱写正常的 AOF 写入文件(appendonly.aof.1.incr.aof),模拟网络闪断文件写 error

  • 重启 redis 之后就会进入 AOF 文件的载入,发现启动都不行

  • 异常修复命令 : redis-check-aof --fix 进行修复

    redis-check-aof --fix appendonly.aof.1.incr.aof

  • 重新 OK

优势

  • 使用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重写机制

由于AOF持久化是Redis不断将写命令记录到AOF文件中,随着Redis不断的进行,AOF的文件会越来越大, 文件越大,占用服务器内存越大以及AOF恢复要求时间会越长。 为了解决这个问题,Redis新增了重写机制,

  1. 当AOF文件的大小超过所设定的峰值时,Redis就会自动启动AOF文件的内容压缩, 只保留可以恢复数据的最小指令集
  2. 或者 可以手动使用命令 bgrewriteaof来重写。

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

触发自动重写机制的条件:

  1. auto- aof- rewrite- percentage 100 :根据上次重写后的 aof 大小,判断当前 aof 大小是不是增长了 1 倍(100%)
  2. auto- aof- rewrite- min- size 64mb :重写时满足文件的大小(64mb)

注意: 两者同时满足才会触发

案例

需求说明:

一开始 :  set k1 v1
然后改为: set k2 v2
最后改为: set k3 v3

开启重写机制后,只保存 set k3 v3 这一条即可,相当于给 aof 文件瘦身减肥,性能更好。

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

前期配置:

  • appendonly 改为 appendonly yes ,默认是 no 关闭,设置为yes就打开 aof 持久化支持

    image-20230720143414916
  • 重写峰值改为 1k

    image-20230720143637643

  • 关闭混合,设置为 no (默认为yes,改为no)

    image-20230720143947856

  • 删除之前的全部 aof 和 rdb,清除干扰项

自动触发重写:

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

    配置项

    image-20230311114659669
  • k1一直写入 数据1111111,最后触发重写机制

image-20230720151544925

手动触发重传

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

image-20230720151739265

结论:

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

AOF文件重写触发机制:通过redis.conf配置文件中的auto-aof-rewrite-percentage:默认值为100,以及auto-aof-rewrite- min-size:64mb配置,也就是说默认Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍 且 文件大于64M时触发。

原理

1:在重写开始前,redis会创建一个“重写子进程”,这个子进程会读取现有的AOF文件,并将其包含的指令进行分析压缩并写入到一个临时文 件中。

2:与此同时,主进程会将新接收到的写指令一边累积到内存缓冲区中,一边继续写入到原有的AOF文件中,这样做是保证原有的AOF文件的可 用性,避免在重写过程中出现意外。

3:当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后就会将内存中缓存的写指令追加到新AOF文件中

4:当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中

5:重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似

AOF 优化配置详解

image-20230311115731747

大部分默认即可

AOF总结

image-20230720153440857

RDB+AOF混合持久化

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

image-20230720153942928

同时开启两种持久化方式

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

RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢? 作者建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份),留着rdb作为一个万一的手段。

  • 开启混合方式设置

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

  • DB+ OF的 混合方式 —–> 结论: RDB 镜像做全量持久化, AOF 做增量持久化

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

image-20230720155208405

纯缓存模式

同时关闭 RDB + AOF ,专心做服务器,不再进行持久化功能

  1. save "" :禁用 RDB

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

  2. appendonly no :禁用 AOF

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

事务

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

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

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

Redis事务

  1. 单独的隔离操作

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

  2. 没有隔离级别的概念

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

  3. 不保证原子性

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

  4. 排他性

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

常用命令

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

执行情况

正常执行

image-20230720162743002

放弃事务

image-20230720162949726

全体连坐

一条命令出错,全部打回去

image-20230720163302777

冤头债主

对的命令执行,错的不执行

image-20230720163710027

watch监控

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

悲观锁: (Redis不用)

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

乐观锁:

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

乐观锁策略:提交版本必须 大于 记录当前版本才能执行更新

CAS: 乐观锁

watch指令在redis事物中提供了CAS的行为。为了检测被watch的keys在是否有多个clients同时改变引起冲突,这些keys将会被监控。如果至少有一个被监控的key在执行exec命令前被修改,整个事物将会回滚,不执行任何动作,从而保证原子性操作,并且执行exec会得到null的回复。

乐观锁工作机制:

watch 命令会监视给定的每一个key,当exec时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。

注意watch的key是对整个连接有效的,事务也一样。如果连接断开,监视和事务都会被自动清除。当然exec,discard,unwatch命令,及客户端连接关闭都会清除连接中的所有监视。还有,如果watch一个不稳定(有生命周期)的key并且此key自然过期,exec仍然会执行事务队列的指令。

watch正常情况:

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

image-20230720164819512

watch有加塞篡改

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

image-20230720165052689

unwatch :取消监控

image-20230720165401736

小结

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

管道

概述

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

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

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

image-20230720170543535

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

解决:

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

image-20230720170743809

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

一句话: 批处理命令变种优化措施,类似 Redis 的原生批命令(mget he mset)

案例

用管道批量处理命令:

先创建一个文件,文件中写入命令,用 cat 文件名 查看

reis-server redis.conf
cat cmd.txt | redis-cli -a abc123 --pipe

image-20230720172014679

总结

pipeline与原生批量命令对比

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

pipeline与事务对比

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

使用pipeline注意事项

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

Redis发布/订阅(pub/sub)

了解即可

概述

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

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

能干嘛

Redis客户端可以订阅任意数量的频道,类似我们微信关注多个公众号:当有新消息通过 PUBLISH 命令发送给频道 channel1 时

总结:

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

image-20230720190803170

常用命令

命令 描述
PSUBSCRIBE pattern [pattern] 订阅一个或多个符合给定模式的频道
PUBSUB subcommand [argument [argument…]] 查看订阅与发布系统状态
PUBLISH channel message 将信息发送到指定的频道
PUNSUBSCRIBE [pattern [pattern…]] 退订所有给定模式的频道
SUBSCRIBE channel [channel…] 订阅给定的一个或多个频道的信息
UNSUBSCRIBE [channel [channel…]] 指退订给定的频道
  • PSUBSCRIBE pattern [pattern] :订阅给定的一个或多个频道的信息

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

    订阅的客户端每次可以收到一个 3 个参数的消息

    • 消息的种类

    • 始发频道的名称

    • 实际的消息内容

      image-20230720191528651
  • publish channel message :发布信息到指定频道

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

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

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

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

案例:

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

    image-20230720192314848

  • 取消订阅

    image-20230720192403996

Pub/Sub缺点

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

Redis复制(replica)

概述

image-20230311155500746
  • 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);

  • 数据的复制是单向的,只能由主节点到从节点。

  • 默认情况下,每台Redis服务器都是主节点,且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

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

  • 有了主从,当 master 挂掉的时候,运维让从库过来接管,服务就可以继续,否则 master 需要经过数据恢复和重启的过程,这就可能会拖很长的时间,影响线上业务的持续服务。

主从复制的作用

  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。
  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。
  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。
  • 高可用基石:主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

权限细节

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

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

image-20230721150413254

常用命令

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

案例

创建三个虚拟机,一个master两个slave

每台都安装redis,拷贝多个 redis.conf 文件,redis6379.confredis6380.confredis6381.conf

image-20230721152345946

小口诀:

  • 三边网络相互ping通 且注意防火墙配置

三大命令:

  • 主从复制
    • replicaof 主库IP 主库端口
    • 配从(库)不配主(库)
  • 改换门庭
    • replicaof 新主库IP 新主库端口
  • 自立为王
    • slaveof no one

操作细节

  1. 开启 daemonize yes

    image-20230721153350536

  2. 注释掉 bind 127.0.0.1

    image-20230721153409328

  3. protected-mode no

    image-20230721153431030

  4. 指定端口

    image-20230721153528662

  5. 指定当前工作目录,dir

    image-20230721153552906

  6. pid 文件名字,pidfile

    image-20230721153618383

  7. log 文件名字,logfile

    image-20230721153657742

  8. requirepass 设置密码

    image-20230721153736947

  9. dump.rdb 名字

    image-20230721153754504
  10. aof文件,appendfilename (非必须)

    image-20230721153839374

  11. 从机访问主机的通行密码 masterauth,必须 (从机需要配置,主机不用

    image-20230721154217008

一主二仆

方案1:配置文件固定写死

  • 配置文件执行:replicaof 主库IP 主库端口

  • 配从(库)不配主(库):

    • 配置从机 6380

      image-20230311202003826

    • 配置从机6381

      image-20230311202020172

  • 先 master 后两台 slave 依次启动 (注意从机的 redis-cli -a 111111 -p 6380) 其中必须要写 -p 6380

image-20230721154608952

  • 查看主从关系(主机日志) vim 6379.log

    image-20230311202708291

  • 备机日志

    image-20230311202743095

  • 使用 info replication 命令查看

    image-20230721155402368

注意:如果从机连接不到主机,可以关闭防火墙,重启服务

  • 首先修改 redis.conf 中的相关配置

    image-20230722153256101
  • 关闭主机的防火墙 systemctl stop firewalld

  • 关闭 redis 服务

    image-20230722153402041

主从问题演示:

  • 从机可以执行写命令

    image-20230311203234915
  • 从机切入点问题

    从机宕机后,主机继续写入,当从机恢复过来了也可以读取到主机写入的,master写,slave跟

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

    当主机 shutdown 后,从机会原地待命,从机数据还可以正常使用,这时候在等待主机恢复。

    image-20230311203934135

    image-20230721160324338

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

    主从关系还在

    image-20230721160516298

方案2:命令操作手动指定

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

    image-20230721160812187

image-20230721161323561

  • 预设的从机上执行命令 slaveof 主库IP 主库端口

    image-20230721161451739

注意:

用命令使用的话,2台从机重启后,主从关系不存在了,说明名命令使用的话只是单次生效的,想要永久是主从关系的话,还是得用配置文件固定写死他们之间的主从关系。

薪火相传

  • 上一个 slave 可以是下一个 slave 的 master,slave 同样可以接收其他 slaves 的连接和同步请求,那么该 slave 作为了链条中的下一个的master,可以有效减轻 主master 的写压力

  • 中途变更转向:会清除之前的数据,重新建立拷贝最新的

  • slaveof 新主库IP 新主库端口

image-20230721162544178

在 6381 redis下,slaveof 192.168.111.172 6380 ,将6381的老大换成 6380

image-20230721164234184 image-20230721164333765

反客为主

slaveof no one :使用当当前数据库停止与其他数据库

image-20230721164546217

复制原理和工作流程

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

复制的缺点

  • 复制延时,信号衰减

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

    image-20230721170600478
  • master 挂了怎么办

    主机shutdown了,从机不懂,原地待命,从机数据可以正常使用,等待主机重启。

    • 默认情况下,不会在 slave 节点中自动重选一个 master
    • 此时,每次都要人工干预

Redis哨兵(sentinel)

概述

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

作用: 俗称,无人值守运维

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

  2. 当 master 宕机,能自动将 slave 切换成新的 master

    image-20230721171524083

能干嘛:

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

前提说明

  • 3个哨兵

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

  • 一主二从

    用于数据读取和存放

image-20230722090827547

步骤

配置

  • /myredis 目录下新建或者拷贝 sentinel.conf 文件,名字不能错

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

    image-20230722091057561
  • 使用 vim sentinel.conf 进入vim界面,重点参数说明

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

    • daemonize :是否以后台 daemon 方式运行

    • protected-mode :安全保护模式

    • port :端口

    • logfile :日志文件路径

    • pidfile :pid 文件路径

    • dir :工作目录

    • sentinel monitor <master-name> <ip> <redis-port> <quorum> :其中quorum表示投票数

      • 设置要监控的 master 服务器
      • quorum 表示最少有几个哨兵认可客观下线,同意故障迁移的法定票数

      image-20230722093855380

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

    • sentinel auth-pass <master-name> <password> :master设置了密码,连接master服务的密码
  • 其他参数说明

    • sentinel down-after-milliseconds <master-name> <milliseconds>

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

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

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

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

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

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

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

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

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

哨兵 sentinel 文件通用配置

这里是在IP为192.168.200.129这台虚拟机中配置的三份sentinel

  • 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 192.168.200.129 6379 2
    sentinel auth-pass mymaster abc123
  • sentinel26380.conf

    bind 0.0.0.0
    daemonize yes
    protected-mode no
    port 26380
    logfile "/myredis/sentinel26380.log"
    pidfile /var/run/redis-sentinel26380.pid
    dir /myredis
    sentinel monitor mymaster 192.168.200.129 6379 2
    sentinel auth-pass mymaster abc123
  • sentinel26381.conf

    bind 0.0.0.0
    daemonize yes
    protected-mode no
    port 26381
    logfile "/myredis/sentinel26381.log"
    pidfile /var/run/redis-sentinel26381.pid
    dir /myredis
    sentinel monitor mymaster 192.168.200.129 6379 2
    sentinel auth-pass mymaster abc123

master主机配置文件说明:

image-20230722145417539

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

  • 架构说明

    image-20230312093624411

    image-20230312093637099

  • redis 6379 / 6380 / 6381.conf 内容,填写主从配置

    • 主机 6379

      主机后面可能会变成从机,也需要设置访问密码,设置masterauth "abc123"

      不然后续会报错 master_link_status:down

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

    redis-cli -a abc123 -p 6379

    redis-cli -a abc123 -p 6380

    redis-cli -a abc123 -p 6381

哨兵部分

  • 在主机6379中启动3个哨兵,完成监控

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

  • 使原有的 master down 机

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

    • 两台从机数据正常
    • 会根据投票数从其他两台从机上选取一个作为master
    • 之前down机的master重启会变为从机
  • 问题

    image-20230723093245394
  • Broken pipe

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

    • 解决

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

    • 总结

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

      image-20230312095222544

  • 投票新选

    • sentinel26379.log

      image-20230312095903020

    • sentinel26380.log

      image-20230312095922085

    • sentinel26381.log

      image-20230312095937279

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

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

      image-20230312100130475

    • 6380还是从机,只不过换了个新老大 6381,6380还是 slave

  • 配置文件

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

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

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

哨兵原理

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

运行流程 故障切换

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

    image-20230723100301354
  • SDown主观下线(Subjectively Down)

    • SDOWN(主观不可用)是在 单个sentinel自己主观上检测到的关于master的状态,从 sentinel 的角度来看,如果发送了Ping后,在一定时间内没有收到合法的回复,就达到了SDOWN的条件。
    • sentinel 配置文件中的 down-after-millisecond 设置了判断主观下线的时间长度

    说明:

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

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

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

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

  • ODown客观下线(Objectively Down)

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

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

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

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

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

    • sentinel26379.log

      image-20230312102841293

    • sentinel26380.log

      image-20230312102929005

    • sentinel26381.log

      image-20230312102957390

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

      • Raft算法

        image-20230723101131392

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

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

新主登基

  • 某个Slave被选中成为新的master

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

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

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

    • 最小 Run ID 的从节点 -> 字典顺序,ASCII 码

群臣俯首

  • 一朝天子一朝臣,换个码头重新拜
  • 执行slaveof no one命令让选出来的从节点成为新的主节点,并通过slaveof命令让其他节点成为其从节点
  • Sentinel leader会对选举出的新master执行slaveof no one操作,将其提升为master节点
  • Sentinel leader向其它slave发送命令,让剩余的slave成为新的master节点的slave

旧主拜服

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

总结 :上述的 failover 操作均有 sentinel 自己独自完成,完全无需人工干预

哨兵使用建议

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

Redis集群(cluster)

概述

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

image-20230312110549308
  • Redis集群是一个提供在多个 Redis 节点间共享数据的程序集
  • Redis集群可以支持多个master

能干嘛:

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

集群算法-分片-槽位slot

官网文档:

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

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

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

HASH_SLOT = CRC16(key) mod 16384

The CRC16 is specified as follows:

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

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

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

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

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

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

槽位slot与分片

Redis集群的数据分片

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

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

image-20230723151526071
  • 分片:

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

  • 如何找到给定key的分片

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

槽位与分片的优势:

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

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

slot槽位映射解决方案

哈希取余分区

image-20230723152749559

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

优点:

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

缺点:

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

一致性哈希算法

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

能干嘛:

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

3大步骤

  • 算法构建一致性哈希环

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

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

image-20230312122518309
  • 服务器IP节点映射

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

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

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

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

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

image-20230312123607085

优点:

  • 哈希算法的 容错性

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

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

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

image-20230312124457726

缺点:

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

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

image-20230312124647270

总结:

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

优点

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

缺点

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

哈希槽分区(重要)

HASH_SLOT = CRC17(key) mod 16384

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

能干什么:

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

image-20230723161333012

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

多少个hash槽:

一个集群只能有16384个槽,编号0-16383(0-2^14-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上。

image-20230723162149231

Redis集群最大槽数是16384个

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

CRC16算法产生的hash值有16bit,该算法可以产生2^16=65536个值。

换句话说值是分布在0~65535之间,有更大的65536不用为什么只用16384就够?

作者在做mod运算的时候,为什么不mod65536,而选择mod16384? HASH_SLOT = CRC16(key) mod 65536为什么没启用

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

原因

The reason is:

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

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

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

image-20230312194505703

(1)如果槽位为635536,发送心跳信息的消息头达8K,发送的心跳包过于庞大

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

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

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

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

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

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

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

注意:

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

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

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

案例

三主三从redis集群配置

  • 新建3台虚拟机,里面新建 mkdir -p /myredis/cluster

  • 新建6个独立的redis实例服务

    • IP:192.168.200.129 ,端口:6381、6382

      • vim /myredis/cluster/redisCluster6381.conf

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

        bind 0.0.0.0
        daemonize yes
        protected-mode no
        port 6382
        logfile "/myredis/cluster/cluster6382.log"
        pidfile /myredis/cluster6382.pid
        dir /myredis/cluster
        dbfilename dump6382.rdb
        appendonly yes
        appendfilename "appendonly6382.aof"
        requirepass 111111
        masterauth 111111
         
        cluster-enabled yes
        cluster-config-file nodes-6382.conf
        cluster-node-timeout 5000
    • IP :192.168.200.132 端口:6383、6384

      • vim /myredis/cluster/redisCluster6383.conf

        bind 0.0.0.0
        daemonize yes
        protected-mode no
        port 6383
        logfile "/myredis/cluster/cluster6383.log"
        pidfile /myredis/cluster6383.pid
        dir /myredis/cluster
        dbfilename dump6383.rdb
        appendonly yes
        appendfilename "appendonly6383.aof"
        requirepass 111111
        masterauth 111111
        
        cluster-enabled yes
        cluster-config-file nodes-6383.conf
        cluster-node-timeout 5000
      • vim /myredis/cluster/redisCluster6384.conf

        bind 0.0.0.0
        daemonize yes
        protected-mode no
        port 6384
        logfile "/myredis/cluster/cluster6384.log"
        pidfile /myredis/cluster6384.pid
        dir /myredis/cluster
        dbfilename dump6384.rdb
        appendonly yes
        appendfilename "appendonly6384.aof"
        requirepass 111111
        masterauth 111111
         
        cluster-enabled yes
        cluster-config-file nodes-6384.conf
        cluster-node-timeout 5000
    • IP :192.168.200.133 端口:6385、6386

      • vim /myredis/cluster/redisCluster6385.conf

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

        bind 0.0.0.0
        daemonize yes
        protected-mode no
        port 6386
        logfile "/myredis/cluster/cluster6386.log"
        pidfile /myredis/cluster6386.pid
        dir /myredis/cluster
        dbfilename dump6386.rdb
        appendonly yes
        appendfilename "appendonly6386.aof"
        requirepass 111111
        masterauth 111111
         
        cluster-enabled yes
        cluster-config-file nodes-6386.conf
        cluster-node-timeout 5000
  • 在各个虚拟机中分别启动他们的redis实例

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

    • 构建主从关系

      redis-cli -a 111111 --cluster create --cluster-replicas 1 192.168.200.129:6381 192.168.200.129:6382 192.168.200.132:6383 192.168.200.132:6384 192.168.200.133:6385 192.168.200.133:6386

    image-20230724100213652image-20230724100247060

注意:

在搭建Redis集群的过程中,执行到 cluter create ... 的时候,发现程序阻塞,显示:Waiting for the cluster to join ... 且一直点个没完,这种情况大部分是因为 集群总线 的端口没有开放

集群总线
每个Redis集群中的节点都需要打开两个TCP连接。一个连接用于正常的给Client提供服务,比如6379,还有一个额外的端口(通过在这个端口号上加10000)作为数据端口,例如:**redis的端口为6379,那么另外一个需要开通的端口是:6379 + 10000, 即需要开启 16379**。16379端口用于集群总线,这是一个用二进制协议的点对点通信信道。这个集群总线(Cluster bus)用于节点的失败侦测、配置更新、故障转移授权,等等。

解决:

  • 开放指定端口:
firewall-cmd --zone=public --add-port=6381/tcp --permanent 
firewall-cmd --zone=public --add-port=16381/tcp --permanent 
等等
  • 重启防火墙

    firewall-cmd --reload
  • 查看是否开启端口

    //查看指定区域所有打开的端口
    firewall-cmd --zone=public --list-ports
    //查看指定端口是否打开
    firewall-cmd --query-port=6379/tcp
  • 查看redis服务进程

    ps -ef|grep redis
  • 关闭redis服务进程

    kill -9 [redis进程号]
  • 重启redis服务即可

    redis-server /myredis/cluster/redisCluster6381.conf

连接: 进入6381,查看并检验集群状态

image-20230724103344012

  • cluster nodes

    image-20230724103503706

  • info replication

    image-20230724103709011

  • cluster info

    image-20230724103742602

注意:

当输入信息的时候,hash计算的不同编号进入其相对应的槽位内,所以当启动 6381的时候,如果写入的数据算出来不是其槽内号的话,不能输入,如果想要输入,则需要在启动redis的时候加入 -c ,即:

redis -cli -a 111111 -p 6381 -c

加入 -c 就可以切换槽位写入数据,不用来回切换redis。

主从容错切换迁移案例

容错切换转移

  • 将主 6381 宕机:shutdown quit

    • 此时6381宕机,一般来说是对应的 6384 从机上位
    • 6381 作为 1 号主机分配的从机以实际情况为准,具体是几号机器就是几号机器
  • 6381宕机重启后 查看集群信息,主从关系

    image-20230724105349780

发现 6384 上位成为了主机,6381成为了6384的从机

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

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

    • 上面换后 6381 和 6384 主从对调了,和原始不一样了,该怎么办
    • 重新登陆 6381 机器,执行 cluster failover 命令

    image-20230724105951766

发现6381和6382成功调换了,6381又成为6384的主机了

集群扩容案例

  • 新建6387、 6388两个服务实例配置文件 + 新建后启动

  • 启动 87、 88两个新节点实例,此时他们自己都是 master

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

    • 将新增的 6387 作为 master节点加入原有集群
    • 命令模版:redis-cli -a 密码 --cluster add-node 自己实际IP地址:6387 自己实际IP地址:6381
    • 6387 就是将要作为 master新增节点
    • 6381 就是原来集群节点里面的领路人,相当于 6387 拜 6381的码头从而找到组织加入集群
    • 命令:redis-cli -a 111111 --cluster add-node 192.168.200.133:6387 192.168.200.129:6381

    image-20230724113508303

  • 检查集群情况第1次

    • redis-cli -a 111111 --cluster check 192.168.200.129:6381
    image-20230724113736167
  • 重新分派槽号(reshard)

    • redis-cli -a 111111 --cluster reshard 192.168.200.129:6381

    image-20230724114902855

  • 检查集群情况第2次

    • redis-cli -a 111111 --cluster check 192.168.200.129.6381

    image-20230724124708555

    • 槽号分派说明:为什么6387是3个新区间(0-1364,5461-6826,10923-12287),6381、6383、 6385还是连续的

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

  • 为主节点 6387 分配从节点 6388

    • 命令模版:redis-cli -a 密码 --cluster add-node ip:新slave端口 ip:新master端口 --cluster-slave --cluster-master-id 新主机节点ID
    • 命令:redis-cli -a 111111 --cluster add-node 192.168.200.133:6388 192.168.200.133:6387 --cluster-slave --cluster-master-id 6e35c954e8eba5fadfeb99b9c5bf1a0f8bfbeca8 (注意,最后的这个ID是主机6387的ID编号,按照自己实际情况写)

    image-20230724130400647

  • 检查集群情况第 3 次

image-20230724130823779

集群缩容案例

目的:6387 和 6388下线

image-20230724131132075
  • 检查集群情况第一次,先获得从节点 6388的节点ID

    • redis-cli -a 111111 --cluster check 192.168.200.133:6388
    image-20230724131648900
  • 从集群中将4号从节点6388 删除

    • 命令模版:redis-cli -a 密码 --cluster del-node ip:从机端口 从机6388节点ID
    • 命令:redis-cli -a 111111 --cluster del-node 192.168.200.133:6388 01c4f0dffe9c77dcae125a0489614066103e294c

    image-20230724132331815

    检查一下,发现只剩下 7 台机器了,6388从节点被删除了

    image-20230724132624821

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

    • redis-cli -a 111111 --cluster reshard 192.168.200.129:6381

    image-20230724132928455

  • 检查集群情况第二次

    • redis-cli -a 111111 --cluster check 192.168.200.129:6381
    • 4096 个槽位都指给6381,它变成了8192个槽位,相当于全部都给了6381了,不然要输入3次,一锅端

    image-20230724133302764

  • 将6387删除

    • 命令:redis-cli -a 111111 --cluster del-node 192.168.200.133:6387 6e35c954e8eba5fadfeb99b9c5bf1a0f8bfbeca8

    image-20230724133707092

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

    • redis-cli -a 111111 --cluster check 192.168.200.129:6381

    image-20230724133824257

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

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

    image-20230312222100122

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

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

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

    CRC16

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

常用命令:

  • 集群是否完整才能对外提供服务:cluster-require-full-coverage

    image-20230724135523178

    默认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:该槽位没占用
    image-20230724135936348
  • cluster keyslot 键名称 :该键应该存在哪个槽位上

    image-20230724140023102

Springboot整合Redis

总体概述: jedis-lettuce-RedisTemplate三者的联系

本地Java连接Redis常见问题:

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

集成Jedis

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

步骤:

  • 建 Module

  • 改 pom文件(加Jedis相关依赖)

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

    server.port=7777
    
    spring.application.name=redis7_study
  • 主启动

  • 业务类

    public class JedisDemo {
        public static void main(String[] args) {
    
            //1. 获取connect连接
            Jedis jedis = new Jedis("192.168.200.129", 6379);
    
            //2.连接密码
            jedis.auth("abc123");
            //测试
            System.out.println(jedis.ping());
    
            //测试set、get
            jedis.set("k1", "v1");
            System.out.println(jedis.get("k1"));
    
            //测试 mset、mget
            jedis.mset("k2", "v2", "k3", "v3");
            System.out.println(jedis.mget("k1", "k2", "k3"));
            jedis.expire("k3", 20L);
    
            //测试list集合
            jedis.lpush("k4","v4","a4","b4","c4");
            System.out.println(jedis.lrange("k4", 0, -1));
    
            //测试set
            jedis.sadd("k1", "v1");
            jedis.sadd("k1", "v2");
            jedis.sadd("k1", "v3");
            Set<String> set1 = jedis.smembers("k1");
            for(Iterator<String> iterator = set1.iterator(); iterator.hasNext();){
                System.out.println(iterator.next());
            }
            jedis.srem("k1","v2");
    
            //测试hash
            jedis.hset("hash1", "userName", "lisi");
            System.out.println(jedis.hget("hash1", "userName"));
            HashMap<String, String> map = new HashMap<>();
            map.put("userName", "wangwu");
            map.put("password","123456");
            map.put("telephone", "11111111");
            map.put("email", "xxxx.com");
            jedis.hmset("hash2",map);
    
            List<String> result = jedis.hmget("hash2", "telephone", "email");
            System.out.println(result);
    
            //测试 zset
            jedis.zadd("k1",200,"v1");
            jedis.zadd("k1",300,"v2");
            jedis.zadd("k1",100,"v3");
            jedis.zadd("k1",600,"v4");
            jedis.zincrby("k1",200,"v2");
            System.out.println(jedis.zrange("k1", 0, -1));
            System.out.println(jedis.zcount("k1",300, 600));
        }
    }

集成Lettuce

Lettuce是一个Redis的Java驱动包

Lettuce和Jedis的区别:

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

  • 配置和Jedis一样

  • pom文件:

    <!--lettuce-->
    <dependency>
        <groupId>io.lettuce</groupId>
        <artifactId>lettuce-core</artifactId>
        <version>6.2.1.RELEASE</version>
    </dependency>
  • 业务类:

public class LettuceDemo {
    public static void main(String[] args) {

        //1. 使用构建器链式编程来builder我们RedisURI
        RedisURI uri = RedisURI.builder()
                .redis("192.168.200.129")
                .withPort(6379)
                .withAuthentication("default", "abc123")
                .build();

        //2.创建连接客户端
        RedisClient redisClient = RedisClient.create(uri);
        StatefulRedisConnection<String, String> conn = redisClient.connect();

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

        //string
        commands.set("k1","v1");
        System.out.println(commands.get("k1"));


        //4.关闭资源
        conn.close();
        redisClient.shutdown();
    }
}

集成RedisTemplate(重要)

连接单机

  • 建 Module

  • 改 pom文件

    <!--引入Redis-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
    </dependency>
    <!--swagger2-->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
  • 写 yml或properties文件

    server.port=7777
    
    spring.application.name=redis7_study
    
    # ========================logging=====================
    logging.level.root=info
    logging.level.com.byxl8112=info
    logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n 
    
    logging.file.name=D:/mylogs2023/redis7_study.log
    logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger- %msg%n
    
    # ========================swagger=====================
    spring.swagger2.enabled=true
    #在springboot2.6.X结合swagger2.9.X会提示documentationPluginsBootstrapper空指针异常,
    #原因是在springboot2.6.X中将SpringMVC默认路径匹配策略从AntPathMatcher更改为PathPatternParser,
    # 导致出错,解决办法是matching-strategy切换回之前ant_path_matcher
    spring.mvc.pathmatch.matching-strategy=ant_path_matcher
    
    # ========================redis单机=====================
    spring.redis.database=0
    # 修改为自己真实IP
    spring.redis.host=192.168.200.129
    spring.redis.port=6379
    spring.redis.password=abc123
    spring.redis.lettuce.pool.max-active=8
    spring.redis.lettuce.pool.max-wait=-1ms
    spring.redis.lettuce.pool.max-idle=8
    spring.redis.lettuce.pool.min-idle=0
    
  • 主启动

  • 业务类

    • 配置类:RedisConfig.java

      package com.byxl8112.config;
      
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
      import org.springframework.data.redis.core.RedisTemplate;
      import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
      import org.springframework.data.redis.serializer.StringRedisSerializer;
      
      /**
       * @auther zzyy
       * @create 2022-11-17 17:34
       */
      @Configuration
      public class RedisConfig
      {
          /**
           * redis序列化的工具配置类,下面这个请一定开启配置
           * 127.0.0.1:6379> keys *
           * 1) "ord:102"  序列化过
           * 2) "\xac\xed\x00\x05t\x00\aord:102"   野生,没有序列化过
           * this.redisTemplate.opsForValue(); //提供了操作string类型的所有方法
           * this.redisTemplate.opsForList(); // 提供了操作list类型的所有方法
           * this.redisTemplate.opsForSet(); //提供了操作set的所有方法
           * this.redisTemplate.opsForHash(); //提供了操作hash表的所有方法
           * this.redisTemplate.opsForZSet(); //提供了操作zset的所有方法
           * @param lettuceConnectionFactory
           * @return
           */
          @Bean
          public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
          {
              RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
      
              redisTemplate.setConnectionFactory(lettuceConnectionFactory);
              //设置key序列化方式string
              redisTemplate.setKeySerializer(new StringRedisSerializer());
              //设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
              redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
      
              redisTemplate.setHashKeySerializer(new StringRedisSerializer());
              redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
      
              redisTemplate.afterPropertiesSet();
      
              return redisTemplate;
          }
      }
    • 配置类:SwaggerConfig.java

      package com.atguigu.redis7.config;
      
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import springfox.documentation.builders.ApiInfoBuilder;
      import springfox.documentation.builders.PathSelectors;
      import springfox.documentation.builders.RequestHandlerSelectors;
      import springfox.documentation.service.ApiInfo;
      import springfox.documentation.spi.DocumentationType;
      import springfox.documentation.spring.web.plugins.Docket;
      import springfox.documentation.swagger2.annotations.EnableSwagger2;
      
      import java.time.LocalDateTime;
      import java.time.format.DateTimeFormatter;
      
      /**
       * @auther zzyy
       * @create 2022-11-17 17:44
       */
      @Configuration
      @EnableSwagger2
      public class SwaggerConfig
      {
          @Value("${spring.swagger2.enabled}")
          private Boolean enabled;
      
          @Bean
          public Docket createRestApi() {
              return new Docket(DocumentationType.SWAGGER_2)
                      .apiInfo(apiInfo())
                      .enable(enabled)
                      .select()
                      .apis(RequestHandlerSelectors.basePackage("com.byxl8112")) //你自己的package
                      .paths(PathSelectors.any())
                      .build();
          }
          public ApiInfo apiInfo() {
              return new ApiInfoBuilder()
                      .title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
                      .description("springboot+redis整合,有问题给管理员阳哥邮件:zzyybs@126.com")
                      .version("1.0")
                      .termsOfServiceUrl("https://www.byxl8112.com/")
                      .build();
          }
      }
    • service :OrderService.java

      package com.atguigu.redis7.config;
      
      import org.springframework.beans.factory.annotation.Value;
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      import springfox.documentation.builders.ApiInfoBuilder;
      import springfox.documentation.builders.PathSelectors;
      import springfox.documentation.builders.RequestHandlerSelectors;
      import springfox.documentation.service.ApiInfo;
      import springfox.documentation.spi.DocumentationType;
      import springfox.documentation.spring.web.plugins.Docket;
      import springfox.documentation.swagger2.annotations.EnableSwagger2;
      
      import java.time.LocalDateTime;
      import java.time.format.DateTimeFormatter;
      
      /**
       * @auther zzyy
       * @create 2022-11-17 17:44
       */
      @Configuration
      @EnableSwagger2
      public class SwaggerConfig
      {
          @Value("${spring.swagger2.enabled}")
          private Boolean enabled;
      
          @Bean
          public Docket createRestApi() {
              return new Docket(DocumentationType.SWAGGER_2)
                      .apiInfo(apiInfo())
                      .enable(enabled)
                      .select()
                      .apis(RequestHandlerSelectors.basePackage("com.byxl8112")) //你自己的package
                      .paths(PathSelectors.any())
                      .build();
          }
          public ApiInfo apiInfo() {
              return new ApiInfoBuilder()
                      .title("springboot利用swagger2构建api接口文档 "+"\t"+ DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
                      .description("springboot+redis整合,有问题给管理员阳哥邮件:zzyybs@126.com")
                      .version("1.0")
                      .termsOfServiceUrl("https://www.byxl8112.com/")
                      .build();
          }
      }
    • controller :OrderController.java

      package com.byxl8112.controller;
      
      import com.byxl8112.service.OrderService;
      import io.swagger.annotations.Api;
      import io.swagger.annotations.ApiOperation;
      import lombok.extern.slf4j.Slf4j;
      import org.springframework.web.bind.annotation.PathVariable;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestMethod;
      import org.springframework.web.bind.annotation.RestController;
      
      import javax.annotation.Resource;
      
      /**
       * @author liang_8112
       * @create 2023-07-24-19:40
       */
      @RestController
      @Slf4j
      @Api(tags = "订单接口")
      public class OrderController {
      
          @Resource
          private OrderService orderService;
      
          @ApiOperation("新增订单")
          @RequestMapping(value="/order/add", method= RequestMethod.POST)
          public void addOrder(){
              orderService.addOrder();
          }
      
          @ApiOperation("按照keyId查询订单")
          @RequestMapping(value="/order/{id}", method = RequestMethod.GET)
          public String  getOrderById(@PathVariable Integer id){
              return orderService.getOrderById(id);
          }
      }
  • 测试

    swagger:http://localhost:7777/swagger-ui.html#/

    序列化问题:

    • 第一种解决方案:键(key)和值(value)都是通过Spring提供的Serializer序列化到数据库的,RedisTemplate默认使用的是 JdkSerializationRedisSerializer,StringRedisTemplate默认使用的是 StringRedisSerializer。

      开启redis数据库的时候加入 --raw

      redis-cli -a abc123 -p 6379 --raw

      image-20230724203449660

      key 被序列化成这样,线上通过key去查询对应的value非常不方便

    • 第二种解决方案:(JDK序列化方式(默认)惹的祸)

      加入RedisConfig.java类指定一个序列化,代码在上面

连接集群

首先在虚拟机中启动6台redis

  • 第一次:改写yaml

    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-cli -a 密码 -p 端口号 -c --raw 打开redis ,

    image-20230724205920495

问题:

  • 人为模拟,master-6381 机器意外宕机,手动 shutdown
  • 先对 redis集群命令方式,手动验证各种读写命令,看看 6384 是否上位
  • redis cluster 集群能够自动感应并自动完成主备切换,对应的 slave 6384 会被选举为新的 master 节点

故障现象:

  • SpringBoot客户端没有动态感知到 RedisCluster 的最新集群信息

  • 经典故障

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

    image-20230724210539073

解决方案:

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

    image-20230313234108359
  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. 刷新节点集群拓扑动态感应(推荐)

image-20230313234154012

第二次改写 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
#支持集群拓扑动态感应刷新,自适应拓扑刷新是否使用所有可用的更新,默认false关闭
spring.redis.lettuce.cluster.refresh.adaptive=true
#定时刷新
spring.redis.lettuce.cluster.refresh.period=2000
spring.redis.cluster.nodes=192.168.111.175:6381,192.168.111.175:6382,192.168.111.172:6383,192.168.111.172:6384,192.168.111.174:6385,192.168.111.174:6386