Redis缓存技术知识体系总结
2020-11-11 21:58:13 5 举报
AI智能生成
缓存 分布式 知识体系 Redis redis
作者其他创作
大纲/内容
参考资料
https://www.processon.com/view/5d4e808ae4b06e49190dbcc4?fromnew=1#map
https://www.processon.com/view/5d4e808ae4b06e49190dbcc4?fromnew=1#map
https://www.processon.com/view/5e847cd5e4b0f4e639c31727?fromnew=1#map
https://www.processon.com/mindmap/5fa96162e401fd45d10c508c
Redis知识图谱
两大维度,三大主线
应用维度
系统维度
高性能主线
高可用主线
高可扩展主线
核心原理分析
Redis概述
优点
读写性能优异-读11万,写8万
支持持久化数据
RDB
AOF
支持事务
所有命令操作都是原子性
支持多个命令合并打包的原子执行(打包顺序执行,之间不执行别的命令,别的命令进入等待队列)
数据结构丰富
支持主从复制
主机自动将数同步给从机
高性能
将用户访问的数据存在缓存中
高并发
将数据库中部分数据移到缓存中
缺点
受物理内存限制,不能作为海量数据高速读写
不具备自动容错和恢复功能,主从机的宕机都会导致部分请求的失败
主机宕机前,部分数据未同步从机,会引起数据不一致和数据丢失的问题
在线扩容较难
集群容量达到上线时再次扩容难度较大
Redis与Map/guava的优势
在多实例下,各个实例单独拥有缓存,导致缓存不一致
速度快的原因
完全基于内存
数据结构简单,操作也简单
采用单线程设计,避免上下文切换和竞争条件
使用多路I/O复用模型,非阻塞IO
https://www.processon.com/mindmap/5eaa3ab5e401fd21c19cdf2a
Redis基于Reactor模式开发了网络事件处理器,这个处理器被称为文件事件处理器(file event handler)。它的组成结构为4部分:多个套接字、IO多路复用程序、文件事件分派器、事件处理器。因为文件事件分派器队列的消费是单线程的,所以Redis才叫单线程模型。
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件虽然文件事件处理器以单线程方式运行, 但通过使用 I/O 多路复用程序来监听多个套接字, 文件事件处理器既实现了高性能的网络通信模型, 又可以很好地与redis 服务器中其他同样以单线程方式运行的模块进行对接, 这保持了 Redis 内部单线程设计的简单性。
子主题
使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,
Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
应用场景
计数器
缓存(内容可以失效)
数据库缓存、会话缓存、全网页缓存(FPC)
查找表(内容不能失效)
消息队列(订阅/发布功能)
分布式锁
redis支持的setnx实现分布式锁
官方推荐的RedLock分布式锁
数据结构
用户数据类型
字符串(String)
对字符串或字符串部分进行操作;
对数据可以进行自增减操作
可以是字符串、 整数或者浮点数
对数据可以进行自增减操作
可以是字符串、 整数或者浮点数
字符串对象String
int型编码
如果一个字符串对象存储的是一个整数值,并且这个整数值可以用long类型来表示,
那么就会使用int编码来生成这个字符串,对于double类型的数据也是当作字符串来存储的
那么就会使用int编码来生成这个字符串,对于double类型的数据也是当作字符串来存储的
raw编码
如果字符串对象保存的是一个字符串值。
并且这个字符串值长度大于32 则会用SDS来做为他的数据结构
并且这个字符串值长度大于32 则会用SDS来做为他的数据结构
embstr编码
是专门用于保存短字符串的一种优化编码方式,它会将字符串存放在一块连续的内存上,
在创建这种类型的字符串时只会调用一次内存分配和内存释放而对于raw编码而言在创建对象时会调用两次内存分配和内存释放
在创建这种类型的字符串时只会调用一次内存分配和内存释放而对于raw编码而言在创建对象时会调用两次内存分配和内存释放
编码的转换,对于int型编码当对其执行添加操作时会将其转换为raw编码。 而对于embstr而言redis并没有为其设置操作函数,所以当我们想要对embstr编码进行操作时,都会将其转为raw编码然后在进行操作。
应用场景:
1、缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,
因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,
可以大大加快系统的读写速度、以及降低后端数据库的压力。
2、计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。
而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
3、共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,
但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,
每次用户Session的更新和获取都可以快速完成。大大提高效率。
1、缓存功能:String字符串是最常用的数据类型,不仅仅是Redis,各个语言都是最基本类型,
因此,利用Redis作为缓存,配合其它数据库作为存储层,利用Redis支持高并发的特点,
可以大大加快系统的读写速度、以及降低后端数据库的压力。
2、计数器:许多系统都会使用Redis作为系统的实时计数器,可以快速实现计数和查询的功能。
而且最终的数据结果可以按照特定的时间落地到数据库或者其它存储介质当中进行永久保存。
3、共享用户Session:用户重新刷新一次界面,可能需要访问一下数据进行重新登录,或者访问页面缓存Cookie,
但是可以利用Redis将用户的Session集中管理,在这种模式只需要保证Redis的高可用,
每次用户Session的更新和获取都可以快速完成。大大提高效率。
列表(List)
从两端压入或弹出元素;对单个或多个进行修剪操作;保留一定范围的元素
列表对象
ziplist编码
压缩列表,每个压缩列表节点保存了一个列表的元素
linkedlist编码
双端链表作为底层实现,每个双端链表节点都保存了一个字符串,而每个字符串都保存了这个节点的值。发没发现双端列表的实现借助了字符串对象,因为其每个节点都保存的是一个字符串,而真正的值是由字符串呈现的。 这种叫做对象的嵌套对象。双端链表里面嵌套了字符串对象,而redis中只有字符串对象是被允许嵌套在另一个对象中的
编码的转换:只有链表对象保存的所有字符串长度小于64字节并且保存的元素数量小于512个才会使用压缩列表否则都会使用双端链表
应用场景:
List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。
比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,
而且当文章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,
可以完美解决分页查询功能。大大提高查询效率
List 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。
比如,我们常用的博客网站的文章列表,当用户量越来越多时,而且每一个用户都有自己的文章列表,
而且当文章多时,都需要分页展示,这时可以考虑使用Redis的列表,列表不但有序同时还支持按照范围内获取元素,
可以完美解决分页查询功能。大大提高查询效率
集合(Set)
添加、获取、移除单个元素,检查一个元素是否存在,计算交、并、差集计算,在集合里面随机获取元素
集合对象
intset编码
使用整数集合作为底层实现 比如SADD numbers 1 2 3
hashtable编码
使用字典作为底层实现,他的实现方式是 对于字典用key的值保存集合的每一个元素,值都是为空的。有点类似与set
编码的转换:只有集合元素保存的对象都是整数值并且集合对象保存的元素数量不超过512个才会使用intset编码
应用场景:
Set 是无序集合,会自动去重,有需要去重的功能可以直接放进set,自动就完成了去重
你当然也可以基于 JVM 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于Redis进行全局的 Set 去重。
可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁?对吧
Set 是无序集合,会自动去重,有需要去重的功能可以直接放进set,自动就完成了去重
你当然也可以基于 JVM 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于Redis进行全局的 Set 去重。
可以基于 Set 玩儿交集、并集、差集的操作,比如交集吧,我们可以把两个人的好友列表整一个交集,看看俩人的共同好友是谁?对吧
散列表(Hash)
添加、获取、移除、单个键值对;获取所有键值对;检查某个键是否存在
哈希对象
ziplist编码
使用了压缩列表当作底层设计,当添加一对键值对时,会先在压缩列表尾部添加键,然后在尾部添加值。这样就保证了一对键值对在列表中挨在一起
hashtable编码
底层使用的是字典来实现
编码转换:只有哈希对象保存的所有键值对的键和值的字符串长度都小于64字节并且键值对数量小于512个才会使用ziplist编码否则都会使用hashtable
应用场景:
Redis中的Hashes类型可以看成具有String Key和String Value的map容器
Redis中的Hashes类型可以看成具有String Key和String Value的map容器
有序集合(ZSet)
添加、获取、删除元素;根据分值范围或成员获取元素;计算一个键的排名
有序集合对象
ziplist编码
由ziplist底层实现,每个集合元素由两个紧紧挨在一起的压缩节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。按元素的分值从小到大排序。
skiplist编码
底层由zset实现,zset使用了skiplist加字典的形式,这样保证了 我们可以基于skiplist查找某一个范围的元素,使用字典可以在时间复杂度为O(1)情况下访问某一个元素的值
编码的转换:只有保存的元素数量小于128个并且每个元素小于64字节才会使用ziplist
应用场景:
Sorted set 是排序的 Set,去重但可以排序
排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,
榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等
Sorted set 是排序的 Set,去重但可以排序
排行榜:有序集合经典使用场景。例如视频网站需要对用户上传的视频做排行榜,
榜单维护可能是多方面:按照时间、按照播放量、按照获得的赞数等
特殊类型
Pipeline(管道)
管道就是打包多条无关命令批量执行,以减少多个命令分别执行消
耗的网络交互时间(TCP网络交互),可以显著提升Redis的性能
耗的网络交互时间(TCP网络交互),可以显著提升Redis的性能
地理位置(geospatial)
地理空间,可以录入地理坐标并计算距离
底层实现原理是ZSet
HyperLogLog 是用来做基数统计的算法
基数统计的算法,根据并集的数量来计数
占用的内存固定,只需要12kb的内存,有0.81%错误率
位存储(bitmaps)
位存储,使用二进制记录,只有0和1两种状态
可以用来统计用户信息、打卡,两个状态的都可以
内部数据类型
整数数组
整数数组和压缩列表在查找时间复杂度方面并没有很大的优势,
那为什么 Redis 还会把它们作为底层数据结构呢?
那为什么 Redis 还会把它们作为底层数据结构呢?
内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少
数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,
默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度
默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度
压缩列表
Hash表
跳表
跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位
时间复杂度是o(logN)
索引设计
全局hash表
hash表变慢的原因
采用链表解决hash冲突,当冲突的数据多了的会导致变慢
hash表变慢的解决方案
rehash增加hash桶的数量
Redis rehash的方案
采用两个hash表
开始时使用 hash表1,当rehash时给hash表2分配更大的空间
将hash表1的数据数据拷贝并映射到hash表2中
大量的数据拷贝会影响性能
采用渐进式rehash
子主题
释放hash表1的空间
数据操作
集合操作耗时原则
单元素操作是基础
范围操作非常耗时
比如 Hash 类型的 HGETALL 和 Set 类型的 SMEMBERS,或者返回一个范围内的部分数据,
比如 List 类型的 LRANGE 和 ZSet 类型的 ZRANGE。
这类操作的复杂度一般是 O(N),比较耗时,我们应该尽量避免。
比如 List 类型的 LRANGE 和 ZSet 类型的 ZRANGE。
这类操作的复杂度一般是 O(N),比较耗时,我们应该尽量避免。
SCAN 系列操作(包括 HSCAN,SSCAN 和 ZSCAN),这类操作实现了渐进式遍历,每次只返回有限数量的数据。
这样一来,相比于 HGETALL、SMEMBERS 这类操作来说,就避免了一次性返回所有元素而导致的 Redis 阻塞。
这样一来,相比于 HGETALL、SMEMBERS 这类操作来说,就避免了一次性返回所有元素而导致的 Redis 阻塞。
统计操作通常高效
合类型对集合中所有元素个数的记录,例如 LLEN 和 SCARD
这类操作复杂度只有 O(1)
例外情况只有几个
持久化机制
AOF
AOF原理
redis所有的写命令会追加到 AOF 缓冲区中,根据对应的策略向硬盘进行同步操
作,随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的
作,随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩的目的
触发机制
(1)每修改同步always:同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好
(2)每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失
(3)不同no:从不同步
(2)每秒同步everysec:异步操作,每秒记录 如果一秒内宕机,有数据丢失
(3)不同no:从不同步
机制说明
1.记录每一个redis的写命令以日志的形式进行存储
2.AOF刷盘时间间隔
1.有命令就刷盘一次
2.一秒刷盘一次(推荐,也是默认的)
3.由系统决定刷盘时间间隔
3.为啥需要设置刷盘时间:持久化的目的是把数据记录在磁盘上,所以当数据在内存中的时候,就需要把内存中的数据放到磁盘上,放到磁盘上的时间间隔就是刷盘时间;
AOF重写
AOF的方式也同时带来了另一个问题。持久化文件会变的越来越大。为了压缩aof的持久化文件。
redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写
重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似
redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写
重写aof文件的操作,并没有读取旧的aof文件,而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似
AOF重写流程
优点
(1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据。
(2)AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。
(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据
(2)AOF日志文件没有任何磁盘寻址的开销,写入性能非常高,文件不容易破损。
(3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。
(4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据
缺点
(1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大
(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的
(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。
(2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的
(3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。
RDB
原理
redis会单独创建(fork)一个与当前进程一模一样的子进程来进行持久化,
将数据写入到一个临时文件中,待持久化结束后替换上次持久化好的文件
触发机制
save
该命令会阻塞当前Redis服务器,执行save命令期间,Redis不能处理其他命令,直到RDB过程完成为止
执行完成时候如果存在老的RDB文件,就把新的替代掉旧的。
我们的客户端可能都是几万或者是几十万,这种方式显然不可取。
我们的客户端可能都是几万或者是几十万,这种方式显然不可取。
bgsave
执行该命令时,Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求
具体流程如下
具体操作是Redis进程执行fork操作创建子进程,RDB持久化过程由子进程负责,完成后自动结束。
阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令
阻塞只发生在fork阶段,一般时间很短。基本上 Redis 内部所有的RDB操作都是采用 bgsave 命令
自动触发
自动触发是由我们的配置文件来完成的。在redis.conf配置文件中
save:这里是用来配置触发 Redis的 RDB 持久化条件,也就是什么时候将内存中的数据保存到硬盘。比如“save m n”。表示m秒内数据集存在n次修改时,自动触发bgsave。
默认如下配置:
#表示900 秒内如果至少有 1 个 key 的值变化,则保存save 900 1
#表示300 秒内如果至少有 10 个 key 的值变化,则保存save 300 10
#表示60 秒内如果至少有 10000 个 key 的值变化,则保存save 60 10000
不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能
默认如下配置:
#表示900 秒内如果至少有 1 个 key 的值变化,则保存save 900 1
#表示300 秒内如果至少有 10 个 key 的值变化,则保存save 300 10
#表示60 秒内如果至少有 10000 个 key 的值变化,则保存save 60 10000
不需要持久化,那么你可以注释掉所有的 save 行来停用保存功能
优
(1)RDB文件紧凑,全量备份,非常适合用于进行备份和灾难恢复。
(2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
(3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
(2)生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
(3)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
劣
RDB快照是一次全量备份,存储的是内存数据的二进制序列化形式,存储上非常紧凑。当进行快照持久化时,会开启一个子进程专门负责快照持久化,子进程会拥有父进程的内存数据,父进程修改内存子进程不会反应出来,所以在快照持久化期间修改的数据不会被保存,可能丢失数据
混合持久化
手动开启:aof-use-rdb-preamble yes
5.0以后默认开启
重写机制优化
重写后新的AOF文件前半段是RDB格式的全量数据,后半段是AOF格式的增量数据
优点
由于绝大部分都是RDB格式,加载速度快,同时结合AOF,增量的数据得以保存,数据更少丢失
缺点
兼容性差,4.0以前不识别该aof文件,可读性差
应该如何选择和平衡两种方式
通常的指示是,如果您希望获得与PostgreSQL可以提供的功能相当的数据安全性,则应同时使用两种持久性方法。
如果您非常关心数据,但是在灾难情况下仍然可以承受几分钟的数据丢失,则可以仅使用RDB。
有很多用户单独使用AOF,但我们不建议这样做,因为不时拥有RDB快照对于进行数据库备份,加快重启速度以及AOF引擎中存在错误是一个好主意。
注意:由于所有这些原因,我们将来可能会最终将AOF和RDB统一为一个持久性模型(长期计划)。
------引自官网
如果您非常关心数据,但是在灾难情况下仍然可以承受几分钟的数据丢失,则可以仅使用RDB。
有很多用户单独使用AOF,但我们不建议这样做,因为不时拥有RDB快照对于进行数据库备份,加快重启速度以及AOF引擎中存在错误是一个好主意。
注意:由于所有这些原因,我们将来可能会最终将AOF和RDB统一为一个持久性模型(长期计划)。
------引自官网
建议查看redis官网,持久化页面:https://redis.io/topics/persistence
AOF和RDB持久性之间的相互作用
Redis> = 2.4可以确保避免在RDB快照操作正在进行时触发AOF重写,或者在AOF重写正在进行时允许BGSAVE。这样可以防止两个Redis后台进程同时执行繁重的磁盘I / O。
当进行快照时,用户使用BGREWRITEAOF显式请求日志重写操作时,服务器将以OK状态码答复,告知用户已计划该操作,并且快照完成后将开始重写。
如果同时启用了AOF和RDB持久性,并且Redis重新启动,则AOF文件将用于重建原始数据集,因为它可以保证是最完整的。
当进行快照时,用户使用BGREWRITEAOF显式请求日志重写操作时,服务器将以OK状态码答复,告知用户已计划该操作,并且快照完成后将开始重写。
如果同时启用了AOF和RDB持久性,并且Redis重新启动,则AOF文件将用于重建原始数据集,因为它可以保证是最完整的。
备份Redis数据
在服务器中创建一个cron作业,在一个目录中创建RDB文件的每小时快照,在另一个目录中创建每日快照。
每次运行cron脚本时,请确保调用find命令以确保删除太旧的快照:例如,您可以在最近的48小时内每小时拍摄一次快照,而在一个或两个月内每天拍摄一次。确保使用数据和时间信息命名快照。
每天至少有一次确保将RDB快照传输到数据中心外部或至少传输到运行Redis实例的物理计算机外部。
每次运行cron脚本时,请确保调用find命令以确保删除太旧的快照:例如,您可以在最近的48小时内每小时拍摄一次快照,而在一个或两个月内每天拍摄一次。确保使用数据和时间信息命名快照。
每天至少有一次确保将RDB快照传输到数据中心外部或至少传输到运行Redis实例的物理计算机外部。
线程模型
图解
子主题
redis与memcache的区别
线程操作
redis数据处理是单线程,memcache是多线程处理
数据结构
Redis支持更多更复杂的数据结构,memcache只支持keyvalue的字符串数据;
数据安全性
Redis支持数据的持久化,会把数据同步到磁盘上;memcache不支持数据的持久化
数据备份
Redis支持数据备份,需要开启主从模式;memcache不支持数据备份
过期策略
Redis支持更多的过期策略;memcache支持的过期策略少
事务
概念:Redis 事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
事务的几个关键命令
事务中发生错误
在调用EXEC命令之前出现错误
可能原因:
命令可能存在语法错误(参数数量错误,错误的命令名称…);或者可能存在某些关键条件,如内存不足的情况(如果服务器使用maxmemory指令做了内存限制)。
redis的结果:
此时redis会返回错误,中止事务并清除命令队列
命令可能存在语法错误(参数数量错误,错误的命令名称…);或者可能存在某些关键条件,如内存不足的情况(如果服务器使用maxmemory指令做了内存限制)。
redis的结果:
此时redis会返回错误,中止事务并清除命令队列
在调用EXEC命令之后出现错误
可能原因:
使用错误的值对某个key进行操作,比如使用list操作命令操作string
redis的结果:
此时redis会执行完剩余的正确命令。错误的命令报错
使用错误的值对某个key进行操作,比如使用list操作命令操作string
redis的结果:
此时redis会执行完剩余的正确命令。错误的命令报错
事务的执行过程
1.开启事务
MULTI 命令的执行标记着事务的开始:这个命令唯一做的就是, 将客户端的 REDIS_MULTI 选项打开, 让客户端从非事务状态切换到事务状态
2.命令入队
当客户端进入事务状态之后, 服务器在收到来自客户端的命令时, 不会立即执行命令, 而是将这些命令全部放进一个事务队列里, 然后返回 QUEUED , 表示命令已入队:
3.执行事务
EXEC命令执行,事务队列中的所有命令会被执行,客户端会从事务状态返回非事务状态,事务执行完成
流程图
过期/淘汰策略
过期策略
策略方式
定时过期策略
到了时间就立即移除(对内存友好,但会占用大量cpu)
惰性过期策略
当访问一个key的时候再判断是否过期(对内存不够友好)
定期过期策略
每过一段扫描一次数据,清楚过过期的
redis采用惰性过期策略和定期过期策略
redis通过expire和persist(移除可以的过期时间)设置过期时间和永久生效
淘汰策略
指缓存的内容不足,怎么处理需要新写入需要额外空间的数据
策略方式
全局的键空间选择性移除
allkeys-lru
最近最少使用
allkeys-random
任意淘汰一个
no-enviction
禁止淘汰,新写入会报错,但保证数据不会丢失,系统默认的方式
设置过期时间的键空间移除
volatile-lru
最近最少使用的数据
volatile-ttl
最接近淘汰时间的数据
volatile-random
随机移除数据
高性能设计
处理层:采用单线程
Redis只是在网络IO和键值对读写上是单线程。
其他处理会利用多线程,利于AOF等
其他处理会利用多线程,利于AOF等
多线程不快的原因
如果有资源竞争,例如多线程同时访问共享资源,
会引入额外的开销,而且增加系统设计实现的复杂度,例如锁
会引入额外的开销,而且增加系统设计实现的复杂度,例如锁
内存层
大部分操作在内存中完成
采用高效的数据结构,如hash表,跳表
网络层
采用多路复用机制,使在网络IO操作中能够并发处理大量的客户端请求,实现高吞吐率
容许内核中,同时监听多个套接字和已连接的套接字
select/epoll提供了基于事件回调机制,即针对不同时间的发送,调用响应处理函数
影响Redis性能的因素
子主题
高可扩展设计
应用场景分析
分布式锁
先用setnx来争抢锁,抢到之后,再用expire给锁加一个失效时间,防止锁忘记了释放
如果在setnx之后,expire之前,进程crash或者重启,锁就得不到释放。现在可以把setnx和expire合成一条命令使用
jedis实现分布式锁
异步队列
一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。 也可以使用 blpop,在没有消息的时候,它会阻塞住直到消息到来。
使用pub/sub主题订阅者模式,可以实现 1:N 的消息队列,但是 在消费者下线的情况下,生产的消息会丢失,此时得使用专业的消息队列如RocketMQ
使用pub/sub主题订阅者模式,可以实现 1:N 的消息队列,但是 在消费者下线的情况下,生产的消息会丢失,此时得使用专业的消息队列如RocketMQ
延时队列
使用sortedset,拿时间戳当作score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理
问题分析
Redis问题画像
图解
常见问题
缓存击穿
问题描述:缓存的某个热点key一直抗着大量并发查询。在这个key失效的瞬间,大量的查询击穿缓存,打到数据库(想象一个水桶底部破了一个洞)
解决办法:
1.设置热点数据永不过期
2.限流(牺牲部分用户体验保证系统可用性)
1.设置热点数据永不过期
2.限流(牺牲部分用户体验保证系统可用性)
缓存雪崩
问题描述:大量的热点key在同一时间失效,瞬间打崩数据库
解决方法:
1.设置热点数据永不过期,有更新操作就更新缓存就行了
2.存入数据时,设置失效时间上加一个随机值,保证热点key不在同一时间失效
3.redis是集群部署,把热点数据分布在不同的redis库中也能避免全部失效的问题
1.设置热点数据永不过期,有更新操作就更新缓存就行了
2.存入数据时,设置失效时间上加一个随机值,保证热点key不在同一时间失效
3.redis是集群部署,把热点数据分布在不同的redis库中也能避免全部失效的问题
缓存穿透
问题描述:大量查询缓存和数据库中都不存在的数据,导致数据库压力过大,可能挂掉。常被用来使用不存在的key进行恶意攻击
解决办法:
1.增加参数校验
2.从nginx网关层进行配置,对于单个ip,每秒访问次数超过阙值都拉黑
3.布隆过滤器(Bloom Fliter)可以高效的判断,key是否存在于数据库,如果不存在就直接返回,如果存在就去查DB刷新KV然后return
4.如果从数据库查不到的数据可以直接缓存一个null,不过失效时间设置的不宜太长,比如1分钟
1.增加参数校验
2.从nginx网关层进行配置,对于单个ip,每秒访问次数超过阙值都拉黑
3.布隆过滤器(Bloom Fliter)可以高效的判断,key是否存在于数据库,如果不存在就直接返回,如果存在就去查DB刷新KV然后return
4.如果从数据库查不到的数据可以直接缓存一个null,不过失效时间设置的不宜太长,比如1分钟
布隆过滤器
缓存淘汰策略
过期机制
定期删除
redis默认100ms就会去随机检查一些设置了过期时间的key,如果过期了,就删除。ps:随机,如果全部检查,redis早卡死了
惰性删除
获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除
存在的问题
由于redis定期删除是随机抽取检查,不可能扫描清除掉所有过期的key并删除,然后一些key由于未被请求,惰性删除也未触发。这样redis的内存占用会越来越高。此时就需要内存淘汰机制
过期机制的官方文档
常见的缓存淘汰策略
FIFO:First In First Out 先进先出。判断被储存的时间,离目前最远的时间的数据优先被淘汰
LRU:Least Recently Used 最近最少使用。判断最近被使用的时间,离目前最远的数据优先被淘汰
LFU:Least Frequently Used 最不经常使用。在一段时间内,数据使用次数最少的被淘汰
redis提供的缓存淘汰策略
noeviction:当内存达到限制并且客户端尝试执行会让更多内存被使用的命令时,返回错误(大部分的写入指令,但DEL和几个例外不会导致内存增加)
allkeys-lru::尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru:尝试回收最少使用的键(LRU),但仅限于在过期集合的键(有设置过期时间),使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random::回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键(有设置过期时间)
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
如果没有键满足回收的前提条件的话,策略volatile-lru, volatile-random以及volatile-ttl就和noeviction 差不多了
关于淘汰策略的官方文档
缓存预热
缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统,刷新过期时间。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
缓存预热解决方案:
(1)直接写个缓存刷新页面,上线时手工操作下;
(2)数据量不大,可以在项目启动的时候自动进行加载;
(3)定时刷新缓存;
缓存预热解决方案:
(1)直接写个缓存刷新页面,上线时手工操作下;
(2)数据量不大,可以在项目启动的时候自动进行加载;
(3)定时刷新缓存;
热数据和冷数据
概念
比如网站的用户总数就是一个小而热的数据,但是比如每个用户的个人轨迹信息就是一个量大但是还冷热不均的数据,
防止数据无限膨胀,所以用户缓存放到内存中都要设立过期时间。
比如,论坛的最新发表列表,最新报名列表,包括比如最新激活的用户可以存在redis做最新列表的使用方式。
redis 一定要用在小而热的情况,防止数据的无限膨胀。
防止数据无限膨胀,所以用户缓存放到内存中都要设立过期时间。
比如,论坛的最新发表列表,最新报名列表,包括比如最新激活的用户可以存在redis做最新列表的使用方式。
redis 一定要用在小而热的情况,防止数据的无限膨胀。
建议
基于redis做冷热分离从技术上是可行的,从业务实用角度看却不一定。因为首先redis不能很好区分冷热数据,然后很难避免读取落地冷数据时的性能问题,因此肯定不如纯内存的redis性能好,而用户对KV数据库性能的期望是没有最好,只有更好。随着内存越来越大、越来越便宜,更多的数据可以直接放到redis内存,会进一步导致冷热分离成为一个无人使用的鸡肋功能。
缓存更新
除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
(1)定时去清理过期的缓存;
(2)当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
缓存降级
核心就是弃车保帅,保证核心服务可用,降级可以丢弃的服务,可以让程序实施自动降级,也可以人工紧急降级。
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
(1)一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
(2)警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
(3)错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
(4)严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
数据一致性
先操作 Redis,再操作数据库
问题焦点
如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据
延时双删策略
在写库前后都进行redis.del(key)操作,并且设定合理的超时时间。具体步骤是:
1)先删除缓存
2)再写数据库
3)休眠500毫秒(根据具体的业务时间来定)
4)再次删除缓存。
那么,这个500毫秒怎么确定的,具体该休眠多久呢? 需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。 当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。
1)先删除缓存
2)再写数据库
3)休眠500毫秒(根据具体的业务时间来定)
4)再次删除缓存。
那么,这个500毫秒怎么确定的,具体该休眠多久呢? 需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。 当然,这种策略还要考虑 redis 和数据库主从同步的耗时。最后的写数据的休眠时间:则在读数据业务逻辑的耗时的基础上,加上几百ms即可。比如:休眠1秒。
先操作数据库,再操作 Redis
1、不做任何操作,等着Redis里的缓存数据过期后,自动从数据库同步最新的数据,此时最严重的数据不一致性周期就是在缓存过期的一段时间(考虑一下这个过期时间的范围);如果在这个时间段内,又有新的更新请求,也许这次就更新缓存成功了。
2、如果数据一致性要求比较高,那么 Redis 操作失败后,我们把这个操作记录下来,异步处理,用 Redis 的数据去和数据库比对,如果不一致,再次更新缓存确保缓存数据与数据库数据一致。
2、如果数据一致性要求比较高,那么 Redis 操作失败后,我们把这个操作记录下来,异步处理,用 Redis 的数据去和数据库比对,如果不一致,再次更新缓存确保缓存数据与数据库数据一致。
设置缓存的过期时间
最终一致性的解决方案
所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存
强一致性、弱一致性、最终一致性
概念
从客户端角度,多进程并发访问时,更新过的数据在不同进程如何获取的不同策略,决定了不同的一致性。对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。如果能容忍后续的部分或者全部访问不到,则是弱一致性。如果经过一段时间后要求能访问到更新后的数据,则是最终一致性。
最终一致性细分
因果一致性。
如果进程A通知进程B它已更新了一个数据项,那么进程B的后续访问将返回更新后的值,且一次写入将保证取代前一次写入。与进程A无因果关系的进程C的访问遵守一般的最终一致性规则。
读己之所写(read-your-writes)”一致性。
当进程A自己更新一个数据项之后,它总是访问到更新过的值,绝不会看到旧值。这是因果一致性模型的一个特例。
会话(Session)一致性。
这是上一个模型的实用版本,它把访问存储系统的进程放到会话的上下文中。只要会话还存在,系统就保证“读己之所写”一致性。如果由于某些失败情形令会话终止,就要建立新的会话,而且系统的保证不会延续到新的会话。
单调(Monotonic)读一致性。
如果进程已经看到过数据对象的某个值,那么任何后续访问都不会返回在那个值之前的值。
单调写一致性。
系统保证来自同一个进程的写操作顺序执行。要是系统不能保证这种程度的一致性,就非常难以编程了。
高可靠设计
主从复制模式
定义
定义:最简单的模式,一个集群中有且只有一个主节点,
主节点可读写,从节点只读。主节点下线,影响写服务,不影响读服务服务
主节点可读写,从节点只读。主节点下线,影响写服务,不影响读服务服务
架构设计
基本概念
1.集群中的每一个redis称为一个节点
2.有两种节点,主节点Master,从节点slave。每个节点都要设置一个优先级,即这个节点在故障切换过程中被选举为master的优先级
3.集群的可用性判断:集群中任何一个master挂了,且这个master没有slave可用,那么这个集群就是挂了
2.有两种节点,主节点Master,从节点slave。每个节点都要设置一个优先级,即这个节点在故障切换过程中被选举为master的优先级
3.集群的可用性判断:集群中任何一个master挂了,且这个master没有slave可用,那么这个集群就是挂了
数据同步机制
:当slave启动后,主动向master发送SYNC命令。master接收到SYNC命令后在后台保存快照(RDB持久化)和缓存保存快照这段时间的命令,然后将保存的快照文件和缓存的命令发送给slave。slave接收到快照和命令后,加载快照文件和缓存的执行命令。复制初始化后,master每次接收到的写命令都会同步发送给slave,保证主从数据的一致性
同步过程
1、从节点执行 slaveof 命令。
2、从节点只是保存了 slaveof 命令中主节点的信息,并没有立即发起复制。
3、从节点内部的定时任务发现有主节点的信息,开始使用 socket 连接主节点。
4、连接建立成功后,发送 ping 命令,希望得到 pong 命令响应,否则会进行重连。
5、如果主节点设置了权限,那么就需要进行权限验证,如果验证失败,复制终止。
6、权限验证通过后,进行数据同步,这是耗时最长的操作,主节点将把所有的数据全部发送给从节点。
7、当主节点把当前的数据同步给从节点后,便完成了复制的建立流程。接下来,主节点就会持续的把写命令发送给从节点,保证主从数据一致性。
数据同步命令
sync:redis 2.8之前的同步命令
psync:redis 2.8之后的同步命令
格式:psync{runId}{offset}
runId:从节点所复制主节点的运行 id
offset:当前从节点已复制的数据偏移量
主节点复制积压缓冲区:
执行流程
主节点会根据 runid 和 offset 决定返回结果
1、如果回复 +FULLRESYNC {runId} {offset} ,那么从节点将触发全量复制流程。
2、如果回复 +CONTINUE,从节点将触发部分复制。
3、如果回复 +ERR,说明主节点不支持 2.8 的 psync 命令,将使用 sync 执行全量复制。
实现主从复制的两种方式
slaveof命令
建立主从命令:slaveof ip port
取消主从命令:slaveof no one
redis.conf配置文件配置
格式:slaveof ip port
从节点只读:slave-read-only yes #配置只读
主从复制
基本概念
复制ID:master节点的数据集的一个随机字符串ID,标记了某个master节点的数据集,
当一个master节点重启或者一个slave节点升级为master节点时,复制ID会重新生成
当一个master节点重启或者一个slave节点升级为master节点时,复制ID会重新生成
偏移量:可以理解为数据的版本号,每次写数据,偏移量都会递增。偏移量一致,则表示数据同步一致
全量复制:顾名思义,使用RDB做master的完全拷贝。一般发生在新slave节点启动或者旧slave节点重连且无法满足增量数据复制条件的时候
增量复制:master向slave发送每一次写操作的命令,是主从复制优先尝试的方式
主从复制过程:
1.当一个slave启动的时候,会使用PSYNC命令将原本的master的复制ID以及偏移量发送给现在的master,请求同步数据
2.如果masterID还可以追溯(复制ID没变或者复制ID是最近故障的master的ID,还保存在新的master中,节点升级 为master,复制ID变了,可能还是做增量复制),复制积压缓存足够,那就直接做增量复制
3.如果复制ID已经无法追溯,或者挤压缓存不足,则做全量备份
1.当一个slave启动的时候,会使用PSYNC命令将原本的master的复制ID以及偏移量发送给现在的master,请求同步数据
2.如果masterID还可以追溯(复制ID没变或者复制ID是最近故障的master的ID,还保存在新的master中,节点升级 为master,复制ID变了,可能还是做增量复制),复制积压缓存足够,那就直接做增量复制
3.如果复制ID已经无法追溯,或者挤压缓存不足,则做全量备份
全量复制过程
1.master节点执行BGSAVE命令fork子进程,生成RDB快照。同时在缓存新进来的写命令
2.master将快照发送给slave
3.slave收到RDB加载并刷新数据
4.master将缓存的新命令以命令流的形式发送给slave
1.master节点执行BGSAVE命令fork子进程,生成RDB快照。同时在缓存新进来的写命令
2.master将快照发送给slave
3.slave收到RDB加载并刷新数据
4.master将缓存的新命令以命令流的形式发送给slave
积压缓存区的判断过程
1.主从分别维护一个seq,主每次完成一个请求就seq加一,从每次同步完后就更新自己的seq
2.从每次打算同步时,都是携带自己的seq到主,主将自身的seq与从做差结果与挤压缓存区大小 比较,如果小于挤压缓存区大小,直接从挤压缓存区取相应的操作做部分重同步
3.否则说明挤压缓存区的数据不足以cover主从不一致的数据,即做全量同步
1.主从分别维护一个seq,主每次完成一个请求就seq加一,从每次同步完后就更新自己的seq
2.从每次打算同步时,都是携带自己的seq到主,主将自身的seq与从做差结果与挤压缓存区大小 比较,如果小于挤压缓存区大小,直接从挤压缓存区取相应的操作做部分重同步
3.否则说明挤压缓存区的数据不足以cover主从不一致的数据,即做全量同步
全量复制
流程图
步骤
1、slave发送psync,由于是第一次复制,不知道master的runid,自然也不知道offset,所以发送psync ? -1
2、master收到请求,发送master的runid和offset给从节点。
3、从节点slave保存master的信息
4、主节点bgsave保存rdb文件
5、RDB文件生成完毕之后,主节点会将RDB发送给slave。
并且在④和⑤的这个过程中产生的数据,会写到复制缓冲区repl_back_buffer之中去。
6、从节点收到 RDB 文件并加载到内存中
7、主节点在从节点接受数据的期间,将新数据保存到“复制客户端缓冲区”,当从节点加载 RDB 完毕,再发送过去。
(如果从节点花费时间过长,将导致缓冲区溢出,最后全量同步失败)
(如果从节点花费时间过长,将导致缓冲区溢出,最后全量同步失败)
8、从节点清空数据后加载 RDB 文件,如果 RDB 文件很大,这一步操作仍然耗时,如果此时客户端访问,将导致数据不一致,可以使用配置slave-server-stale-data 关闭.
9、从节点成功加载完 RBD 后,如果开启了 AOF,会立刻做 bgrewriteaof。
复制缓冲区(repl_back_buffer)
注意
1、如过 RDB 文件大于 6GB,并且是千兆网卡,Redis 的默认超时机制(60 秒),会导致全量复制失败。可以通过调大 repl-timeout 参数来解决此问题。
2、Redis 虽然支持无盘复制,即直接通过网络发送给从节点,但功能不是很完善,生产环境慎用。
部分复制
流程图
步骤
1、当从节点出现网络中断,超过了 repl-timeout 时间,主节点就会中断复制连接。
2、主节点会将请求的数据写入到“复制积压缓冲区”,默认 1MB。
3、当从节点恢复,重新连接上主节点,从节点会将 offset 和主节点 id 发送到主节点。
4、主节点校验后,如果偏移量的数后的数据在缓冲区中,就发送 cuntinue 响应 —— 表示可以进行部分复制。
5、主节点将缓冲区的数据发送到从节点,保证主从复制进行正常状态。
部分重同步功能主要由以下三个部分构成
1、复制偏移量
2、复制积压缓冲区
3、服务器运行ID
主从节点心跳链接
说明:主从节点在建立复制后,他们之间维护着长连接并彼此发送心跳命令。
1、主从都有心跳检测机制,各自模拟成对方的客户端进行通信,通过 client list 命令查看复制相关客户端信息,
主节点的连接状态为 flags = M,从节点的连接状态是 flags = S。
主节点的连接状态为 flags = M,从节点的连接状态是 flags = S。
2、主节点默认每隔 10 秒对从节点发送 ping 命令,可修改配置 repl-ping-slave-period 控制发送频率。
3、从节点在主线程每隔 1秒发送 replconf ack{offset} 命令,给主节点上报自身当前的复制偏移量。
4、主节点收到 replconf 信息后,判断从节点超时时间,如果超过 repl-timeout 60 秒,则判断节点下线。
缺点:
复制延时
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
不满足高可用
master挂掉后,不能继续提供写服务(slave不会自动升级为master),因此主从模式,只提供了读写分离的特性,不满足高可用
哨兵模式(sentinal)
概述
哨兵节点
特殊的redis节点,不存数据
监控
sentinel会不断检测主节点和从节点是否正常
提醒
当检测某个节点时,可以通过API像管理员和其他应用发送通知
自动故障转移
当一个主服务不能正常工作时会开始一次故障处理操作
数据节点
主节点和从节点都是数据节点
定义
基于主从模式,多了哨兵的角色。
sentinal由一个或多个sentinal实例组成的sentinal系统可以监视任意多个主服务器,
以及这些主服务器属下的从服务器,
并在被监视的主服务器进入线下状态时,
自动将下线主服务器属下的从服务器升级为新的主服务器
sentinal由一个或多个sentinal实例组成的sentinal系统可以监视任意多个主服务器,
以及这些主服务器属下的从服务器,
并在被监视的主服务器进入线下状态时,
自动将下线主服务器属下的从服务器升级为新的主服务器
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,
当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
sentinel可以让redis实现主从复制,当一个集群中的master失效之后,sentinel可以选举出一个新的master用于自动接替master的工作,
集群中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。
集群中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。
部署架构图
子主题
主要功能
监控
监控master和slaver的进程是否正常
消息通知
如果某个节点处故障,发消息通知管理员
故障转移
如果master挂了,自动转移到slave节点
自动故障切换机制
1.当一个master被大多数的sentinal标记为主观下线时,这个master从主观下线标记为客观下线,failover故障机制会被触发
2.投票产生领头sentinal,每个发现master客观下线的sentinal都会要求其他sentinal将自己选为领头去执行故障迁移;投票采用先到先得策略,已经给某个候选人投过票的sentinal,不再接受其他候选人的要求投票请求
3.选举slave升级为master,再淘汰已下线,超时,响应慢的slave之后,领头sentinal先后根据节点的优先级,复制的偏移量(标记slave复制了多少master数据),进程id,选择最优的slave升级为master。将原master和slave都归为新master的slave
4.更新节点配置信息
2.投票产生领头sentinal,每个发现master客观下线的sentinal都会要求其他sentinal将自己选为领头去执行故障迁移;投票采用先到先得策略,已经给某个候选人投过票的sentinal,不再接受其他候选人的要求投票请求
3.选举slave升级为master,再淘汰已下线,超时,响应慢的slave之后,领头sentinal先后根据节点的优先级,复制的偏移量(标记slave复制了多少master数据),进程id,选择最优的slave升级为master。将原master和slave都归为新master的slave
4.更新节点配置信息
三个定时任务
1、每10秒每个sentinel会对master和slave执行info命令,这个任务达到两个目的:
发现slave节点
确认主从关系
2、每2秒每个sentinel通过master节点的channel交换信息(pub/sub)
通过_sentinel_:hello频道交互
交互对节点的“看法”和自身信息
3、每1秒每个sentinel对其他sentinel和redis节点执行ping操作(相互监控),这个其实是一个心跳检测,是失败判定的依据。
Sentinel工作方式(每个Sentinel实例都执行的定时任务)
每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他 Sentinel 实例发送一个PING命令。
如果一个实例(instance)距离最后一次有效回复PING命令的时间超过 own-after-milliseconds 选项所指定的值,
则这个实例会被Sentinel标记为主观下线。
则这个实例会被Sentinel标记为主观下线。
如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。
当有足够数量的Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态,
则Master会被标记为客观下线。
则Master会被标记为客观下线。
在一般情况下,每个Sentinel 会以每10秒一次的频率向它已知的所有Master,Slave发送 INFO 命令。
当Master被Sentinel标记为客观下线时,
Sentinel 向下线的 Master 的所有Slave发送 INFO命令的频率会从10秒一次改为每秒一次。
Sentinel 向下线的 Master 的所有Slave发送 INFO命令的频率会从10秒一次改为每秒一次。
若没有足够数量的Sentinel同意Master已经下线,Master的客观下线状态就会被移除。
若 Master重新向Sentinel 的PING命令返回有效回复,Master的主观下线状态就会被移除。
若 Master重新向Sentinel 的PING命令返回有效回复,Master的主观下线状态就会被移除。
流程分析
监控
主观下线
客观下线
故障转移过程
1、多个sentinel发现并确认master有问题。
2、选举出一个sentinel作为领导。
原因:只有一个sentinel节点完成故障转移
选举:通过sentinel is-master-down-by-addr命令都希望成为领导者
每个做主观下线的Sentinel节点向其他Sentinel节点发送命令,要求将它设置为领导者。
收到命令的Sentinel节点如果没有同意通过其他Sentinel节点发送的命令,那么将同意该请求,否则拒绝
如果该Sentinel节点发现自己的票数已经超过Sentinel集合半数且超过quorum,那么它将成为领导者。
如果此过程有多个Sentinel节点成为了领导者,那么将等待一段时间重新进行选举。
3、选出一个slave作为master。
故障转移(sentinel领导者节点完成)
1.从slave节点中选出一个“合适的"节点作为新的master节点
2.对上面的slave节点执行slaveof no one命令让其成为master节点。
3.向剩余的slave节点发送命令,让它们成为新master节点的slave节点,复制规则和parallel-syncs参数有关。
4.更新对原来master节点配置为slave,并保持着对其“关注",当其恢复后命令它去复制新的master节点。
4、通知其余slave成为新的master的slave。
5、通知客户端主从变化
6、等待老的master复活成为新master的slave。
缺点:(其实是单master的缺陷)
1.每个节点保存的数据都一样,存在数据冗余,造成内存浪费
2.因为每个节点保存的数据都一样,master的最大存储容量决定了整个集群的容量,上限有限
3.如果数据量较多,故障恢复需要较长的时间
2.因为每个节点保存的数据都一样,master的最大存储容量决定了整个集群的容量,上限有限
3.如果数据量较多,故障恢复需要较长的时间
参考链接
集群模式(cluster)
概述
基于主从模式和哨兵模式,引入分片的设计。
通过hash计算,将数据分开存储在对应节点的槽(slot)
集群是redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能
通过hash计算,将数据分开存储在对应节点的槽(slot)
集群是redis提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能
为什么使用集群
redis单机最高可以达到10万/s,如果业务需要100万/s呢?
单机器内存太小,无法满足需求
高可用,可扩展。横向扩容可支持海量数据
结构特点
所有的 Redis 节点彼此互联(PING-PONG机制),内部使用二进制协议优化传输速度和带宽。
节点的fail是通过集群中超过半数的节点检测失效时才生效。
客户端与 Redis 节点直连,不需要中间Proxy层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
Redis Cluster 把所有的物理节点映射到[0-16383] slot(哈希槽) 上(不一定是平均分配),Cluster 负责维护node <-> slot <-> value。
Redis 集群预分好 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384 的值,决定将一个 key 放到哪个桶中。
流程
集群新增节点
节点A根据命令给定的ip和端口号创建一个ClusterNode结构用于表示节点B,并向节点B发送一条meet消息
节点B接收到消息为节点A构建一个ClusterNode结构并将其添加到自身的 ClusterState.nodes字典里。这个字典就是存放当前节点视角下的集群中所有的节点,其中有一个myself指针指向当前节点
节点B向节点A发送一条PONG消息(三次握手的原理),节点A接收到了这条消息,知道节点B接收到了自己发送的MEET消息。然后节点A向节点B发送一条ping命令
节点B接收到了ping命令 知道自己发送给节点A的pong命令被接收了。至此握手完成。实现了节点A和节点B可以互相通信
最后节点A会通过Gossip协议将节点B的信息传播给集群中的其他节点。让这些节点尝试与节点B进行握手
槽指派
redis集群通过分片的方式来保存数据库中的键值对:集群的整个数据库被分为16384个槽(slot),数据库中的每一个键都属于这16384个槽的其中一个。只有这16384个槽都有节点在处理时,此集群才是上线状态。 可以使用 CLUSTER ADDSLOTS 命令为节点指定槽。 其实也就是将整个数据库划分为16384份,每个节点处理某一部分,这一部分就当作分片。其实就相当于一个大数据库由很多节点的数据库组成
记录节点的槽指派信息
通过ClusterNode结构的slots属性和numslot属性记录当前节点处理的槽。slots属性是一个二进制数组共16384位,通过下标是否为1来判断当前节点处理那些槽,numslot代表当前节点一共处理多少个槽
传播节点的槽指派信息
每个节点会将自己负责的槽告诉集群中的其他节点,这就保证了集群中的任一节点都可以知道其他节点负责的槽
记录集群所有槽的指派信息
对于集群结构 ClusterState 也有一个数组slots,他也具有16384位,但是其每一位都指向一个ClusterNode结构,表示某一个槽被那个节点处理,我们可以方便的通过此数组找到处理某个槽的节点
CLUSTER ADDSLOTS命令的实现
首先通过指定节点前往此节点的ClusterNode类查看这个节点是否已经被指派了槽,然后再去集群类ClusterState中的slots数组遍历查看当前需要指派的槽是否已经有别的节点在处理了,如果有的话则失败,否则将槽指派给指定的节点,也就是在ClusterState中slots数组设置槽的指针为指定的节点,在ClusterNode中设置其处理的节点有那些
重新分片
redis可以通过redis-trib向节点发送命令从而将一个节点拥有的槽转移给另一个节点
redis-trib对目标节点发送cluster setslot <slot> importing <sourve_id>命令,让目标节点准备好从源节点导入输入指定槽的键值对
对源节点发送 cluster setslot <slot> migrating <traget_id> 让源节点准备好将属于槽slot的键值对迁移至目标节点
向源节点发送 cluster getkeysinslot <slot> <count> 获得最多count个slot的键值对
对于步骤三获得的键,都向源节点发送一个MIGRATE <target_ip> <target_prot> <key_name> 0 <timeout> 将键原子性的从源节点迁至目标节点
重复步骤三和步骤四知道slot中的所有键值对都从目标节点迁至源节点
向集群中任一节点发送 cluster setslot <slot> node <target_id> 通知集群中所有的节点,让他们都知道这些槽分配给了新的节点
ASK错误
当在进行重新分片期间,源节点的一部分数据已经转移到了目标节点。
但是现在客户端向源节点发送命令,要进行操作的键已经转移到了目标节点。
这时源节点就会返回一个ASK错误指示客户端前往目标节点操作键。
但是现在客户端向源节点发送命令,要进行操作的键已经转移到了目标节点。
这时源节点就会返回一个ASK错误指示客户端前往目标节点操作键。
查看上面重新分片的第一步执行的命令。这个命令还会指示集群结构ClusterState中importing_slots_from属性保存当前节点正在从其他节点导入的槽
重新分片的第二个命令。会指示集群结构ClusterState的migrating_slots_to保存了当前节点正在向其他节点转移的槽。通过命令1和2 我们就会保存两个数组,一个是当前节点正在从其他节点导入那些槽,一个是当前节点正在向其他节点转移的槽
通过上面两个数组,当客户端向一个节点发送一个命令是,如果键在节点中不存在,则前往 migrating_slots_to数组查看当前键所在的槽是否正在被前移,如果槽正在被前移则前往importing_slots_from数组查看当前槽转移到的节点。然后向目标节点发送ASKING命令,之后在向目标节点发送客户端的命令
先向目标节点发送ASKING命令然后在发送客户端命令是因为,如果直接发送客户端命令会造成MOVED错误。因为我们现在还是在源节点上执行如果直接执行客户端命令会使用moved错误转向目标节点,这样就会将当前节点变为目标节点,而ASKING命令指示临时转换为目标节点,当目标节点处理完客户端命令还是会将当前节点转变为源节点
复制与故障转移
集群中的节点分为主节点和从节点,主节点负责处理槽,从节点复制主节点的槽。
设置从节点
- 向一个节点发送 cluster replicate <node_id> 可以让接收命令的节点成为node_id节点的一个从节点。接收到命令的节点会前往nodes属性里找到指定的节点然后修改自己的myself.slaveof属性指向目标节点。代表当前节点正在复制哪一个节点。然后会调用 SLAVEOF <master_ip> <master_port> 跟主从节点的复制一样
故障检测
集群中的每个节点都会定期的向集群中的其他节点发送ping消息,如果某一个节点未在规定的时间内回复这个ping消息,那么就会将其标记为疑似下线状态,如果其他节点发现某个节点为疑似下线状态,则会向一个数组中记录本节点也认为此节点疑似下线,如果超过指定数量一般为半数的节点认为某一个主节点为疑似下线状态,则将此主节点标记为下线状态,并向集群广播一条FULL消息,告诉其他节点此主节点已经下线。 有点类似于哨兵的原理,每秒向主节点发送ping消息
故障转移
从下线的主节点的从节点中。选择一个从节点当作主节点
被选中的从节点执行SLAVEOF no one命令将自己转换为主节点
新的主节点将下线的主节点的槽指派转移为自己的槽指派
新的主节点向集群中其他节点发送一条PONG消息,告诉其他节点自己成为主节点了
新的主节点开始处理命令,和指派槽的键
选举新的主节点
与选取领头哨兵有点类似,也具有配置纪元的原理,也就是在一个配置纪元中主节点只能投票一次,从节点会向当前集群中所有具有槽的主节点发送一条命令,让这些主节点推荐自己作为主节点,如果这些主节点没有投过票就会将票投给此从节点。从节点如果接受到的票数超过N/2+1 那么这个从节点就会被选举成为主节点。如果没有任何一个从节点投票数达到规定则将配置纪元+1 在执行新一轮的投票
集群新增和删除节点
新增节点
1.启动节点(主节点)
2.redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7000将节点加入集群
3.执行redis-cli --cluster reshard 127.0.0.1:7000重新分配集群slot
4.新增节点8为节点7的从节点 redis-cli --cluster add-node 127.0.0.1:7008 127.0.0.1:7000 --cluster-slave --cluster-master-id ef1bcdb677b1c8f8c3d290a9b1ce2e54f8589835
2.redis-cli --cluster add-node 127.0.0.1:7007 127.0.0.1:7000将节点加入集群
3.执行redis-cli --cluster reshard 127.0.0.1:7000重新分配集群slot
4.新增节点8为节点7的从节点 redis-cli --cluster add-node 127.0.0.1:7008 127.0.0.1:7000 --cluster-slave --cluster-master-id ef1bcdb677b1c8f8c3d290a9b1ce2e54f8589835
删除节点
1.先删除从节点再删除主节点,不然哨兵会执行故障转移
2.redis-cli --cluster del-node 127.0.0.1:7000 f24b935a50a788692479c6beaf7c556f6d082253 (7000代表的是集群) 后接删除的节点id
3.下线主节点的时候记得执行reshard,把slot分配给剩余节点
2.redis-cli --cluster del-node 127.0.0.1:7000 f24b935a50a788692479c6beaf7c556f6d082253 (7000代表的是集群) 后接删除的节点id
3.下线主节点的时候记得执行reshard,把slot分配给剩余节点
在节点变化过程中数据不会受到影响
MOVED重定向
概述
降低重定向思路
结构图
ASK 重定向
概述
结构图
数据分片方式
顺序分布
详情图
特点
可顺序访问
键值与业务相关
顺序分区的数据量不可确定性导致倾斜
不支持批量操作
哈希分布
详情图
特点
数据分散度高
如果要增加分区,数据迁移量在80%左右
数据迁移第一次是无法从数据中取到的,数据库需要进行回写到新节点
迁移数量和添加节点数量有关:建议翻倍扩容
键值与业务无关
可顺序访问
一致性哈希分布
详细图
特点
客户端分片:哈希+顺时针(优化取余)
节点伸缩:只影响邻近节点,但是还是有数据迁移
翻倍伸缩:保证最小迁移数据和负载均衡
比较适合节点多的情况
虚拟槽分布
详细图
特点
预设虚拟槽:每个槽映射一个数据子集,一般比节点数大
良好的哈希函数:例如CRC16
服务端管理节点、槽、数据:例如Redis Cluster
共享消息模式
keepalived机制(双机热备)
优点:通过VIP的漂移实现主备切换,提供不间断的读写服务。实现简单,成本低
缺点:单机吞吐量有限
0 条评论
下一页