Redis基础
Redis基础
修改redis.conf文件
启动服务 redis-server 配置文件
连接服务 redis-cli -a 密码 -p 6379
关闭服务 单例模式 redis-cli -a 密码 shutdown
;多例模式 redis-cli -p 6379 shutdown
数据结构
应用场景:
- String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
- List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
- Hash 类型:缓存对象、购物车等。
- Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
-
Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
-
BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
- HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
- GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
- Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
String
String是redis最基本的数据类型,一个key对应一个value。value可以保存字符串和数字,value最多可以容纳 512 MB
String 类型的底层的数据结构实现主要是 SDS(简单动态字符串)
应用场景
-
缓存对象:缓存对象的json;属性分离缓存
-
常规计数:因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。访问次数、点赞、转发、库存量等(
INCR key
) -
分布式锁(
setnx key value
) -
共享Session信息:分布式系统中将Session保存到redis中
List
Redis列表是最简单的字符串列表,按照插入顺序排序。List 类型的底层数据结构是由双向链表或压缩列表,最多可以包含2^32-1
个元素。在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
应用场景:消息队列
消息队列必须满足三个要求:消息保序、处理重复消息、消息可靠
- 消息保序:使用 LPUSH + RPOP;阻塞读取:使用 BRPOP;
- 重复消息处理:生产者自行实现全局唯一 ID;
- 消息的可靠性:使用 BRPOPLPUSH,作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存
List 作为消息队列有什么缺陷?不支持多个消费者消费同一个消息
Hash
Redis Hash是一个string类型的field(字段)和value(值)的映射表,Hash特别适合用户存储对象。Redis中每个Hash可以存储2^32-1个键值对。
Hash 类型的底层数据结构是压缩列表或哈希表,在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
应用场景:
- 缓存对象
String + Json也是存储对象的一种方式,那么存储对象时,到底用 String + json 还是用 Hash 呢?
一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。
- 购物车
Set
Set 类型是一个无序并唯一的键值集合,它的存储顺序不会按照插入的先后顺序进行存储。
一个集合最多可以存储 2^32-1
个元素。概念和数学中个的集合基本类似,可以交集,并集,差集等等,所以 Set 类型除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
Set 类型的底层数据结构是哈希表或整数集合。
应用场景:
集合的主要几个特性,无序、不可重复、支持并交差等操作。因此 Set 类型比较适合用来数据去重和保障数据的唯一性,还可以用来统计多个集合的交集、差集和并集等,当我们存储的数据是无序并且需要去重的情况下,比较适合使用集合类型进行存储。
Set 的差集、并集和交集的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。
在主从集群中,为了避免主库因为 Set 做聚合计算(交集、差集、并集)时导致主库被阻塞,我们可以选择一个从库完成聚合统计,或者把数据返回给客户端,由客户端来完成聚合统计。
-
抽奖:去重功能。key为抽奖活动名,value为员工名称(
spop key 3 或者 SRANDMEMBER key 3
) -
点赞:一个用户点一次赞。key 是文章id,value 是用户id
-
共同好友:交集运算(
sinter key1 key2
)
ZSet
Redis zset 和 set 一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数,redis正是通过分数来为集合中的成员进行从小到大的排序。
应用场景:
-
排行榜
-
电话、姓名排序
使用有序集合的 ZRANGEBYLEX
或 ZREVRANGEBYLEX
可以帮助我们实现电话号码或姓名的排序,我们以 ZRANGEBYLEX
(返回指定成员区间内的成员,按 key 正序排列,分数必须相同)为例。
注意:不要在分数不一致的 SortSet 集合中去使用 ZRANGEBYLEX和 ZREVRANGEBYLEX 指令,因为获取的结果会不准确。
例如,获取132、133开头的电话
- 延迟队列:使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。
使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。
地理空间(GEO)
Redis GEO主要用于存储地理位置信息,并对存储的信息进行操作,包括:添加地理位置的坐标、获取地理位置的坐标、计算两个位置之间的距离、根据用户给定的经纬度坐标来获取指定范围内的地址位置集合。
内部实现:
GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。
GEO 类型使用 GeoHash 编码方法实现了经纬度到 Sorted Set 中元素权重分数的转换,这其中的两个关键机制就是「对二维地图做区间划分」和「对区间进行编码」。一组经纬度落在某个区间后,就用区间的编码值来表示,并把编码值作为 Sorted Set 元素的权重分数。
这样一来,我们就可以把经纬度保存到 Sorted Set 中,利用 Sorted Set 提供的“按权重进行有序范围查找”的特性,实现 LBS 服务中频繁使用的“搜索附近”的需求。
应用场景:导航定位、打车
基数统计(HyperLogLog)
HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定且是很小的(使用12KB就能计算2^64个不同元素的基数)。但要注意,HyperLogLog 是统计规则是基于概率完成的,不是非常准确,标准误算率是 0.81%。
应用场景:去重、计数
- UV:unique visitor 独立访客数
- PV:page view 页面浏览量
- DAU:daily active user 日活
- MAU:月活
位图(bitmap)
Bitmap,即位图,是一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。BitMap通过最小的单位bit来进行0|1
的设置,表示某个元素的值或者状态,时间复杂度为O(1)。
由于 bit 是计算机中最小的单位,使用它进行储存将非常节省空间,特别适合一些数据量大且使用二值统计的场景。
内部实现
Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。
String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。
应用场景
- 签到统计
- 判断用户登录态。将用户 ID 作为 offset,在线就设置为 1,下线设置 0。通过
GETBIT
判断对应的用户是否在线。 5000 万用户只需要 6 MB 的空间。 - 连续签到用户总数。把每天的日期作为 Bitmap 的 key,userId 作为 offset,对应的 bit 位做 『与』运算
位域(bitfield)
通过bitfield命令可以一次性操作多个比特位,它会执行一系列操作并返回一个响应数组,这个数组中的元素对应参数列表中的相应操作的执行结果。主要功能是:
- 位域修改
- 溢出控制
- WRAP:使用回绕(wrap around)方法处理有符号整数和无符号整数溢出情况
- SAT:使用饱和计算(saturation arithmetic)方法处理溢出,下溢计算的结果为最小的整数值,而上溢计算的结果为最大的整数值
- fail:命令将拒绝执行那些会导致上溢或者下溢情况出现的计算,并向用户返回空值表示计算未被执行
Redis流(Stream)
Redis Stream 主要用于消息队列(MQ,Message Queue)
-
Redis 本身是有一个 Redis 发布订阅 (pub/sub) 来实现消息队列的功能,但它有个缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。
-
List 实现消息队列的方式不能重复消费,一个消息消费完就会被删除,而且生产者需要自行实现全局唯一 ID,不支持多播,分组消费
而 Redis Stream 支持消息的持久化、支持自动生成全局唯一ID、支持ack确认消息的模式、支持消费组模式等,让消息队列更加的稳定和可靠
常用命令
帮助命令: help @类型
。例如,help @string
持久化
AOF 文件的内容是操作命令;
RDB 文件的内容是二进制数据。
RDB (Redis Database)
在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复时再将硬盘快照文件直接读回到内存里。
Redis的数据都在内存中,保存备份时它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中。
RDB保存的是dump.rdb文件。
持久化方式:
自动触发:修改 redis.conf 里配置的 save <seconds> <changes>
。自动执行 bgsave 命令,会创建子进程来生成 RDB 快照文件。
手动触发:使用save
或者bgsave
命令。save在主程序中执行会阻塞当前redis服务器,直到持久化工作完成, 执行save命令期间,Redis不能处理其他命令,线上禁止使用。bgsave会在后台异步进行快照操作,不阻塞,该方式会fork一个子进程由子进程完成持久化过程。
如果服务器开启了AOF持久化功能,那么服务器优先使用AOF文件来还原数据库状态,只有AOF处于关闭状态,才会使用RDB。
优势:
- RDB通过快照的形式保存某一时刻的数据状态,文件体积小;
- 备份和恢复的速度非常快;
- RDB是在主线程之外通过fork子进程来进行的,不会阻塞服务器处理命令请求;
- RDB文件通常比AOF文件小得多。
劣势:
- 在一定间隔时间做一次备份,所以如果redis意外down掉的话,就会丢失从当前至最近一次快照期间的数据,快照之间的数据会丢失。
- 内存数据的全量同步,如果数据量太大会导致IO严重影响服务器性能。因为RDB需要经常fork()以便使用子进程在磁盘上持久化。如果数据集很大,fork()可能会很耗时,并且如果数据集很大并且CPU性能不是很好,可能会导致Redis停止为客户端服务几毫秒甚至一秒钟。
如何检查修复dump.rdb文件?
进入到redis安装目录,执行redis-check-rdb命令 redis-check-rdb ./redisconfig/dump.rdb
哪些情况会触发RDB快照?
- 配置文件中默认的快照配置
- 手动save/bgsave命令
- 执行flushdb/fulshall命令也会产生dump.rdb文件,但是也会将命令记录到dump.rdb文件中,恢复后依旧是空,无意义
- 执行shutdown且没有设置开启AOF持久化
- 主从复制时,主节点自动触发
如何禁用快照?
- 动态所有停止RDB保存规则的方法:redis-cli config set value ""
- 手动修改配置文件
RDB 在执行快照的时候,数据能修改吗?
可以的,执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的,关键的技术就在于写时复制技术(Copy-On-Write, COW)。
执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果执行读操作,则主进程和 bgsave 子进程互相不影响。
如果主进程执行写操作,则被修改的数据会复制一份副本,主线程在这个数据副本进行修改操作,然后 bgsave 子进程会把原来的数据写入 RDB 文件,新修改的数据只能交由下一次的 bgsave 快照。
AOF (Append Only File)
以日志的形式来记录每个写操作,将Redis执行过的所有写指令记录下来,只许追加文件但是不可以改写文件,恢复时,以逐一执行命令的方式来进行数据恢复。
默认情况下,redis是没有开启AOF的。
开启:
开启AOF功能需要设置配置:appendonly yes
AOF保存的是 appendonly.aof 文件
aof文件:
-
redis6及之前:appendonly.aof
-
Redis7 Multi Part AOF的设计, 将AOF分为三种类型:
-
BASE: 表示基础AOF,它一般由子进程通过重写产生,该文件最多只有一个。
- INCR:表示增量AOF,它一般会在AOFRW开始执行时被创建,该文件可能存在多个。
- HISTORY:表示历史AOF,它由BASE和INCR AOF变化而来,每次AOFRW成功完成时,本次AOFRW之前对应的BASE和INCR AOF都将变为HISTORY,HISTORY类型的AOF会被Redis自动删除。
为了管理这些AOF文件,我们引入了一个manifest (清单)文件来跟踪、管理这些AOF。
异常修复命令:redis-check-aof --fix incr文件
AOF持久化流程:
先执行写命令,然后记录命令到AOF日志。
-
日志并不是直接写入AOF文件,会将其这些命令先放入AOF缓存中进行保存。这里的AOF缓冲区实际上是内存中的一片区域,存在的目的是当这些命令达到一定量以后再写入磁盘,避免频繁的磁盘IO操作。
-
AOF缓冲会根据AOF缓冲区同步文件的三种写回策略将命令写入磁盘上的AOF文件。
-
ALways:同步写回,每个写命令执行完立刻同步地将日志写回磁盘。
-
everysec(默认):每秒写回,每个写命令执行完,只是先把日志写到AOF文件的内核缓冲区,每隔1秒把缓冲区中的内容写入到磁盘
-
no:操作系统控制的写回,每个写命令执行完,只是先把日志写到AOF文件的内核缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
-
随着写入AOF内容的增加为避免文件膨胀,会根据规则进行命令的合并(AOF重写),从而起到AOF文件压缩的目的。
-
当Redis Server服务器重启的时候会对AOF文件载入数据。
为什么先执行命令,再把数据写入日志呢?
Reids 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,这么做其实有两个好处。
- 避免额外的检查开销:因为如果先将写操作命令记录到 AOF 日志里,再执行该命令的话,如果当前的命令语法有问题,那么如果不进行命令语法检查,该错误的命令记录到 AOF 日志里后,Redis 在使用日志恢复数据时,就可能会出错。
- 不会阻塞当前写操作命令的执行:因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。
当然,这样做也会带来风险:
- 数据可能会丢失: 执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险。
- 可能阻塞其他操作: 由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前命令的执行,但因为 AOF 日志也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。
AOF重写机制:
随着写入AOF内容的增加为避免文件膨胀,会根据规则进行命令的合并(又称AOF重写),从而起到AOF文件压缩的目的。
配置项:auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size 同时满足两个条件时触发重写
- 自动触发: 满足配置文件中的选项后,Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时
- 手动触发:
bgrewriteaof
AOF文件重写并不是对AOF件进行重新整理,而是直接读取服务器数据库中现有的键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
重写原理:
-
触发重写机制后,主进程会创建一个“重写子进程 bgrewriteaof”,这个子进程会携带主进程的数据副本(fork子进程时复制页表,父子进程在写操作之前都共享物理内存空间,从而实现数据共享。主进程第一次写时发生写时复制才会进行物理内存的复制)。重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。
-
与此同时,主进程依然能正常处理命令。但是执行写命令时怎么保证数据一致性呢?在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。
-
当“重写子进程”完成重写工作后,它会给父进程发一个信号,父进程收到信号后将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中
-
当追加结束后,redis就会用新AOF文件来代替旧AOF文件,之后再有新的写指令,就都会追加到新的AOF文件中
Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,这么做可以达到两个好处:
-
子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
-
子进程带有主进程的数据副本,这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。
混合持久化
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快。
混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。
开启:设置 appendonly 为 yes、aof-use-rdb-preamble 为 yes
当开启了混合持久化时,在 AOF 重写日志时,fork
出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
纯缓存模式
同时关闭RDB+AOF,专心做缓存
- save "" -- 禁用RDB
禁用RDB持久化模式下,我们仍然可以使用命令save、bgsave生成RDB文件
- appendonly no -- 禁用AOF
禁用AOF持久化模式下,我们仍然可以使用命令bgrewriteaof生成AOF文件
AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork()
函数创建一个子进程来处理任务。会有两个阶段会导致阻塞父进程(主线程):
- 创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
- 创建完子进程后,如果父进程修改了共享数据中的大 Key,就会发生写时复制,这期间会拷贝物理内存,由于大 Key 占用的物理内存会很大,那么在复制物理内存这一过程,就会比较耗时,所以有可能会阻塞父进程。
事务
redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。Redis 中并没有提供回滚机制 ,并不一定保证原子性
-
开启:以multi开始一个事务
-
入队:将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面
-
执行:exec命令触发事务
性质 | 解释 |
---|---|
不保证原子性 | Redis的事务不保证原子性,也就是不保证所有指令同时成功或同时失败,只有决定是否开始执行全部指令的能力,没有执行到一半进行回滚的能力 |
一致性 | redis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结 |
隔离性 | redis事务是严格遵守隔离性的,原因是redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断 |
不保证持久性 | redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑 |
- 正常执行:MULTI 标记事务开始 、EXEC 执行事务
- 放弃事务:MULTI、DISCARD 取消事务
- 事务全部失败:MULTI后的命令直接报错,EXEC执行也会报错,事务失败,命令全部失效。类似编译错误
- 事务部分失败:MULTI后的命令没有直接报错(例如,INCR email),EXEC时报错,该条命令失败,其余命令成功。类似运行错误
- watch监控:使用watch提供乐观锁定。可以在EXEC执行前监视任意数量的键值对,并在EXEC命令执行时检查被监视的键是否至少有一个被修改过,如果是的话拒绝执行事务。
管道
管道(pipeline)可以一次性发送多条命令给服务端,服务端依次处理完毕后,通过一 条响应一次性将结果返回,通过减少客户端与redis的通信次数来实现降低往返延时时间。pipeline实现的原理是队列,先进先出特性就保证数据的顺序性
cat cmd.txt | redis-cli -a 密码 --pipe
pipeline与原生批量命令对比
- 原生批量命令是原子性(例如:mset、mget),pipeline是非原子性的
- 原生批量命令一次只能执行一种命令,pipeline支持批量执行不同命令
- 原生批量命令是服务端实现,而pipeline需要服务端与客户端共同完成
pipeline与事务对比
- 事务具有原子性,管道不具有原子性
- 管道一次性将多条命令发送到服务器,事务是一条一条的发,事务只有在接收到exec命令后才会执行
- 执行事务时会阻塞其他命令的执行,而执行管道中的命令时不会
使用pipeline注意事项
- pipeline缓冲的指令只是会依次执行,不保证原子性,如果执行中指令发生异常,将会继续执行后续的指令
- 使用pipeline组装的命令个数不能太多,不然数量过大客户端阻塞的时间可能过久,同时服务端此时也被迫回复一个队列答复,占用很多内存
复制(replication)
主从复制,读写分离,master可以读写,slave以读为主,当master数据变化的时候,自动将新的数据异步同步到其他的slave数据库。
配置:(配从不配主)
配置从机:
- master 如果配置了
requirepass
参数,需要密码登录 ,那么slave就要配置masterauth
来设置校验密码,否则的话master会拒绝slave的访问请求; replicaof 主库IP 主库端口
基本操作命令:
info replication
:可以查看节点的主从关系和配置信息
replicaof 主库IP 主库端口
:一般写入进Redis.conf配置文件内,重启后依然生效
slaveof 主库IP 主库端口
:每次与master断开之后,都需要重新连接,除非你配置进了redis.conf文件;在运行期间修改slave节点的信息,如果该数据库已经是某个主数据库的从数据库,那么会停止和原主数据库的同步关系,转而和新的主数据库同步
slaveof no one
:使当前数据库停止与其他数据库的同步,转成主数据库
主从问题演示
- Q:从机可以执行写命令吗?
A:不可以,从机只能读
- Q:从机切入点问题?slave是从头开始复制还是从切入点开始复制?
A: 首次接入全量复制,后续增量复制
- Q:主机shutdown后,从机会上位吗?
A:从机不动,原地待命,从机数据可以正常使用,等待主机重启归来
- Q:主机shutdown后,重启后主从关系还在吗?从机还能否顺利复制?
A:主从关系依然存在,从机依旧是从机,可以顺利复制
- Q:某台从机down后,master继续,从机重启后它能跟上大部队吗?
A:可以,类似于从机切入点问题
复制功能分为同步和命令传播两个操作:同步又分为完整重同步和部分重同步:
完整重同步:完整重同步用于处理初次复制情况,主服务器创建并发送RDB文件,以及向从服务器发送保存在缓冲区中的写命令进行同步。
- 从服务器向主服务器发送 PSYNC 命令
- 收到 PSYNC 命令的主服务器执行 bgsave 命令,后台生成RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令
- bgsave 执行完后,将 RDB文件发送给从服务器,从服务器接收并载入这个RDB文件
- 主服务器将缓冲区中的所有写命令发送给从服务器,从服务器执行这些命令
部分重同步:处理断线后重复制情况。主服务器可以将主从服务器连接断开期间执行的写命令发送给从服务器来使主从一致。部分重同步功能由三个部分组成:
- 主服务器的复制偏移量和从服务器的复制偏移量
- 主服务器的复制积压缓冲器:一个默认大小1MB的队列
- 服务器的运行ID
当从服务器重新连接上主服务器时,从服务器会通过 PSYNC 命令将自己的偏移量和服务器运行ID发送给主服务器:
- 如果偏移量之后的数据仍然存在于复制积压缓冲区里面,那么执行部分重同步操作
- 相反,如果偏移量之后的数据已经不存在于复制积压缓缓区,那么执行完整重同步操作
命令传播:主服务器将自己执行的写命令发送给从服务器,从服务器接收并执行这些命令。
怎么判断 Redis 某个节点是否正常工作?
Redis 判断节点是否正常工作,基本都是通过互相的 ping-pong 心态检测机制,如果有一半以上的节点去 ping 一个节点的时候没有 pong 回应,集群就会认为这个节点挂掉了,会断开与这个节点的连接。
Redis 主从节点发送的心态间隔是不一样的,而且作用也有一点区别:
- Redis 主节点默认每隔 10 秒对从节点发送 ping 命令,判断从节点的存活性和连接状态,可通过参数repl-ping-slave-period控制发送频率。
- Redis 从节点每隔 1 秒发送
replconf ack <offset>
命令,作用: - 检测主从节点网络连接状态;
- 检测命令丢失。
哨兵(Sentinel)
监视一个或多个主服务器及这些主服务器属下的从服务器,当主服务器进入下线状态时,自动将下线主服务器的某个从服务器升级为新的主服务器继续处理命令请求。
哨兵节点主要负责三件事情:监控、选主、通知。
当redis集群的主节点故障时,Sentinel集群将从剩余的从节点中选举一个新的主节点,有以下步骤:
- 故障节点主观下线。Sentinel节点会定时对redis集群的所有节点发心跳包检测节点是否正常,如果一个节点在规定时间内没有回复Sentinel节点的心跳包,则该redis节点被该Sentinel节点主观下线。
- 故障节点客观下线。该Sentinel节点会询问其他Sentinel节点,如果Sentinel集群中超过quorum数量的Sentinel节点认为该redis节点主观下线,则该redis客观下线。
- Sentinel集群选举Leader。
- Sentinel Leader决定新主节点。
整体流程:
1、第一轮投票:判断主节点下线
当哨兵集群中的某个哨兵判定主节点下线(主观下线)后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。
当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。
如果客观下线的redis节点是从节点或者是Sentinel节点,则操作到此为止,没有后续的操作了;如果客观下线的redis节点为主节点,则开始故障转移,从从节点中选举一个节点升级为主节点。
2、第二轮投票:选出哨兵 leader
某个哨兵判定主节点客观下线后,该哨兵就会发起投票,会请求其他Sentinel节点要求将自己选举为Leader。被请求的Sentinel节点如果没有同意过其他Sentinel节点的选举请求,则同意该请求(选举票数+1),否则不同意。
如果一个Sentinel节点获得的选举票数达到Leader最低票数 (quorum和Sentinel节点数/2+1的最大值),则该Sentinel节点选举为Leader;否则重新进行选举。
3、由哨兵 leader 进行主从故障转移
选举出了哨兵 leader 后,就可以进行主从故障转移的过程了。该操作包含以下四个步骤:
- 第一步:挑选出一个从节点,并将其转换为主节点,选择的规则:
- 过滤掉已经离线的从节点;
- 过滤掉历史网络连接状态不好的从节点;
- 将剩下的从节点,进行三轮考察:优先级、偏移量、ID 号。在每一轮考察过程中,如果找到了一个胜出的从节点,就将其作为新主节点。
- 第二步,让其他从节点修改复制目标,修改为复制新主节点;
- 第三步:将新主节点的 IP 地址和信息,通过发布者/订阅者机制通知给客户端主节点更换;
- 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点;
集群(Cluster)
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系,一个切片集群共有 16384 个哈希槽。
槽指派
redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每个键都属于16384槽中的的其中一个,集群中的每个节点可以处理0个或最多16384个槽。
如果键所在的槽正好就指派给当前节点,那么节点直接执行这个命令;否则会指引客户端转向正确的节点再次发送要执行的命令。
slot槽位映射3种解决方案:
-
哈希取余分区: hash(key)%N 在服务器个数固定不变时没有问题,如果需要弹性扩容或故障停机的情况下,取模公式就会发生变化
-
一致性哈希算法分区:
-
构建一致性哈希环: 一致性Hash算法是对2^32取模,简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环
- 将redis服务器 IP节点映射到哈希环某个位置
- key落到服务器的落键规则:当我们需要存储一个kv键值对时,首先将这个key使用相同的函数Hash计算出哈希值并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器,并将该键值对存储在该节点上。
优点:
- 容错性:在一致性Hash算法中,如果一台服务器不可用,则受影响的数据仅仅是此服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响
- 扩展性:数据量增加了,需要增加一台节点NodeX,X的位置在A和B之间,那收到影响的也就是A到X之间的数据,重新把A到X的数据录入到X上即可,不会导致hash取余全部数据重新洗牌。
缺点:一致性Hash算法在服务节点太少时,容易因为节点分布不均匀而造成数据倾斜(集中存储在某台服务器上)
- 哈希槽分区:为解决数据倾斜问题,在数据和节点之间又加入了一层,把这层称为哈希槽(slot)。Redis集群中内置了16384个哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点,数据映射到哈希槽,根据操作分配到对应节点。集群会记录节点和槽的对应关系,哈希槽的计算:HASH_SLOT = CRC16(key) mod 16384。
为什么集群的最大槽数是16384个?
- 如果槽位为65536,发送心跳信息的消息头达8k,发送的心跳包过于庞大。
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为65536时,这块的大小是: 65536÷8÷1024=8kb
在消息头中最占空间的是myslots[CLUSTER_SLOTS/8]。 当槽位为16384时,这块的大小是: 16384÷8÷1024=2kb
因为每秒钟,redis节点需要发送一定数量的ping消息作为心跳包,如果槽位为65536,这个ping消息的消息头太大了,浪费带宽。
- redis的集群节点数量基本不可能超过1000个。
集群节点越多,心跳包的消息体内携带的数据越多。如果节点过1000个,也会导致网络拥堵。因此redis作者不建议redis cluster节点数量超过1000个。 那么,对于节点数在1000以内的redis cluster集群,16384个槽位够用了。没有必要拓展到65536个。
- 槽位越小,节点少的情况下,压缩比高,容易传输
Redis主节点的配置信息中它所负责的哈希槽是通过一张bitmap的形式来保存的,在传输过程中会对bitmap进行压缩,但是如果bitmap的填充率slots / N很高的话(N表示节点数),bitmap的压缩率就很低。 如果节点数很少,而哈希槽数量很多的话,bitmap的压缩率就很低。
集群脑裂导致数据丢失怎么办?
脑裂:
在 Redis 主从架构中,如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),此时这些数据被旧主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。
这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在「从节点」中选举出一个 leader 作为主节点,这时集群就有两个主节点了 —— 脑裂出现了。
然后,网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。
解决方案:
当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
复制与故障转移:
集群中节点分为主节点和从节点,主节点用于处理槽,从节点用于复制某个主节点,并在主节点下线时代替主节点继续处理命令请求。
当从节点发现主节点下线时,从节点对下线主节点进行故障转移:
- 在从节点中选取一个成为主节点
- 新的主节点会撤销所有对已下线主节点的槽指派,并将这些槽指派给自己
- 新的主节点向集群广播一条PONG消息,让其他节点立即直到这个节点已经由从节点变为主节点。
- 新的主节点开始接收和处理自己负责的槽
RedisTemplate
redis客户端:Redisson、Jedis、lettuce等等,官方推荐使用Redisson。