redis
- redis(持久化原理 安全策略 过期删除&内存淘汰策略 性能压测 高可用 Redis Cluster)
- 1. 持久化原理
- 1.1 持久化流程(落盘)
- 1.2 RDB详解
- 1.2.1 概念
- 1.2.2 触发&原理
- 1.2.3 实现
- 1.2.4 RDB总结
- 1.3 AOF详解
- 1.3.1 概念
- 1.3.2 AOF 持久化的实现
- 1.3.2 开启
- 1.3.4 命令追加
- 1.3.5 文件写入和同步(触发)
- 1.3.6 AOF 数据恢复
- 1.3.7 AOF "重写"
- 1.3.8 AOF重写原理
- 1.4 持久化优先级
- 1.5 性能与实践
- 2. 安全策略
- 3. 过期删除策略&内存淘汰策略
- 3.1. 问题分析:
- 3.2 设置Redis键过期时间
- 3.3 Redis过期时间的判定
- 3.4 过期删除策略
- 3.5 Redis过期删除策略
- 3.6 内存淘汰策略
- 4. 性能压测
- 4.1. redis-benchmark
- 4.2. 语法
- 4.3. 快速测试
- 4.4. 精简测试
- 4.5 实战演练
- 5. Redis高可用
- 5.1 主从复制
- 5.1.1 面临问题
- 5.1.2 解决办法
- 5.1.3 主从复制
- 5.1.4 常用策略
- 5.1.5 主从复制原理
- 5.1.6 配置主从复制
- 5.1.7 测试主从关系
- 5.2 sentinel哨兵模式
- 6. Redis Cluster
- 6.1主从 + 哨兵 问题分析
- 6.2 Cluster概念
- 6.3 故障转移
- 6.4 集群分片策略
- 6.5 Redis 集群的数据分片
- 6.6 搭建Redis Cluster
redis(持久化原理 安全策略 过期删除&内存淘汰策略 性能压测 高可用 Redis Cluster)
1. 持久化原理
持久化:Redis是内存数据库,数据都是存储在内存中,为了避免进程退出导致数据的永久丢失,需要定期将Redis中的数据以某种形式(数据或命令)从内存保存到硬盘;当下次Redis重启时,利用持久化文件实现数据恢复。除此之外,为了进行灾难备份,可以将持久化文件拷贝到一个远程位置
1.1 持久化流程(落盘)
既然redis的数据可以保存在磁盘上,那么这个流程是什么样的呢?
要有下面五个过程:
(1)客户端向服务端发送写操作(数据在客户端的内存中)。
(2)数据库服务端接收到写请求的数据(数据在服务端的内存中)。
(3)服务端调用write这个系统调用,将数据往磁盘上写(数据在系统内存的缓冲区中)。
(4)操作系统将缓冲区中的数据转移到磁盘控制器上(数据在磁盘缓存中)。
(5)磁盘控制器将数据写到磁盘的物理介质中(数据真正落到磁盘上)。
这5个过程是在理想条件下一个正常的保存流程,但是在大多数情况下,我们的机器等等都会有各种各样的故障,这里划分了两种情况
(1)Redis数据库发生故障,只要在上面的第三步执行完毕,那么就可以持久化保存,剩下的两步由操作系统替我们完成。
(2)操作系统发生故障,必须上面5步都完成才可以。
为应对以上5步操作,redis提供了两种不同的持久化方式:RDB(Redis DataBase)和AOF(Append OnlyFile)
1.2 RDB详解
1.2.1 概念
- RDB:在指定的时间间隔能对你的数据进行快照存储。
RDB持久化是将当前进程中的数据生成快照保存到硬盘(因此也称作快照持久化),保存的文件后缀是
rdb;当Redis重新启动时,可以读取快照文件恢复数据。
在我们安装了redis之后,所有的配置都是在redis.conf文件中,里面保存了RDB和AOF两种持久化机制的各 种配置。
1.2.2 触发&原理
在Redis中RDB持久化的触发分为两种:指令手动触发和 redis.conf 配置自动触发
指令手动触发
save命令和bgsave命令都可以生成RDB文件
- save:会阻塞当前Redis服务器,直到RDB文件创建完毕为止,线上应该禁止使用。
- bgsave:该触发方式会fork一个子进程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程的时候。
自动触发 - 根据我们的 save m n 配置规则自动触发;
- 从节点全量复制时,主节点发送rdb文件给从节点完成复制操作,主节点会触发 bgsave;
- 执行 debug reload 时;
- 执行 shutdown时,如果没有开启aof,也会触发。
redis.conf:
# 时间策略
save 900 1 # 表示900 秒内如果至少有 1 个 key 的值变化,则触发RDB
save 300 10 # 表示300 秒内如果至少有 10 个 key 的值变化,则触发RDB
save 60 10000 # 表示60 秒内如果至少有 10000 个 key 的值变化,则触发RDB
# 文件名称
dbfilename dump.rdb
# 文件保存路径
dir /home/work/app/redis/data/
# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes
# 是否压缩
rdbcompression yes
# 导入时是否检查
rdbchecksum yes
配置其实非常简单,这里说一下持久化的时间策略具体是什么意思。
- save 900 1 表示900s内如果有1条是写入命令,就触发产生一次快照,可以理解为就进行一次备份
- save 300 10 表示300s内有10条写入,就产生快照
下面的类似,那么为什么需要配置这么多条规则呢?因为Redis每个时段的读写请求肯定不是均衡的,为了平衡性能与数据安全,我们可以自由定制什么情况下触发备份。所以这里就是根据自身Redis写入情况来进行合理配置。
- stop-writes-on-bgsave-error yes
这个配置也是非常重要的一项配置,这是当备份进程出错时,主进程就停止接受新的写入操作,是为了保护持久化的数据一致性问题。如果自己的业务有完善的监控系统,可以禁止此项配置,否则请开启。 - 关于压缩的配置 rdbcompression yes,建议没有必要开启,毕竟Redis本身就属于CPU密集型服务器,再开启压缩会带来更多的CPU消耗,相比硬盘成本,CPU更值钱。
当然如果你想要禁用RDB配置,也是非常容易的,只需要在save的最后一行写上: save “”
1.2.3 实现
手动触发bgsave方法
自动触发
配置了save 20 3
1.2.4 RDB总结
ps -ef | grep redis
优势
1、执行效率高,适用于大规模数据的备份恢复。自动备份不会影响主线程工作。
2、备份的文件占用空间小。其备份的是数据快照,相对于AOF来说文件大小要小一些。
劣势
1、可能会造成部分数据丢失。因为是自动备份,所以如果修改的数据量不足以触发自动备份,同时发生断电等异常导致redis不能正常关闭,所以也没有触发关闭的备份,那么在上一次备份到异常宕机过程中发生的写操作就会丢失。
2、自动备份通过fork进程来执行备份操作,而fork进程会将当前进程的内存数据完整的复制一份,所以这个过程占用的空间是原来的2倍,可能会导致内存不足
1.3 AOF详解
1.3.1 概念
AOF(append only file):记录每次对服务器写的操作(命令),当服务器重启的时候会重新执行这些命令来恢复原始的数据。(默认不开启)
AOF特点:
- 以日志的形式来记录用户请求的写操作,读操作不会记录,因为写操作才会存储
- 文件以追加的形式而不是修改的形式
- redis的aof恢复其实就是把追加的文件从开始到结尾读取 执行 写操作
1.3.2 AOF 持久化的实现
如上图所示,AOF 持久化功能的实现可以分为命令追加( append )、文件写入( write )、文件同步( sync)、文件重写(rewrite)和重启加载(load)。其流程如下:
- 所有的写命令会追加到 AOF 缓冲中。
- AOF 缓冲区根据对应的策略向硬盘进行同步操作。
- 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的。
- 当 Redis 重启时,可以加载 AOF 文件进行数据恢复。
1.3.2 开启
# 可以通过修改redis.conf配置文件中的appendonly参数开启
appendonly yes
# AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的。
dir ./
# 默认的文件名是appendonly.aof,可以通过appendfilename参数修改
appendfilename appendonly.aof
1.3.4 命令追加
当 AOF 持久化功能处于打开状态时,Redis 在执行完一个写命令之后,会以协议格式(也就是RESP,即Redis 客户端和服务器交互的通信协议 )将被执行的写命令追加到 Redis 服务端维护的 AOF 缓冲区末尾。
比如说 SET mykey myvalue 这条命令就以如下格式记录到 AOF 缓冲中。
- “*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n”
Redis 协议格式本文不再赘述,AOF之所以直接采用文本协议格式,是因为所有写入命令都要进行追加操作,直接采用协议格式,避免了二次处理开销。
1.3.5 文件写入和同步(触发)
Redis 每次结束一个事件循环之前,它都会调用 flushAppendOnlyFile 函数,判断是否需要将 AOF 缓存区中的内容写入和同步到 AOF 文件中。
flushAppendOnlyFile 函数的行为由 redis.conf 配置中的 appendfsync 选项的值来决定。该选项有三个可选值,分别是 always 、 everysec 和 no :
- always :每执行一个命令保存一次 高消耗,最安全
- everysec :每一秒钟保存一次
- no :只写入 不保存, AOF 或 Redis 关闭时执行,由操作系统触发刷新文件到磁盘
写入 和保存概念
WRITE:根据条件,将 aof_buf 中的缓存写入到 AOF 文件。
SAVE:根据条件,调用 fsync 或 fdatasync 函数,将 AOF 文件保存到磁盘中。
1.3.6 AOF 数据恢复
AOF 文件里边包含了重建 Redis 数据所需的所有写命令,所以 Redis 只要读入并重新执行一遍 AOF 文件里边保存的写命令,就可以还原 Redis 关闭之前的状态
Redis 读取 AOF 文件并且还原数据库状态的详细步骤如下:
- 创建一个不带网络连接的的伪客户端( fake client),因为 Redis 的命令只能在客户端上下文中执行,而载入 AOF文件时所使用的的命令直接来源于 AOF 文件而不是网络连接,所以服务器使用了一个没有网络连接的伪客户端来执行 AOF文件保存的写命令,伪客户端执行命令的效果和带网络连接的客户端执行命令的效果完全一样的。
- 从 AOF 文件中分析并取出一条写命令。
- 使用伪客户端执行被读出的写命令。
- 一直执行步骤 2 和步骤3,直到 AOF 文件中的所有写命令都被处理完毕为止。
当完成以上步骤之后,AOF 文件所保存的数据库状态就会被完整还原出来。
1.3.7 AOF “重写”
问题分析:AOF采用文件追加方式,随着Redis长时间运行,会产生什么问题?
概念:
为了解决 AOF 文件体积膨胀的问题,Redis 提供了 AOF 文件重写( rewrite) 策略
如上图所示,重写前要记录名为 list 的键的状态,AOF 文件要保存五条命令,而重写后,则只需要保存一条命令。
AOF 文件重写并不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,而是通过读取服务器当前的数据库状态来实现的。首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是 AOF 重写功能的实现原理。
触发:
rewrite的触发机制主要有:
- 手动调用 bgrewriteaof 命令,如果当前有正在运行的 rewrite 子进程,则本次rewrite会推迟执行,否则,直接触发一次 rewrite
- 自动触发 就是根据配置规则来触发
# 重写机制:避免文件越来越大,自动优化压缩指令,会fork一个新的进程去完成重写动作,新进程里的内存
数据会被重写,此时旧的aof文件不会被读取使用,类似rdb
# 当前AOF文件的大小是上次AOF大小的100% 并且文件体积达到64m,满足两者则触发重写
auto-aof-rewrite-percentage 100
#第二个是自动触发只会使用一次,当达到容量时进行rewrite,后续就用第一个条件
auto-aof-rewrite-min-size 64mb
1.3.8 AOF重写原理
AOF 重写函数会进行大量的写入操作,调用该函数的线程将被长时间阻塞,所以 Redis 在子进程中执行AOF 重写操作。
在整个 AOF 后台重写过程中,只有信号处理函数执行时会对 Redis 主进程造成阻塞,在其他时候,AOF后台重写都不会阻塞主进程。
1.4 持久化优先级
如果一台服务器上有既有RDB文件,又有AOF文件,该加载谁呢?
1.5 性能与实践
通过上面的分析,我们都知道RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。
- 降低fork的频率,比如可以手动来触发RDB生成快照、与AOF重写;
- 控制Redis最大使用内存,防止fork耗时过长;
- 使用更牛逼的硬件;
- 合理配置Linux的内存分配策略,避免因为物理内存不足导致fork失败
线上实践经验 - 如果Redis中的数据并不是特别敏感或者可以通过其它方式重写生成数据,可以关闭持久化,如果丢失数据可以通过其它途径补回;
- 自己制定策略定期检查Redis的情况,然后可以手动触发备份、重写数据;
- 可以加入主从机器,利用一台从机器进行备份处理,其它机器正常响应客户端的命令;
- RDB持久化与AOF持久化可以同时存在,配合使用。
2. 安全策略
1. 密码认证
可以通过 redis 的配置文件设置密码参数,这样客户端连接到 redis 服务就需要密码验证,这样可以让你的 redis 服务更安全。
redis在redis.conf配置文件中,设置配置项requirepass, 开户密码认证。
打开redis.conf,找到requirepass所在的地方,修改为指定的密码,密码应符合复杂性要求:
1、长度8位以上
2、包含以下四类字符中的三类字符:
英文大写字母(A 到 Z)
英文小写字母(a 到 z)
10 个基本数字(0 到 9)
非字母字符(例如 !、$、#、%、@、^、&)
3、避免使用已公开的弱密码,如:abcd.1234 、admin@123等
再去掉前面的#号注释符,然后重启redis
实例
我们可以通过以下命令查看是否设置了密码验证:
127.0.0.1:6379> CONFIG get requirepass
1) "requirepass"
2) ""
默认情况下 requirepass 参数是空的,这就意味着你无需通过密码验证就可以连接到 redis 服务。
你可以通过以下命令来修改该参数:
127.0.0.1:6379> CONFIG set requirepass "runoob"
OK
127.0.0.1:6379> CONFIG get requirepass
1) "requirepass"
2) "runoob"
设置密码后,客户端连接 redis 服务就需要密码验证,否则无法执行命令。
语法
AUTH 命令基本语法格式如下:
127.0.0.1:6379> AUTH password
实例
127.0.0.1:6379> AUTH "runoob"
OK
127.0.0.1:6379> SET mykey "Test value"
OK
127.0.0.1:6379> GET mykey
"Test value"
3. 过期删除策略&内存淘汰策略
3.1. 问题分析:
①、如何设置Redis键的过期时间?
②、设置完一个键的过期时间后,到了这个时间,这个键还能获取到么?假如获取不到那这个键还占据着内存吗?
③、如何设置Redis的内存大小?当内存满了之后,Redis有哪些内存淘汰策略?我们又该如何选择?
3.2 设置Redis键过期时间
Redis提供了四个命令来设置过期时间(生存时间)。
①、EXPIRE :表示将键 key 的生存时间设置为 ttl 秒。
②、PEXPIRE :表示将键 key 的生存时间设置为 ttl 毫秒。
③、EXPIREAT :表示将键 key 的生存时间设置为 timestamp 所指定的秒数时间戳。
④、PEXPIREAT :表示将键 key 的生存时间设置为 timestamp 所指定的毫秒数时间戳。
PS:在Redis内部实现中,前面三个设置过期时间的命令最后都会转换成最后一个PEXPIREAT 命令来完成。
另外补充两个知识点:
一、移除键的过期时间
PERSIST :表示将key的过期时间移除。
二、返回键的剩余生存时间
TTL :以秒的单位返回键 key 的剩余生存时间。
PTTL :以毫秒的单位返回键 key 的剩余生存时间。
3.3 Redis过期时间的判定
在Redis内部,每当我们设置一个键的过期时间时,Redis就会将该键带上过期时间存放到一个过期字典中。当我们查询一个键时,Redis便首先检查该键是否存在过期字典中,如果存在,那就获取其过期时间。然后将过期时间和当前系统时间进行比对,比系统时间大,那就没有过期;反之判定该键过期。
3.4 过期删除策略
通常删除某个key,我们有如下三种方式进行处理。
①、定时删除
在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。
②、惰性删除
设置该key 过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
③、定期删除
每隔一段时间,我们就对一些key进行检查,删除里面过期的key
3.5 Redis过期删除策略
Redis的过期删除策略就是:惰性删除和定期删除两种策略配合使用
惰性删除:Redis的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有键读写命令执行之前都会调用expireIfNeeded 函数对其进行检查,如果过期,则删除该键,然后执行键不存在的操作;未过期则不作操作,继续执行原有的命令。
定期删除:由redis.c/activeExpireCycle 函数实现,函数以一定的频率运行,每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
注意:并不是一次运行就检查所有的库,所有的键,而是随机检查一定数量的键。
定期删除函数的运行频率,在Redis2.6版本中,规定每秒运行10次,大概100ms运行一次。
在Redis2.8版本后,可以通过修改配置文件redis.conf 的 hz 选项来调整这个次数。
思考:会不会存在某些永远使用不到的键,并且多次定期删除也没选定到进行删除的key?
3.6 内存淘汰策略
①、设置Redis最大内存
在配置文件redis.conf 中,可以通过参数 maxmemory 来设定最大内存:
不设定该参数默认是无限制的,但是通常会设定其为物理内存的四分之三
②、设置内存淘汰方式
当现有内存大于 maxmemory 时,便会触发redis主动淘汰内存方式,通过设置 maxmemory-policy
有如下几种淘汰方式:
- volatile-lru :设置了过期时间的key使用LRU算法淘汰;
- allkeys-lru :所有key使用LRU算法淘汰;
- volatile-lfu :设置了过期时间的key使用LFU算法淘汰;
- allkeys-lfu :所有key使用LFU算法淘汰;
- volatile-random :设置了过期时间的key使用随机淘汰;
- allkeys-random :所有key使用随机淘汰;
- volatile-ttl :设置了过期时间的key根据过期时间淘汰,越早过期越早淘汰;
- noeviction:默认策略,当内存达到设置的最大值时,所有申请内存的操作都会报错(如set,lpush等),只读操作如get命令可以正常执行;
* LRU、LFU和volatile-ttl都是近似随机算法;
使用下面的参数maxmemory-policy配置淘汰策略:
#配置文件
maxmemory-policy noeviction
#命令行
127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"
127.0.0.1:6379> config set maxmemory-policy allkeys-random
OK
127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "allkeys-random"
在缓存的内存淘汰策略中有FIFO、LRU、LFU三种,其中LRU和LFU是Redis在使用的。
FIFO是最简单的淘汰策略,遵循着先进先出的原则,这里简单提一下:
LRU算法
LRU(Least Recently Used)表示最近最少使用,该算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
LRU算法的常见实现方式为链表:
新数据放在链表头部 ,链表中的数据被访问就移动到链头,链表满的时候从链表尾部移出数据。
而在Redis中使用的是近似LRU算法,为什么说是近似呢?Redis中是随机采样5个(可以修改参数maxmemory-samples配置)key,然后从中选择访问时间最早的key进行淘汰,因此当采样key的数量
与Redis库中key的数量越接近,淘汰的规则就越接近LRU算法。但官方推荐5个就足够了,最多不超过10个,越大就越消耗CPU的资源。
但在LRU算法下,如果一个热点数据最近很少访问,而非热点数据近期访问了,就会误把热点数据淘汰而留下了非热点数据,因此在Redis4.x中新增了LFU算法。LFU算法
LFU(Least Frequently Used)表示最不经常使用,它是根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
LFU算法反映了一个key的热度情况,不会因LRU算法的偶尔一次被访问被误认为是热点数据。
LFU算法的常见实现方式为链表:
新数据放在链表尾部 ,链表中的数据按照被访问次数降序排列,访问次数相同的按最近访问时间降序排列,链表满的时候从链表尾部移出数据。
总结
Redis过期删除策略是采用惰性删除和定期删除这两种方式组合进行的,惰性删除能够保证过期的数据我们在获取时一定获取不到,而定期删除设置合适的频率,则可以保证无效的数据及时得到释放,而不会一直占用内存数据。
但是我们说Redis是部署在物理机上的,内存不可能无限扩充的,当内存达到我们设定的界限后,便自动触发Redis内存淘汰策略,而具体的策略方式要根据实际业务情况进行选取。
4. 性能压测
Redis 的性能测试工具,目前主流使用的是 redis-benchmark
4.1. redis-benchmark
Redis 官方提供 redis-benchmark 的工具来模拟 N 个客户端同时发出 M 个请求,可以便捷对服务器进行读写性能压测
4.2. 语法
redis 性能测试的基本命令如下:
4.3. 快速测试
redis-benchmark
在安装 Redis 的服务器上,直接执行,不带任何参数,即可进行测试。测试结果如下:
====== PING_INLINE ======
100000 requests completed in 1.18 seconds
50 parallel clients
3 bytes payload
keep alive: 1
100.00% <= 0 milliseconds
84388.19 requests per second
====== PING_BULK ======
100000 requests completed in 1.17 seconds
50 parallel clients
3 bytes payload
keep alive: 1
100.00% <= 0 milliseconds
85106.38 requests per second
====== SET ======
100000 requests completed in 1.18 seconds
50 parallel clients
3 bytes payload
keep alive: 1
99.95% <= 1 milliseconds
.......
基本可以看到,常用的 GET/SET/INCR 等命令,都在 8W+ QPS 以上
4.4. 精简测试
redis-benchmark -t set,get,incr -n 1000000 -q
- 通过 -t 参数,设置仅仅测试 SET/GET/INCR 命令
- 通过 -n 参数,设置每个测试执行 1000000 次操作。
- 通过 -q 参数,设置精简输出结果。
执行结果如下:
➜ redis-6.2.5 redis-benchmark -t set,get,incr -n 1000000 -q
SET: rps=0.0 (overall: nan) avg_msec=naSET: rps=190490.0 (overall: 190490.0) aSET: rps=222580.0 (overall: 206503.0) aSET: rps=220816.0 (overall: 211267.6) aSET: rps=222572.0 (overall: 214090.9) aSET: rps=220256.0 (overall: 215322.9) aSET: rps=220378.5 (overall: 216167.8) aSET: rps=219636.0 (overall: 216662.7) aSET: rps=221700.0 (overall: 217291.7) aSET: rps=203976.0 (overall: 215813.5) aSET: rps=221524.0 (overall: 216384.1) aSET: rps=215408.0 (overall: 216295.4) aSET: rps=194564.0 (overall: 214485.7) aSET: rps=219163.4 (overall: 214846.6) aSET: rps=216488.0 (overall: 214963.8) aSET: rps=219796.0 (overall: 215285.6) aSET: rps=206972.0 (overall: 214766.4) aSET: rps=220368.0 (overall: 215095.7) aSET: rps=214236.0 (overall: 215048.0) a SET: 214961.31 requests per second, p50=0.135 msec
GET: rps=78640.0 (overall: 196600.0) avg_msec=0.151 (overall: 0.151GET: rps=193278.9 (overall: 194225.1) aGET: rps=215244.0 (overall: 202968.4) aGET: rps=211336.0 (overall: 205426.5) aGET: rps=216424.0 (overall: 207923.7) aGET: rps=219952.0 (overall: 210149.5) aGET: rps=216748.0 (overall: 211179.9) aGET: rps=196220.0 (overall: 209159.4) aGET: rps=208745.0 (overall: 209109.9) aGET: rps=222148.0 (overall: 210495.8) aGET: rps=204872.0 (overall: 209955.4) aGET: rps=204840.0 (overall: 209507.0) aGET: rps=212588.0 (overall: 209755.3) aGET: rps=168172.0 (overall: 206653.9) aGET: rps=194868.0 (overall: 205835.9) aGET: rps=182298.8 (overall: 204302.6) aGET: rps=217360.0 (overall: 205098.2) aGET: rps=219036.0 (overall: 205898.7) aGET: rps=214468.0 (overall: 206364.1) a GET: 206868.02 requests per second, p50=0.135 msec
INCR: rps=16364.0 (overall: 227277.8) avg_msec=0.126 (overall: 0.12INCR: rps=222900.0 (overall: 223194.0) INCR: rps=223864.0 (overall: 223517.4) INCR: rps=222629.5 (overall: 223227.6) INCR: rps=223684.0 (overall: 223339.5) INCR: rps=224528.0 (overall: 223573.7) INCR: rps=224328.0 (overall: 223697.8) INCR: rps=217444.0 (overall: 222814.0) INCR: rps=204196.0 (overall: 220508.7) INCR: rps=202464.0 (overall: 218520.5) INCR: rps=194668.0 (overall: 216153.2) INCR: rps=196255.0 (overall: 214350.2) INCR: rps=175292.0 (overall: 211116.9) INCR: rps=214512.0 (overall: 211376.5) INCR: rps=206040.0 (overall: 210997.4) INCR: rps=223676.0 (overall: 211838.2) INCR: rps=223348.0 (overall: 212554.0) INCR: rps=221160.0 (overall: 213057.8) INCR: rps=195844.0 (overall: 212105.8) INCR: 211282.50 requests per second, p50=0.135 msec
➜ redis-6.2.5 redis-benchmark -q script load "redis.call('set','foo','bar')"
script load redis.call('set','foo','bar'): rps=0.0 (overall: 0.0) avg_msec=nan (overall: nascript load redis.call('set','foo','bar'): rps=188264.0 (overall: 187514.0) avg_msec=0.170 script load redis.call('set','foo','bar'): 202839.75 requests per second, p50=0.143 msec
4.5 实战演练
看一个实际的案例,压测开启、关闭 aof下,redis的性能剖析
1)参考1.1.3小节,关掉auth认证,打开aof,策略为always,配置文件如下
#redis.conf
appendonly yes
appendfsync always
#requirepass abc #关掉auth
#kill旧进程,重启redis
[root@iZ8vb3a9qxofwannyywl6zZ aof]# pwd
/opt/redis/latest/aof
[root@iZ8vb3a9qxofwannyywl6zZ aof]# ../src/redis-server redis.conf
2)压测aof下的性能,以get,set为测试案例,将结果记录下来,留做后面对比
[root@iZ8vb3a9qxofwannyywl6zZ aof]# ../src/redis-benchmark -q
PING_INLINE: 95328.88 requests per second, p50=0.263 msec
PING_MBULK: 95693.78 requests per second, p50=0.263 msec
SET: 17217.63 requests per second, p50=3.111 msec
GET: 96525.09 requests per second, p50=0.263 msec
INCR: 16747.61 requests per second, p50=3.143 msec
LPUSH: 16194.33 requests per second, p50=3.247 msec
RPUSH: 16417.67 requests per second, p50=3.159 msec
LPOP: 16339.87 requests per second, p50=3.191 msec
RPOP: 15615.24 requests per second, p50=3.215 msec
SADD: 95510.98 requests per second, p50=0.263 msec
HSET: 15855.40 requests per second, p50=3.311 msec
SPOP: 94250.71 requests per second, p50=0.263 msec
ZADD: 95877.28 requests per second, p50=0.263 msec
ZPOPMIN: 95877.28 requests per second, p50=0.263 msec
LPUSH (needed to benchmark LRANGE): 16452.78 requests per second, p50=3.191 msec
LRANGE_100 (first 100 elements): 53676.86 requests per second, p50=0.463 msec
LRANGE_300 (first 300 elements): 23474.18 requests per second, p50=1.007 msec
LRANGE_500 (first 450 elements): 17114.50 requests per second, p50=1.351 msec
LRANGE_600 (first 600 elements): 12863.39 requests per second, p50=1.711 msec
MSET (10 keys): 14669.21 requests per second, p50=3.647 msec
3)将配置文件的appendonly改为no,关掉aof,重启redis,再来压测同样的指令
[root@iZ8vb3a9qxofwannyywl6zZ aof]# ../src/redis-benchmark -q
PING_INLINE: 96899.23 requests per second, p50=0.263 msec
PING_MBULK: 93720.71 requests per second, p50=0.263 msec
SET: 96618.36 requests per second, p50=0.263 msec
GET: 95419.85 requests per second, p50=0.263 msec
INCR: 98135.42 requests per second, p50=0.263 msec
LPUSH: 99304.87 requests per second, p50=0.255 msec
RPUSH: 99206.34 requests per second, p50=0.255 msec
LPOP: 97656.24 requests per second, p50=0.263 msec
RPOP: 96711.80 requests per second, p50=0.263 msec
SADD: 97847.36 requests per second, p50=0.263 msec
HSET: 98328.42 requests per second, p50=0.263 msec
SPOP: 96339.12 requests per second, p50=0.263 msec
ZADD: 97560.98 requests per second, p50=0.263 msec
ZPOPMIN: 96618.36 requests per second, p50=0.263 msec
LPUSH (needed to benchmark LRANGE): 97465.88 requests per second, p50=0.263 msec
LRANGE_100 (first 100 elements): 53561.86 requests per second, p50=0.463 msec
LRANGE_300 (first 300 elements): 24084.78 requests per second, p50=0.999 msec
LRANGE_500 (first 450 elements): 17649.13 requests per second, p50=1.343 msec
LRANGE_600 (first 600 elements): 12928.25 requests per second, p50=1.703 msec
MSET (10 keys): 101729.40 requests per second, p50=0.255 msec
4)结果分析
- 对各种读取操作来说,性能差别不大:get、spop、队列的range等
- 对写操作影响极大,以set为例,有将近6倍的差距,mset则更大,将近7倍
5)参考价值
如果你的项目里对数据安全性要求较高,写少读多的场景,可以适当使用aof
如果追求极致的性能,只做缓存,容忍数据丢失,还是关掉aof
5. Redis高可用
5.1 主从复制
5.1.1 面临问题
Redis有 两种不同的持久化方式, Redis 服务器通过持久化,把 Redis 内存中持久化到硬盘当中,当Redis 宕机时,我们重启 Redis 服务器时,可以由 RDB 文件或 AOF 文件恢复内存中的数据。
问题1:不过持久化后的数据仍然只在一台机器上,因此当硬件发生故障时,比如主板或 CPU 坏了,这时候无法重启服务器,有什么办法可以保证服务器发生故障时数据的安全性?或者可以快速恢复数据呢?
问题2:容量瓶颈
5.1.2 解决办法
针对这些问题,redis提供了 复制(replication) 的功能, 通过"主从(一主多从)"和"集群(多主多从)"的方 式对redis的服务进行水平扩展,用多台redis服务器共同构建一个高可用的redis服务系统。
5.1.3 主从复制
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave),数据的复制是单向的,只能由主节点到从节点。
5.1.4 常用策略
策略1 :一主多从 主机(写),从机(读)
策略2:薪火相传
5.1.5 主从复制原理
Redis 的主从复制是异步复制,异步分为两个方面,一个是 master 服务器在将数据同步到 slave 时是异步的,因此master服务器在这里仍然可以接收其他请求,一个是slave在接收同步数据也是异步的。
复制方式
- 全量复制
master 服务器会将自己的 rdb 文件发送给 slave 服务器进行数据同步,并记录同步期间的其他写入,再发送给 slave 服务器,以达到完全同步的目的,这种方式称为全量复制。
- slave向master确认runid,发送偏移量-1
- master收到后,把自己的runid,偏移量发送给salve
- slave保存信息
- master收到全量复制的命令后,执行basave,在后台生成rdb文件,并使用缓冲区记录从现在开始执行的所有命令
- 期间其他的写操作也会发送到同步的缓冲区中
- slave拿到rdb文件后清理所有的数据
- 加载rdb文件
增量复制
因为各种原因 master 服务器与 slave 服务器断开后, slave 服务器在重新连上 maste r服务器时会尝试重新获取断开后未同步的数据即部分同步,或者称为部分复制。
- 因为网络抖动断开链接
- master还是会把写请求放入复制缓冲区
- 从机尝试链接主机
- 把当前runid和偏移量发送给master,并执行pysnc命令同步
- master发现偏移量是在缓冲区范围内,返回continue命令
- 同步了偏移量差值的数据给slave
工作原理
master 服务器会记录一个 replicationId 的伪随机字符串,用于标识当前的数据集版本,还会记录一个当数据集的偏移量 offset ,不管 master 是否有配置 slave 服务器,replication Id和offset会一直记录并成对存在,我们可以通过以下命令查看replication Id和offset
> info repliaction
通过redis-cli在master或slave服务器执行该命令会打印类似以下信息(不同服务器数据不同,打印信息不同)
connected_slaves:1
slave0:ip=127.0.0.1,port=6380,state=online,offset=9472,lag=1
master_replid:2cbd65f847c0acd608c69f93010dcaa6dd551cee
master_repl_offset:9472
当master与slave正常连接时,slave使用PSYNC命令向master发送自己记录的旧master的replicationid和offset,而master会计算与slave之间的数据偏移量,并将缓冲区中的偏移数量同步到slave,此时master和slave的数据一致。
而如果slave引用的replication太旧了,master与slave之间的数据差异太大,则master与slave之间会使用全量复制的进行数据同步。
5.1.6 配置主从复制
注:主从复制的开启,完全是在从节点发起的;不需要我们在主节点做任何事情。
从节点开启主从复制,有3种方式:
(1)配置文件:在从服务器的配置文件中加入:slaveof
(2)redis-server启动命令后加入 --slaveof
(3)Redis服务器启动后,直接通过客户端执行命令:slaveof ,则该Redis实例成为从节点
启动
redis-server --port 端口号
链接
redis-cli -p 端口号
演示:
①、通过 info replication 命令查看三台节点角色
初始状态,三台节点都是master
②、设置主从关系,从节点执行命令:SLAVEOF 127.0.0.1 6379
再看主节点信息:
这里通过命令来设置主从关系,一旦服务重启,那么角色关系将不复存在。想要永久的保存这种关系,可以通过配置redis.conf 文件来配置。
slaveof 127.0.0.1 6379
取消复制:slave of on one
5.1.7 测试主从关系
①、增量复制
master 操作写入:
slave操作获取:
②、全量复制
通过执行 SLAVEOF 127.0.0.1 6379,如果主节点 6379 以前还存在一些 key,那么执行命令之后,从节点会将以前的信息也都复制过来
③、主从读写分离
尝试slave操作获取:
原因是在配置文件 6380redis.conf 中对于 slave-read-only 的配置
如果我们将其修改为 no 之后,执行写命令是可以的,但是从节点写命令的数据从节点或者主节点都不
能获取的。
④、主节点宕机
主节点 Maste 挂掉,两个从节点角色会发生变化吗?
上图可知主节点 Master 挂掉之后,从节点角色还是不会改变的。
⑤、主节点宕机后恢复
主节点Master挂掉之后,马上启动主机Master,主节点扮演的角色还是 Master 吗?
也就是说主节点挂掉之后重启,又恢复了主节点的角色。
5.2 sentinel哨兵模式
通过前面的配置,主节点Master 只有一个,一旦主节点挂掉之后,从节点没法担起主节点的任务,那么整个系统也无法运行。
如果主节点挂掉之后,从节点能够自动变成主节点,那么问题就解决了,于是哨兵模式诞生了。
哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例。
哨兵模式搭建步骤:
①、在配置文件目录下新建 sentinel.conf 文件,名字绝不能错,然后配置相应内容
sentinel monitor 被监控机器的名字(自己起名字) ip地址 端口号 得票数
分别配置被监控的名字,ip地址,端口号,以及得票数。上面的得票数为1表示表示主机挂掉后salve投票看让谁接替成为主机,得票数大于1便成为主机
②、启动哨兵
redis-sentinel /etc/redis/sentinel.conf
接下来,我们干掉主机 6379,然后看从节点有啥变化。
干掉主节点之后,我们查看后台打印日志,发现 6380投票变为主节点
PS:哨兵模式也存在单点故障问题,如果哨兵机器挂了,那么就无法进行监控了,解决办法是哨兵也建立集群,Redis哨兵模式是支持集群的。
6. Redis Cluster
引言
6.1主从 + 哨兵 问题分析
(1)在主从 + 哨兵模式中,仍然只有一个Master节点。当并发写请求较大时,哨兵模式并不能缓解写压力
(2) 在Redis Sentinel模式中,每个节点需要保存全量数据,冗余比较多
6.2 Cluster概念
从3.0版本之后,官方推出了Redis Cluster,它的主要用途是实现数据分片(Data Sharding),不过同样可以实现HA,是官方当前推荐的方案。
1.Redis-Cluster采用无中心结构
2.只有当集群中的大多数节点同时fail整个集群才fail。
3.整个集群有16384个slot,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod16384的值,决定将一个key放到哪个桶中。读取一个key时也是相同的算法。
4.当主节点fail时从节点会升级为主节点,fail的主节点online之后自动变成了从节点
6.3 故障转移
Redis集群的主节点内置了类似Redis Sentinel的节点故障检测和自动故障转移功能,当集群中的某个主节点下线时,集群中的其他在线主节点会注意到这一点,并对已下线的主节点进行故障转移
6.4 集群分片策略
Redis-cluster分片策略,是用来解决key存储位置的
常见的数据分布的方式:顺序分布、哈希分布、节点取余哈希、一致性哈希…
6.5 Redis 集群的数据分片
Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念.
预设虚拟槽,每个槽就相当于一个数字,有一定范围
Redis Cluster中预设虚拟槽的范围为0到16383
步骤:
1.把16384槽按照节点数量进行平均分配,由节点进行管理
2.对每个key按照CRC16规则进行hash运算
3.把hash结果对16383进行取余
4.把余数发送给Redis节点
5.节点接收到数据,验证是否在自己管理的槽编号的范围
- 如果在自己管理的槽编号范围内,则把数据保存到数据槽中,然后返回执行结果
- 如果在自己管理的槽编号范围外,则会把数据发送给正确的节点,由正确的节点来把数据保存在对应的槽中
需要注意的是:Redis Cluster的节点之间会共享消息,每个节点都会知道是哪个节点负责哪个范围内的数据槽
虚拟槽分布方式中,由于每个节点管理一部分数据槽,数据保存到数据槽中。当节点扩容或者缩容时,对数据槽进行重新分配迁移即可,数据不会丢失。
6.6 搭建Redis Cluster
步骤分析:
启动节点:将节点以集群方式启动,此时节点是独立的。
节点握手:将独立的节点连成网络。
槽指派:将16384个槽位分配给主节点,以达到分片保存数据库键值对的效果。
主从复制:为从节点指定主节点。
步骤实现
启动节点
(1)新建目录,并拷贝出6个节点的配置文件
mkdir redis-cluster
mkdir 900{1,2,3,4,5,6}
(2)将redis.conf,依次拷贝到每个900X目录内,并修改每个900X目录下的redis.conf配置文件:
以集群方式启动
# cluster-enabled yes 将前面的 # 去掉
集群节点nodes信息配置文件(是自动生成的)
# cluster-config-file nodes-6379.conf 修改为 cluster-config-file
"/usr/local/redis/cluster/nodes-9001.conf" # 对应各个端口
注意:前面的#要注释掉!
(3)启动6个Redis实例
查看进程:
节点握手&槽指派&主从复制
redis5.0使用redis-cli作为创建集群的命令,使用c语言实现,不再使用ruby语言。
1)有了实例后,搭建集群非常简单,使用redis-cli一行命令即可
#replicas表示副本数,如果指定1则表示1个从库做备用
redis-cli --cluster create 127.0.0.1:9001 127.0.0.1:9002 127.0.0.1:9003 127.0.0.1:9004 127.0.0.1:9005 127.0.0.1:9006 --cluster-replicas 1
参数解释:
–cluster-replicas 1:表示希望为集群中的每个主节点创建一个从节点(一主一从)。
–cluster-replicas 2:表示希望为集群中的每个主节点创建两个从节点(一主二从)。
2)备注:如果节点上有数据,可能会有错误提示:
[ERR] Node 127.0.0.1:8004 is not empty. Either the node already knows other nodes
(check with CLUSTER NODES) or contains some key in database 0.
删除dump.rdb,nodes.conf,登录redis-cli,flushdb即可
3)如果没问题,将收到集群创建成功的消息:
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join
....
>>> Performing Cluster Check (using node 127.0.0.1:8081)
M: a085dd0366e08d4c03093ea24351ce4e12fcb69f 127.0.0.1:8081
slots:[0-5460] (5461 slots) master
M: 843d8da882f78d3cb09b1eb837140aefba309e06 127.0.0.1:8082
slots:[5461-10922] (5462 slots) master
M: 043d39422d93ef5c7c69e1c6cfb1557f655b5d72 127.0.0.1:8083
slots:[10923-16383] (5461 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
集群验证
用redis-cli在服务器上set多个值,比如czbk,分别在不同的实例上,分片成功!
1)cluster命令验证
#使用redis-cli登录任意节点,使用cluster nodes可以查看集群信息
127.0.0.1:9001> cluster nodes
39c613372129fe80fe93b6fb3070f9562c315a59 127.0.0.1:9001@18082 master - 0
1615193645000 2 connected 5461-10922
725c09c568cb4010afe84d5cb4672fff5a248879 127.0.0.1:9002@18083 master - 0
1615193645976 3 connected 10923-16383
9fad54e90628814c1b2a5b57c2ad22b92f0f7b05 127.0.0.1:9003@18081 myself,master - 0
1615193644000 1 connected 0-5460
2)使用key值和数据验证
#注意,redis-cli参数:
# -c : 自动重定向到对应节点获取信息,如果不加,只会返回重定向信息,不会得到值
#不加 -c
[root@ src]# redis-cli -p 9001
127.0.0.1:9001> set a a
(error) MOVED 15495 127.0.0.1:8083
#加上 -c
[root@ src]# redis-cli -p 9001 -c
127.0.0.1:9001> set a a
-> Redirected to slot [15495] located at 127.0.0.1:9003 #自动跳到9003
OK
127.0.0.1:9003> get a #可以成功get到a的值
"a"
扩容
1)按上面方式,新起一个redis , 8084端口
#第一个参数是新节点的地址,第二个参数是任意一个已经存在的节点的IP和端口
redis-cli --cluster add-node 127.0.0.1:9097 127.0.0.1:9001
redis-cli --cluster add-node 127.0.0.1:9098 127.0.0.1:9001
2)使用redis-cli登录任意节点,使用cluster nodes查看新集群信息
127.0.0.1:9001> cluster nodes
#注意!新加进来的这个8084是空的,没有分配片段
eb49056da71858d58801f0f28b3d4a7b354956bc 127.0.0.1:9004@18084 master - 0
1602665893207 0 connected
16a3f8a4be9863e8c57d1bf5b3906444c1fe2578 127.0.0.1:9003@18082 master - 0
1602665891204 2 connected 5461-10922
214e4ca7ece0ceb08ad2566d84ff655fb4447e19 127.0.0.1:9002@18083 master - 0
1602665892000 3 connected 10923-16383
864c3f763ab7264ef0db8765997be0acf428cd60 127.0.0.1:9001@18081 myself,master - 0
1602665890000 1 connected 0-5460
3)重新分片
redis-cli --cluster reshard 127.0.0.1:9001
#根据提示一步步进行,再次查看node分片,可以了!
127.0.0.1:8081> cluster nodes
eb49056da71858d58801f0f28b3d4a7b354956bc 127.0.0.1:9004@18084 master - 0
1602666306047 4 connected 0-332 5461-5794 10923-11255
16a3f8a4be9863e8c57d1bf5b3906444c1fe2578 127.0.0.1:9003@18082 master - 0
1602666305045 2 connected 5795-10922
214e4ca7ece0ceb08ad2566d84ff655fb4447e19 127.0.0.1:9002@18083 master - 0
1602666305000 3 connected 11256-16383
864c3f763ab7264ef0db8765997be0acf428cd60 127.0.0.1:9001@18081 myself,master - 0
1602666303000 1 connected 333-5460
springboot
- 由以上原理就不难理解,springboot连接redis cluster时,可以连任意一台,也可以全部写。
- boot1.x默认客户端为jedis,2.x已经替换为Lettuce,Jedis在实现上是直接连接的redis server,Lettuce的连接是基于Netty的。两者配置项上略有不同。基础知识和配置,可以翻阅springbootdata部分文档。
- cluster下slave一般只作为对应master机器的数据备份,可以通过设置readonly作为读库,但一般不这么搞。如果你只是用作缓存,不在乎数据的丢失,觉得它浪费了资源,甚至你可以让slave数量为0,只做数据分片用。
- Jedis也好,Lettuce也好,其对于redis-cluster架构下的数据的读取,都是默认是按照redis官方对redis-cluster的建议,所以两者默认均不支持redis-cluster下的读写分离。
- 如果我们强行只配置slave地址而不配置master(这个操作比较欠),实际上还是可以读到数据,但其内部操作是通过slave重定向到相关的master主机上,然后再将结果获取和输出。