Redis知识点脑图
2024-01-12 15:55:47 0 举报
AI智能生成
Redis是一款开源的内存数据结构存储系统,支持多种数据结构如字符串、列表、集合、散列和有序集合等。它具有高性能、高并发、持久化等特点,广泛应用于缓存、消息队列、排行榜等场景。Redis采用单线程模型,通过异步非阻塞IO和事件驱动的方式实现高并发处理。同时,Redis还提供了丰富的客户端语言库,如Python、Java、Node.js等,方便开发者进行快速开发。
作者其他创作
大纲/内容
过期策略
周期删除
slow模式(默认)
- 执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms
- 执行清理耗时不超过一次执行周期的25%.默认slow模式耗时不超过25ms
- 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期
- 如果没达到时间上限(25ms)并且过期key比例大于10%,再进行一次抽样,否则结束
fast模式
- 执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms
- 执行清理耗时不超过1ms
- 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否过期
- 如果没达到时间上限(1ms)并且过期key比例大于10%,再进行一次抽样,否则结束
惰性删除
惰性删除的意思就是当我们需要用到这个key的时候redis才会去判断这个key是否已经过期
一个key的过期时间是记录在哪里的呢?
每一个db都对应一个结构体,db结构体中会有两个dict,一个dict自然就是记录所有的key-value,另一个dict记录的则是key-ttl
内存淘汰
触发时机
在每一次命令执行之前会判断,如果此时所占内存大于设置的最大内存限制以及当前没有lua脚本在执行的时候就会触发内存淘汰
淘汰策略
- noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略。
- volatile-ttl: 对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰
- allkeys-random:对全体key ,随机进行淘汰。也就是直接从db->dict中随机挑选
- volatile-random:对设置了TTL的key ,随机进行淘汰。也就是从db->expires中随机挑选。
- allkeys-lru: 对全体key,基于LRU算法进行淘汰
- volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
- allkeys-lfu: 对全体key,基于LFU算法进行淘汰
- volatile-lfu: 对设置了TTL的key,基于LFI算法进行淘汰
相关优化
键值设计
拒绝BigKey
BigKey的判定
BigKey 通常以 Key 的大小和 Key 中成员的数量来综合判定,例如:
Key 本身的数据量过大:一个 String 类型的 Key ,它的值为 5 MB
Key 中的成员数过多:一个 ZSET 类型的 Key ,它的成员数量为 10,000 个
Key 中成员的数据量过大:一个 Hash 类型的 Key ,它的成员数量虽然只有 1,000 个但这些成员的 Value(值)总大小为 100 MB
Key 本身的数据量过大:一个 String 类型的 Key ,它的值为 5 MB
Key 中的成员数过多:一个 ZSET 类型的 Key ,它的成员数量为 10,000 个
Key 中成员的数据量过大:一个 Hash 类型的 Key ,它的成员数量虽然只有 1,000 个但这些成员的 Value(值)总大小为 100 MB
如何发现BigKey
利用 redis-cli 提供的–bigkeys 参数(redis-cli --bigkeys),可以遍历分析所有 key,并返回 Key 的整体统计信息与每个数据类型的 Top1 的 big key
自己通过代码调用scan命令扫描出所有的key,然后再利用 strlen、hlen 等命令判断 key 的长度
利用第三方工具,如 Redis-Rdb-Tools 分析 RDB 快照文件,全面分析内存使用情况
服务器端优化
批处理优化
单机
使用原生的M操作,比如mset
使用pipeline进行批处理
集群
为什么集群不支持批处理?
由于redis集群是对每个key分配的slot去选择不用的redis节点,所以批处理的key有可能分配的slot不是在同一个redis节点上,也就是说一次连接并不能处理这批key,而批处理的初衷就是为了减少连接次数,所以最终会报错
解决方案
串行命令
遍历每一个命令去执行
耗时最长,不建议使用
串行slot
在客户端计算不同key的slot,把相同slot的key分为一组,串行执行每一组key的批处理
耗时比较快,slot越多耗时越多
并行slot
在客户端计算不同key的slot,把相同slot的key分为一组,并行执行每一组key的批处理
耗时最快,实现有点复杂
hash_tag
将所有key设置相同的hash_tag,这样的话所有key的slot就一定相同了
耗时快,但是会出现数据倾斜
6.0新特性
线程模型
客户端缓存
acl
底层原理
底层数据结构
动态字符串SDS
作用
redis中key都是字符串,value也是字符串或者是字符串的集合,所以字符串在redis中随处可见,所以这是专门为了redis中存储字符串的时候使用的
为何不直接使用c语言的字符串?
c语言的字符串获取长度的时间复杂度是o(n),而sds直接o(1)的时间复杂度就可以获取到字符串长度
c语言的字符串不是二进制安全的,因为c语言的字符串是根据/0去判断字符串是否已经结束,如果字符串中本身就有/0那么就不能获取到正确的字符串长度了
结构
redis中提供了5种不同类型的sds,区别在于每一种sds所能存储的字符串长度是不同的,比如说flags为1的sds,最大只能够存储2的8次方-1个字符
动态扩容
当追加字符串的时候如果字符数组长度不够,会去动态申请足够的内存空间
如果新字符串小于1M,则新空间为扩展后长度的两倍+1
如果新字符串大于1M,则新空间为扩展后长度+1M+1
优点
sds可以在o(1)的时间复杂度中去获取字符串长度
二进制安全,可以存任意的二进制数据
支持动态扩容
IntSet
IntSet是redis中set集合的一种实现方式,基于整数数组来实现,并且具备长度可变,有序的特征
结构
contents数组存放的就是IntSet元素,而每一个元素支持的大小则是由encoding去控制的
为什么每一个元素都要严格按照encoding去控制内存大小?
因为只有每一个元素都按照指定的encoding去控制大小,IntSet才可以根据数组下标去计算出元素的起始地址,如上图所示
如果指定的encoding分配的内存不足以存放元素本身怎么办?
需要注意的是倒序对每个元素进行扩容,如果正序的话在升级的过程中则会覆盖掉后面的元素
Dict
作用
redis是一个键值型的数据库,所以Dict就是用来存储我们的kv键值对的,它类似于java中的HashTable的结构
哈希冲突怎么解决?
哈希表中的哈希桶肯定是比String类型的kv键值对要少很多,所以这就不可避免地会出现哈希冲突的情况,当出现哈希冲突的时候,redis会使用链表组织这些冲突的entry,每个entry里面都有个next指针指向了下一个冲突的entry,类似于HashMap结构
rehash
当一个哈希桶中的冲突entry越来越多的时候,此时这个桶的链表就会越来越长,那么如果要查找的key在链表的尾部,时间复杂度就是O(n),所以此时redis就会进行一个rehash的操作,而为了进行rehash操作,redis默认使用了两个全局哈希表
给哈希表2分配更大的空间,比如是哈希表1的两倍
把哈希表1的数据重新映射并且拷贝到哈希表2中
释放哈希表1的数据
结构
dictEntry中的key和val都是指向RedisObject的指针
ZipList
定义
ZipList是一种特殊的“双端链表”,由一系列特殊编码的连续内存块组成。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为O(1)
解决目的
针对传统的链表来说,传统的链表需要大量的指针(消耗内存)并且每一个节点都是一个独立的内存空间,这样就会产生内存碎片化的问题,所以ZipList就可以解决上述传统链表的这两个问题,它使用了一段连续的内存空间去存储字符串和数字,这样就可以解决掉内存碎片化的问题,并且数据之间不需要使用指针进行关联
结构
zlbytes,占4个字节,记录整个压缩列表所占用的内存字节数
zltail,占4个字节,记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址,从而可以从表尾开始插入删除节点
zllen,占2个字节,记录了压缩列表包含的节点数量,最大值为65534,如果超过这个值,此处会记录为65535,此时就需要遍历整个压缩列表才能知道节点的数量
entry,占用的长度不确定,节点的长度由节点保存的内存决定
zlend,占1个字节,该字节只存特殊值0xFF(十进制255),表示标记压缩列表的末端
zltail,占4个字节,记录压缩列表表尾节点距离压缩列表的起始地址有多少字节,通过这个偏移量,可以确定表尾节点的地址,从而可以从表尾开始插入删除节点
zllen,占2个字节,记录了压缩列表包含的节点数量,最大值为65534,如果超过这个值,此处会记录为65535,此时就需要遍历整个压缩列表才能知道节点的数量
entry,占用的长度不确定,节点的长度由节点保存的内存决定
zlend,占1个字节,该字节只存特殊值0xFF(十进制255),表示标记压缩列表的末端
ZipListEntry的结构
由于ZipList中entry不像普通链表那样通过指针进行前后关联(因为指针占用8个字节,前后指针就要占用16个字节,浪费内存),所以entry的结构就设计如上图所示:
(1)previous_entry_length,前一节点的长度,占用1个或者5个字节
(3)contents:负责保存节点的数据,可以是字符串或整数
(1)previous_entry_length,前一节点的长度,占用1个或者5个字节
- 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
- 如果前一节点的长度大于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据
(3)contents:负责保存节点的数据,可以是字符串或整数
ZipListEntry的encoding编码
ZipListEntry中的encoding编码分为字符串和整数两种
字符串
整数
对于比较小的整数,如果也用1个字节去存储的话就会显得有点内存的浪费了,所以此时会把保存的整数直接存储到encoding中(如上图的最后一种情况)
特点
QuitList
解决目的
ZipList虽然可以节省内存,但是申请的内存必须是要连续的,所以如果当前内存占用比较大,那么申请内存的效率就会比较低,所以我们要解决这个问题的话可以直接限制ZipList的大小,但是如果我们就是要去存储大数据量的呢?那么QuitList就出现了,它是一个节点为ZipList的双端链表
结构
特点
节点采用了ZipList,解决了传统链表内存占用大的问题
控制了ZipList的大小,解决了连续内存空间申请效率的问题
中间节点可以压缩,进一步节省了内存
SkipList
解决目的
ZipList以及QuitList的缺点在于它们都需要从头到尾或者从尾到头去进行遍历才能找到对应的数据,这样就会使得查询效率比较低了,而SkipList可以通过建立多级索引然后使用二分查找法去提高查询效率(空间换时间)
结构
特点
RedisObject
结构
如果我们都使用string类型去保存数据,那么每一对key-value都会产生两个redis对象,同样的如果使用集合的数据结构,那么就只会有两个redis对象,相对就节省了很多内存空间
作用
redis中的任意键和值都会被封装成一个RedisObject,也叫Redis对象
五种数据结构的编码类型
String
raw
string的基本编码类型,基于SDS实现,存储上限为512mb,此时RedisObject的pr指针指向了SDS,也就是说RedisObject和SDS是两个独立的内存空间
embstr
如果存储的SDS字符串大小小于44个字节,那么则会采用embstr编码,此时RedisObject与SDS则是一段连续的内存空间,只需要调用一次内存分配函数即可,可以提高内存分配的效率
int
如果存储的字符串是整数值,并且大小是在LONG_MAX范围内,那么就会采用int编码,此时就会将数值直接存储在pr指针(8个字节),而不需要使用SDS了
List
在redis3.2之前是在数据量小的情况下使用ZipList,数据量大的情况下使用LinkedList(普通链表),在redis3.2之后就把ZipList和LinkedList结合起来了,也就是QuickList
Set
Dict,Dict中的key存储元素值,value统一为null
当存储的所有数据都是整数的时候,并且元素数量不超过set-max-intset-entries,Set就会采用intset编码,以节省内存
ZSet
Dict+SkipList
使用Dict进行元素唯一性判断,使用SkipList进行元素分数排序,并且SkipList可以进行键值对的存储(分数+元素)
优点是无论Dict还是SkipList两者的查询性能都很高,缺点也很明显,就是消耗内存空间太大了,所以ZSet在元素少的情况下可以使用ZipList的编码方式
ZipList
满足条件
元素数量少于zset_max_ziplist_entries,默认值是128
每个元素的大小不超过zset_max_ziplist_value,默认值是64
ziplist本身没有排序功能,也没有键值对的概念,所以要达到zset的功能,需要额外进行编码干预实现:
1.ziplist是一段连续的内存,所以在添加元素的时候强制把score和element紧挨在一起,element在前,score在后
2.score值越小越接近队首,score值越大越接近队尾,按照score值升序排列
1.ziplist是一段连续的内存,所以在添加元素的时候强制把score和element紧挨在一起,element在前,score在后
2.score值越小越接近队首,score值越大越接近队尾,按照score值升序排列
Hash
ZipList
与ZSet类似,Hash也是一对对键值对存储的,所以同样的也可以使用ZipList进行存储,key和value分别作为一个entry存储在ZipList中即可,但是Hash并不需要进行排序,所以Hash并不需要SkipList编码,而且ZipList中的元素也并不需要进行排序
Dict
当数据量比较大的时候,会把ZipList编码转换成Dict编码,触发条件有两个
ZipList中的元素数量超过hash_max_ziplist_entries,默认值512
ZipList中的任意元素大小超过hash_max_ziplist_value,默认值是64字节
网络IO模型
在redis6.0之后,在命令解析以及命令回复处理器写出数据这两块中使用了多线程进行处理,进一步提高了redis的性能
持久化
AOF
开启AOF命令
appendonly yes
当redis写入数据时,会把这条命令以一定的格式存储在一个aof文件中,由于写aof是在redis主线程做的,所以这时会造成线程阻塞
3种写回策略
Always
同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
性能消耗较大,但是能保证数据基本不会丢失
Everysec
每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲
区,每隔一秒把缓冲区中的内容写入磁盘;
区,每隔一秒把缓冲区中的内容写入磁盘;
性能消耗较小,最多丢失1s的数据
No
操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓
冲区,由操作系统决定何时将缓冲区内容写回磁盘。
冲区,由操作系统决定何时将缓冲区内容写回磁盘。
性能消耗最小,但是写磁盘的主动权交到了操作系统手上,宕机的时候容易丢失较多数据
重写机制
随着数据的增多,aof文件也会越来越大,这样就会造成一个很影响性能的问题,就是在恢复数据的时候会非常缓慢,所以重写机制就是重写整个aof文件使得aof文件变得更小
如何重写?
比如当有一对kv键值对被多次修改形成了多条更新命令之后,在重写aof文件之后,这对kv键值对对应的命令就是只取当前内存中最新的值生成最新的写入命令,这样在恢复数据的时候就只取按照最新的命令去执行就可以了
aof重写的时候会阻塞主线程吗?
答案是不会的,因为redis会开启一个子进程去处理aof的重写操作,首先redis会拷贝一份当前内存最新的数据(这里的拷贝不会拷贝物理内存,而是拷贝父进程内存的虚实映射,也就是能共享父进程的所有数据),然后子进程会根据这份拷贝的内存数据去生成最新的写入操作命令,然后再写入到aof的重写文件中。如果此时有写请求过来了,那么也会写入到原本的aof缓冲区中并且会写入到原来的aof文件,同时也会写入新的aof缓冲区,等到新的aof文件重写成功了,再把新的aof缓冲区的数据追加到新aof文件中,最后就可以替代掉原来旧的aof文件了
RDB
对某一个时刻内存中的所有数据生成快照文件(二进制文件,可以直接导入到内存中)
生成RDB文件的命令
save
执行该命令生成RDB二进制文件,在redis的主进程中执行,执行期间redis服务器不会接受任何请求
bgsave
执行该命令生成RDB二进制文件,父进程会fork出一个子进程去执行该过程,在fork子进程的时候会短暂地阻塞redis主进程
快照时数据能否修改
对于快照操作来说,它希望能够得到该时刻所有数据状态的快照版本,所以如果在T时刻执行了快照命令,如果要避免数据被改变的话就需要在此期间暂停写操作了,但是redis肯定不会这么干,redis是怎么做的呢,它利用了操作系统的copy on write(写时复制)技术,在这期间如果有修改操作去修改一块数据的话就拷贝一份这块数据的副本,然后快照的时候就去写入这块副本数据,然后修改操作继续修改原数据
AOF和RDB混合持久化
使用bgsave命令fork子进程的时候会短暂地阻塞父进程,所以,如果频繁地去生成快照的话肯定会对redis性能造成影响,所以redis4.0新特性就有一个AOF与RDB混合使用,当第一次生成了RDB文件之后,之后对数据的写操作都会写入aof文件,然后到第二次生成RDB文件的时候再把aof文件清空,也就是说aof文件只会写入两次生成RDB文件之间的增量数据,这样,在恢复数据的时候就可以根据RDB文件以及aof文件去恢复数据了,既利用了RDB文件快速恢复数据的好处,也能享受了aof只记录简单操作命令的优势
数据恢复
停止redis进程,修改appendonly no,然后拷贝一份RDB冷备到redis对应目录下,然后重启redis,此时redis就会基于这个RDB恢复数据。数据恢复完成后,直接在命令行热修改redis的appendonly yes,在redis-cli里使用config set appendonly yes(热修改只针对本次redis,redis重启后修改的配置就失效了),此时redis就会将内存中的数据全部写入AOF日志文件中,这样AOF就跟RBD保持一致了,然后再停掉redis进程,修改redis.conf :appendonly yes,然后重启redis,这样就完成了在RDB ,AOF双开的情况下,基于RDB冷备完成了数据恢复。
数据同步
主从同步
读写分离,读操作主库从库都可以接收,写操作只能在主库接收
主从间如何进行第一次数据同步
第一阶段:从库向主库发送psync指令,其中包括主库runID以及复制进度offset,第一次发送的话是psync ? -1,因为第一次同步从库不知道主库的runID,所以就用?表示,offset=-1表示第一次复制。主库收到从库发送过来的psync指令之后,就会响应FULLRESYNC给从库,表示将要全量复制
第二阶段:主库会使用bgsave命令开启子进程生成RDB快照文件,然后把这个RDB文件发送给从库,从库接收到主库发送过来的RDB文件之后,会把本地内存的数据全部清空,然后完成RDB文件的数据加载
第三阶段:主库在发送RDB文件给从库的时候,主库并不会阻塞,依然可以接收服务请求,所以此时可能会有写请求过来,那么主库会把这些写命令操作放到replication buffer中,在主库完成RDB文件的发送之后会继续把这些写命令操作发送给从库,以达成数据的一致性
主从级联模式分担全量复制时的主库压力
如果此时有很多从库都要和主库去进行数据同步的话,那么根据上面主从间数据同步的原理过程可以知道,数据同步的时候主库的性能消耗最大的有两处,一是fork子进程生成RDB文件,二是发送RDB文件给从库,因为fork子进程会阻塞父进程,发送RDB会占用宽带资源,所以我们可以采用“主-从-从”模式,也就是说选出一个从库然后让某几个从库与这个选中的从库形成主从关系,这样就话就相当于减少了原来主库下的从库数量,就能减轻主库的压力了
主从库间网络断了怎么办?
redis2.8之前当主从重新连接之后,会再做一次全量复制,但是2.8之后就采用了增量复制
当主从之间的网络断开之后,主库接收到的写请求除了会写到replication buffer中,还会写到一个repl_backlog_buffer缓冲区中,主库会记录自己写到的位置,从库则会记录自己已经读到的位置,当主从之间网络恢复连接时,从库会发送psync命令,把自己当前的slave_repl_offset发送给主库,然后主库会去对比slave_repl_offset与master_repl_offset,拿到它们之间相差的操作然后主库再把这些操作发送给从库
这里有个需要注意的点是replication buffer是一个环形的缓冲区,如果它满了主库的写操作会覆盖掉之前写入的数据,所以主库的这个缓冲区的大小设置是需要去研究的,可以调整repl_backlog_size这个参数去改变缓冲区的大小,如果设置得过小的话可能会导致主从库之间数据不一致
架构搭建
哨兵机制
redis的哨兵机制解决的主要就是当主库挂了如何对外继续提供服务,所谓的哨兵其实是一个运行在特殊模式的redis进程,它在运行的时候主要有三个任务,分别是监控,选主,通知
监控
哨兵在运行的时候,会周期性地给所有的主从节点发送ping命令,如果在规定时间没有相应的主库或从库,哨兵就会认为其已经下线了,这里特别是主库,如果主库没有响应就会进入下一个“选主”的任务,但是判断主库是否下线的话并没有那么容易,因为很有可能哨兵会产生误判的情况,比如由于网络拥堵等其他问题导致主库没有及时响应给哨兵,而一旦产生了误判,就会去重新选主,首先哨兵重新选主就会花费时间,之后新主库又要与从库进行数据同步,所以为了避免误判带来的不必要的性能消耗,通常都会使用由多个哨兵实例组成的哨兵集群去判断主库是否下线了。如何判断?很简单,少数服从多数,一个哨兵实例认为这个主库下线的话就会判断为“主观下线”,如果超过一半的实例都认为这个主库是“主观下线”,那么这个主库就会被标记为“客观下线”,这时主库就会真正地被认为是已经下线了
选主
在监控到当前主库已经下线的时候,这时去选主就要去从库里面筛选了,首先把一些已经下线的从库去掉,还有把一些网络总是断连的去掉,具体怎么判断哪些网络总是断连的呢?可以使用配置项 down-after-milliseconds * 10,down-after-milliseconds意思是如果超过这个配置的时长就会认为主从库断连了,而超过10的话就说明这个从库的网络状况不好,不适合选做主库。筛选之后的从库还要根据一定的打分,这个打分规则分别是从库优先级、从库复制进度以及从库 ID 号,第一个从库优先级可以通过设置slave-priority,比如说如果两个从库的内存不一样,你想希望内存大的能够被选上主库,那么这个配置就设置最高,第二个从库复制进度就是说判断下哪个从库的复制进度最接近主库的,选最接近的从库做主库,第三个每个从库都有一个自己的ID编号,在优先级和复制进度都相同的情况下,ID 号最小的从库得分最高,会被选为新主库。
通知
在选出了新主库之后,哨兵就要去通知所有的从库说这个就是新主库,并且从库会向新主库发送replicatof命令成为该主库的从库,然后哨兵还要去发送新主库的连接信息给客户端
哨兵集群
哨兵之间如何互相获取ip进行通信?
在配置哨兵的时候,我们只需要在配置文件中配置下主库的ip+端口就可以了,并没有配置其他哨兵节点的ip,那么哨兵之间是怎么去进行通信的呢?答案就是基于redis的pub/sub机制(发布订阅机制,类似于观察者模式)去实现的,当一个哨兵与主库进行了连接之后,那么它就可以发布自己的连接信息,当然自身也可以在主库上订阅信息去获取到其他哨兵节点的信息
哨兵如何获取从库的ip地址?
既然哨兵能与主库进行通信,那么它可以向主库发送info命令,主库收到这个命令之后会把从库的ip列表返回给哨兵
如何确定由哪个哨兵发起主从切换?
当一个哨兵认定为这个从库是“主观下线”的时候,它就会向其他哨兵发送是否赞成的投票,其他哨兵会根据自己与该从库的连接情况响应“Y”或“N”,如果获得的票数大于或等于quorum这个配置值,那么这个从库就会被认定为“客观下线”,此时这个哨兵就能够再次发出投票请求,表示希望自己能够成为整个哨兵集群的Leader,只有成为了Leader才能有发起主从切换的权力,如何成为Leader呢?要满足两个条件:第一,拿到半数以上的赞成票;第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
为什么哨兵集群最少需要三个节点,而且推荐节点的个数是奇数?
如果哨兵集群只有 2 个实例,此时,一个哨兵要想成为 Leader,必须获得2票,而不是1票。所以,如果有个哨兵挂掉了,那么,此时的集群是无法进行主从库切换的。
奇数个哨兵节点可以在满足选举该条件的基础上节省一个节点,比如三个master节点和四个master节点的集群相比,大家如果都挂了一个master节点都能选举新master节点,如果都挂了两个master节点都没法选举新master节点了,所以奇数的master节点更多的是从节省机器资源角度出发说的。
脑裂问题
产生的原因以及造成的影响
比如当前有3台redis节点,一台是master,另外两台是slave,如果此时master节点所在的网络分区与slave节点和sentinel所在的网络分区之间出现了网络异常,那么sentinel就会从两台slave节点中选出一台作为新的master节点,在这期间原先的master节点还是会接收客户端发送过来的请求,但是新的master节点与另一台slave节点却不能进行数据的同步。当网络正常之后,sentinel就会告诉客户端新的master节点,然后原先的master节点就会变成salve节点与新的master节点进行数据同步,这个时候因为要保持主从之间的数据一致,所以原先的master节点就会丢失掉网络异常期间客户端写入的数据
解决方案
redis提供了两个配置参数
min-slaves-to-write 1
要求master至少有一个slave
min-slaves-max-lag 10
主从数据同步超时时间,10秒
切片集群
当我们要存储的数据量日渐增多的时候,这时机器内存不够用了,而且当内存数据过多的时候,redis的持久化机制比如生成RDB要fork子进程的时候阻塞的时间就会变长,为了避免这些情况,这时我们通常有两种方案,一是垂直扩展,二是水平扩展
垂直扩展
垂直扩展比较简单且粗暴,就是将机器的内存等配置加强,但是这种方式上限较高,很容易受硬件的容量成本所限制,并且会造成内存过大使得redis持久化性能下降
水平扩展
水平扩展就是我们所说的切片集群,采用多台机器去存储数据,每一台都存储一部分数据
集群原理
redis采用了哈希槽来处理数据和实例之间的映射关系,整个集群一共有16384个槽位,每一个节点实例都占据16384/N个槽位,当一个key过来的时候客户端会对它进行一次CRC16算法进行hash得到一个值,然后再通过这个值去对16384进行取模,得到就是槽位值,再把这个槽位值去对比下是属于集群中的哪个redis节点实例,定位到具体的实例之后进行发送命令
客户端如何定位数据?
客户端在发送命令的时候首先自己去对一个key进行CRC16的hash运算然后对16384进行取模得到槽位值,根据这个槽位值就可以定位到某一个实例,但是客户端是怎么拿到槽位值与实例的映射关系表的呢?答案就是客户端与集群的任意实例建立连接之后,这个实例就会把槽位的分配信息发送给客户端,客户端再对其进行保存,但是这里就又有一个问题了,每个实例只知道自己所对应的槽位值是多少,它是怎么知道整个集群的槽位值分配信息的呢?原因就在于集群之间的实例会互相进行通信,这样集群中的每个实例都会有整个集群的槽位分配信息了
客户端重定向机制
对于整个redis集群来说,槽位的分配信息不会是一成不变的,主要是可能会有实例的新增和删除,当有实例新增或者删除的时候,实例间的数据会进行重新的调整,这样为了整个集群的负载均衡,redis集群会重新计算整个集群的槽位分配信息,并且通过实例之间的通信各自得到最新的槽位分配信息,但是对于客户端来说它是不知道这个槽位分配信息的改变的,所以此时客户端所拥有的还是一个旧的槽位分配信息,当客户端去根据这个旧的槽位分配信息去定位到某一个实例的时候,发现这个实例的并没有这个数据(比如本来在实例1的时候但是由于集群有实例的新增,所以这个数据重新分配到了实例2),然后这个实例会发送一个MOVED命令给客户端,这个MOVED命令就包含了当前要找的槽位在哪个实例中,然后把这个实例地址返回给客户端,然后客户端就能够直接与这个返回的实例进行连接并发送请求了
如果在重定向的过程中,实例间的数据还在迁移,比如slot1中有key1,key2,key3,key4,现在槽位需要重新分配了,slot1中的数据要从实例1迁移到实例2,其中key1和key2的数据已经从实例1移到了实例2,key3和key4还在实例1,那么当客户端发送key2的请求到实例1中时(客户端还拿着旧的分配信息),实例1会发送一个ASK命令给客户端,表明当前正在数据迁移中,并且还带上了key2所在的实例地址,然后客户端拿到key2的实例地址后需要发送一个ASKING命令给实例2,然后再发送操作命令,这里ASK命令与MOVED命令不同的是,ASK命令是并不会更新客户端的本地槽位分配信息的
实际应用
基本数据类型
string
hash
list
set
sorted set
扩展数据类型
Bitmap
可以用在签到打卡的场景
HyperLoglLog
可以用在UV统计上,优势是使用极少的内存去存储大量数据,但是统计的数据会有点丢失
GEO
计算地理坐标的场景,比如附近的商家等
应用场景
聚合统计
定义
统计多个集合的交集,并集和差集
场景
统计手机 App 每天的新增用户数和第二天的留存用户数,利用set结构能够支持交集,并集与差集计算的特点,以时间为key,value为当天App注册的所有用户,当天与前天的差集计算可以算出每天的新增用户,当天与前天的交集计算可以算出两天内的留存用户,但是set的交集,并集与差集的计算复杂化比较高,容易造成redis阻塞,所以推荐从主从集群中选用一台从库进行运算或者是直接把数据读到客户端让客户端进行运算
排序统计
利用sorted set集合能够给元素赋予权重的特点进行排序,通常用于在面对需要展示最新列表、排行榜等场景时
二值状态统计
定义
指的是集合元素的取值只有0和1两种,比如签到打卡,用0代表签到带卡,1代表没有签到打卡
场景
利用redis的bitmap数据结构,redis的bitmap结构其实就是一个bit数组,如果我们现在有一个需求,需要统计1亿个用户在10天内的签到情况,那么我们以时间为key,value是1亿个用户的签到状态集合,每一个用户的签到状态用0或1表示然后放到这个bit数组中,最后对这10个bit数组进行与操作就可以知道连续签到10天的用户有多少了,而且使用bitmap结构能够节省空间,所以关于统计二值状态的需求可以尝试去使用bitmap实现
基数统计
利用set的默认去重功能统计基数
收藏
0 条评论
下一页