Redis进阶知识个人汇总

持久化

三种方式实现它的持久化:

  • RDB持久化

    • 全称Redis数据备份文件,又称Redis数据快照

    • 这种就是将Redis内存中所有数据记录到磁盘中,当实例出故障后,从磁盘中读快照文件进行恢复数据。

    • 一般使用bgsave指令实现

      • 复制主线程得到一个子进程,共享同一内存空间

      • 子进程读取Redis内存数据,并写入一个新RDB文件

      • 最后,用新的RDB文件替代旧的

    • 可以配置:save 60 100

      • 代表60s内至少执行100次触发RDB

    • 缺点

      • RDB执行间隔长,两次RDB之间写有数据丢失风险

      • fork子进程、压缩、写RDB文件都耗时

  • AOF持久化

    • 全称追加文件

    • Redis处理的每一个写命令都记录在AOF文件(类似操作日志文件)

    • 配置

      • 默认关闭,修改redis.conf开启

      • 记录频率的三种方式

        • always(立刻记录)

        • everysec(每隔一秒记录)

        • no(由操作系统决定)

  • 混合持久化

    • 结合了 RDB 和 AOF 持久化的优点,开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。

    • 缺点是可读性差,兼容性差

Redis主从架构

搭建主从集群,实现读写分离

  • 全量同步:master将完整的内存数据生成RDB,发送RDB到slave后。后续命令则记录在repl_baklog,逐个发送给slave

  • 增量同步:slave提交自己的offset到master,master获取repl_backlog中从offset之后命令给slave

主从数据同步流程

  1. slave节点请求增量同步

  2. master判断replid ,如果不一致拒绝增量

  3. slave情况数据,加载master的RDB

  4. master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave

  5. slave执行接收到的命令,并与master保持同步

什么时候执行全量同步

  • slave节点第一次连接master的时候

  • slave节点断开时间太久,repl_baklog中的offset已经被覆盖的时候

Redis哨兵

哨兵是Redis提供的一种利用心跳机制监听主节点是否存活的机制。

如果主节点挂了,通过判断slave的slave-priority选举一个新的。

分片集群

集群中多个master,每个master保持不同数据。

就是将多个Redis组合成了一个大的。

Redis会把每个master节点映射到0-16383供16384个插槽上(hash slot),查看集群信息就能看到。

Redis如何判断某个key应该在哪个实例?
  • 将16384个插槽分配到不同的实例中

  • 根据key的有效部分计算hash值,对16384取余

  • 余数作为插槽,寻找插槽所在实例

多级缓存

JVM进程缓存

使用Caffeine,利用JVM的进程缓存。

Caffeine 的3中缓存淘汰策略:

  1. 基于容量

  2. 基于时间

  3. 基于对象引用。

    • 设置对象为 软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

Nginx-反向代理缓存

在服务端,可以利用反向代理(如Nginx)设置本地缓存。当请求到达Nginx时,它会首先检查本地是否有请求的数据,如果有则直接返回,避免了到达应用服务器的请求。

Canal

使用Canal实现数据一致性

Redis键值设计

一般约定:

  1. 基本格式:[业务名称]:[数据名]:[id]

  2. 长度不超过44字节

  3. 不包括特殊字符

拒绝BigKey,

  • key本身数据量过大:一个string类型的key,值为5MB

  • key中成员数过多:一个zset类型的key,成员数量为:10000个

  • key中成员数据量过大:一个hash类型的key,成员数量虽只有1000个,但这个value的总大小为100MB

推荐key值:

  • 单个key的value小于10KB

  • 对于集合类型的key,建议元素数量为小于1000

bigKey危害:

  1. 执行对BigKey读的时候,少量的QPS可能导致带宽使用占满

  2. 导致数据倾斜,bigkey所在Redis实例内存远超其他实例

  3. 可能运算耗时过久,导致主线程阻塞

批处理优化

  • Mset(处理数据类型有限)

    • mset

    • hmset

  • Pipeline(可以对复制数据类型的批处理需要)

    @Test
    void testPipeline(){创建管道
        Pipeline pipeline = jedis.pipelined();
        for(int i=1;i <= 100000; i++){
            //放入命令到管道
            pipeline.set("test:key_" + i,"value_"+i);
            if(i % 1000 == 0){
                //批量执行
                pipeline.sync();
            }
        }
    }

集群下数据处理

使用Spring提供的stringRedisTemplate就行,底层使用的是并行slot

image-20240428134902755

Redis底层数据结构

动态字符串SDS

具备自动扩容的能力。

结构:

  1. len:字符串字节数

  2. alloc:申请的总字节数

  3. flags:不同的SDS头类型,用来控制头的大小

  4. buf:具体的字符串

申请空间策略:

  1. 当新字符串小于1M,则新空间扩展后为字符串长度的两倍+1

  2. 当新字符串大于1M,则新空间为扩展后字符串长度 + 1M +1。(称为内存预分配)

SDS是一种由C语言实现的动态字符串,具有空间预分配和惰性释放空间的特点

IntSet

  • Inset是Redis中set集合的一种实现方式

  • 基于整形数组来实现,并具备长度可变、有序等特性。(适合使用于数据量不多的情况)

结构:

  1. encoding:编码方式

  2. length:元素个数

  3. contents[]:整数数组,保存集合数据

总结:

  1. Redis确保Intset元素唯一、有序

  2. 具备类型升级机制(升级编码方式到合适大小),可省内存空间

  3. 底层采用二分查询

intset是一种元素全部都是整形的set集合,具有对新增数据判断升级、底层采用二叉查找数据、保证数据有序且唯一的特点

Dict

实现了键值的映射关系

组成部分:哈希表(dictht:数组+链表)、哈希节点(DictEntry)、字典(Dict)

dictEntry结构:

  1. key:键

  2. v:值

  3. struct dictEntry *next:下一个Entry指针

dictht(dictHashTable)结构:

  1. dictEntry **table:entry类型数组

  2. size:哈希表大小

  3. sizemask:哈希表大小的掩码,总等于:size-1

  4. used:entry个数

dict结构:

  1. type:dict类型,内置不同的hash函数

  2. privdate:私有数据,做特殊hash运算使用

  3. ht[2]:包含两个哈希表,一个是当前数据,另一个是空,rehash使用

  4. rehashidx:rehash进度,-1为未进行

  5. pauserehash:rehash是否暂停,1暂停,0继续

image-20240606163826453

扩容条件

因为dict中的hashtable是由数组+单向链表实现。当元素过多的时候,会导致哈希冲突;而且链表过长,影响查询效率,所以这个时候就需要扩容了。

LoadFactor=used(所有的entry)/size(哈希表大小-1)

下面这两种情况会扩容:

  1. LoadFactor >= 1,并服务器没有执行bgsave / bgrewriteaof等后台进行时候

  2. LoadFactor > 5

每次扩容到2^n

Dict收缩

  • LoadFactor < 0.1时,进行收缩

  • 每次收缩到2^n,但是必须大于 4

rehash

  • 因为我们知道扩容还是收缩都要创建一个新的哈希表,但是这会导致size和sizemask变化。

  • 所以必须对哈希表中每个key重新计算索引,插入新的哈希表中,这个过程就叫rehash

rehash过程:

  1. 计算哈希表大小 realeSize(应分配的哈希表大小)

    1. 扩容:realeSize为 大于等于dict.ht[0].used + 1的新的2^n

    2. 收缩:同上,但是是小于等于,但不能小于4

  2. 申请内存空间并创建一个新dictht

  3. 设置dict.rehashidx=0,标志rehash的开始

  4. 重新对旧hash表中的数据进行hash求值,并复制到新hash表中

  5. 切换hash表,并释放旧哈希表中的内存空间

渐进式rehash:(因为一次性搬迁哈希表可能出现阻塞情况,所以可以采用渐进式,将一步分为多步实现)

  • 渐进式rehash相对普通的rehash而言就是,在数据迁移的时候,rehash是一次性的操作,而渐进式rehash是通过客户端请求过程中同时进行迁移操作

  • 在渐进式rehash搬迁数据的时候,如果要查询数据,是对两个hash表中进行查询

ziplist

ziplist是一种类似双端队列的设计,可以在头尾两端进行加/减元素操作,然后他存储的方式是一种连续内存的方式,当前元素记录上一个元素的长度,可以根据元素的type类型区分是字符串还是数字,然后选择是全数字的ziplist还是全字符串的。

ziplist结构:

  1. zlbytes:记录整个压缩列表占用内存字节数

  2. zltail:记录压缩列表表尾部节点距离压缩列表的起始地址有多少字节。通过这个偏移量,可以确定表尾节点的地址。

  3. zlen:记录了压缩列表包含的节点数量。

  4. entry:压缩列表包含的各个节点,节点的长度由字节保存的内容决定。

  5. zlend:特殊值OxFF(十进制 255),用于标记压缩列表的末端

entry结构:(整个entry字节数=前节点长度 + 编码 + 当前节点内容)

  1. previous_entry_length:前一节点长度,占1或5个字节

    • 如果前一节点长度小于254字节,则采用1个字节保存这个长度值

    • 大于254字节,用5个字节

  2. encoding:编码属性,记录content的数据类型(字符串/整数)以及长度。占用1/2/5个字节

  3. contents:负责保存节点的数据,可以是字符串或整数

编码:

  1. 字符串:编码是以 “00”、“01”、“10”开头

  2. 数字:“11”开始,且encoding固定占用1个字节

问题:连锁更新 => 导致内存连续被申请喝销毁

QuickList

用来解决ziplist连续空间、无法存储大量数据、数据拆分后分散不易管理的问题。

quicklist是一个双端链表,每个节点都是一个ziplist。

  • 限制ziplist的entry数量:Redis提供了一个list-max-ziplist-size的配置项,用来限制ziplist的entry过多。

    • -1:不超过4kb

    • -2:8

    • -3:16

    • -4:32

    • -5:64

  • 限制ziplist的大小:quicklist还可以通过配置list-compress-depth进行对ziplist做压缩

    • 0:特殊值,不压缩

    • 1:首尾1个节点不压缩,中间节点压缩

    • 2:首尾2个节点不压缩,中间节点压缩

    • 。。。同上类推

QuickListNode结构:

  • QuickListNode *prev:前一节点指针

  • QuickListNode *next:后一节点指针

  • char *zl:当前节点的ziplist指针

  • sz:当前节点的ziplist的字节大小

  • count:当前节点的ziplist的entry个数

  • encoding:编码(1:ziplist,2:lzf压缩模式)

  • container:数据容器类型(预留)1:其他,2:ziplist

  • ...

quicklist结构:

  • head:头节点指针

  • tail:尾节点指针

  • count:所有ziplist的entry的数量

  • len:ziplist总数量

  • fill:ziplist的entry上限

  • compress:首尾不压缩节点数量

  • ...

image-20240429212852394

总结

我的理解:

quicklist是一种用来解决ziplist连续存储空间、无法存储超大容量数据、数据拆分不易管理而提出的一种数据类型,是一个双端链表,节点上存储的是一个ziplist。

skiplist

skiplist是一种特殊的链表,具有升序排序存储、节点包含多个不同跨度的指针的特点。

zskiplistNode结构:

  • ele:节点存储的值

  • score:节点分数。用来排序、查询

  • backword:前一个节点指针

  • level[] :多级索引数组

    • forward:下一个节点指针

    • span:索引跨度

zskiplist:

  • header,tail:头尾指针

  • length:节点数量

  • level:索引层级,默认1,最多32级

image-20240429214103125

总结:

个人理解:

skiplist是一种按score值升序排序存储的双向链表,对这个链表按照全局固定的一个span值进行划分一个索引层次,可以用这个层次来加速查询

RedisObject

通过对上述的任意数据类型的键值封装成一个RedisObject形成Redis的基本数据类型。

结构:

  • type:对象类型。string、hash、list、set、zset(占4bit)

  • encoding:底层编码方式,11种(占4bit)

  • lru:

  • refcount:对象引用计数器,计数器为0时,无人引用,可被回收

  • ptr:指向实际的数据存储空间

image-20240429214646035

上述的头部已经占用了16个字节了。

这就是为什么存储大容量数据不用string类型的原因了,一个string数据就多占用16个字节

11种编码方式:

image-20240429214820332

对应着不同数据类型采用的不同编码:

image-20240429214909437

Redis的5种基本数据类型

string

采用的编码:int、embstr、raw

  • 基本编码方式是raw,基于动态字符串(SDS)实现,存储上限512MB

  • 当SDS长度小于44字节,则采用embstr编码。

  • 当存储的字符串是整数值,并在long_max范围,采用int编码

    • 直接将数据保存在RedisObject的ptr指针位置(刚好有8个字节大小),不需要SDS了

image-20240430103804424

list

采用的编码

  • 3.2之前:linkedlist + ziplist

  • 3.2之后:quicklist

image-20240430104705677

set

采用的编码:intset、ht

  • 当存储的所有数据都是整数的时候,并且元素的数量不超过set-max-intset-entries,set会采用intset这种编码节省空间

  • 其他时候采用ht编码(Dict),key用来存储元素,value统一为null(虽然这个value为null有点浪费,但是整体来说还是值得的)

image-20240430105510393

zset

采用的编码:ziplist、ht、skiplist

因为zset的特点:键值存、键必须唯一、可排序

通过skiplist + ht(Dict)实现:

  • skiplist:可排序,可存储score喝ele值(member)

  • ht(Dict):存储键值,根据key找value

typedef struct zset{
    dict *dict;
    zskiplist *zls;
}

image-20240430112627572

当元素数量不多的时候,ht+skiplist的优势不明显,而且更耗内存。这个时候会通过ziplist来节省内存。

要满足以下两个条件:

  1. 元素数量小于zset_ziplist_entries,默认128

  2. 每个元素都小于 zset_max_ziplist_value字节,默认64

ziplist本身没有键值存储、没有排序功能,这个时候要功能代码进行实现:

  • ziplist是连续内存,所以score和ele是紧凑一起的两个entry,ele前,score后

  • score越小越近队首,score越大越接近队尾,按score升序排序

image-20240430113128446

hash

采用的编码:ziplist、ht

hash底层跟zset基本一致,只需把排序有关的skiplist去掉即可:

  • hash默认采用ziplist,节省内存,相邻的两个entry分别保存field 和 value

  • 数据较大时,hash转ht编码(Dict)。触发条件:

    1. ziplist的元素数量超过了hash-max-ziplist-entries(默认512)

    2. ziplist种任意的entry大小超过了hash-max-ziplist-value(默认64字节)

image-20240430113745962

总结

  1. string

    • intset:全整数+数据小时

    • embstr:小于44字节

    • raw默认这种,采用SDS,最大512字节

  2. list

    • 3.2前:看情况使用ziplist和linkedlist(大)实现

    • 3.2后:统一使用quicklist实现

  3. set

    • intset:全数字 + 不超多一个阙值

    • dict

  4. zset

    • ziplist:数据量小时=> 头小,尾大;前ele,后score

    • skiplist(排序) + dict(唯一)

  5. hash

    • ziplist:数据量小时=> 头小,尾大;前field,后value

    • dict(唯一)

Linux网络模型

在Linux操作系统中,用户是不是没有直接的权限去操作文件的,而是通过调用系统内核的接口实现调用的。这里分为用户空间和内核空间两个概念了。

阻塞IO

  • 要读取数据,发现没有数据,就阻塞等待。

  • 在等待数据阶段和拷贝数据阶段这两个阶段都在阻塞。

image-20240430202355182

非阻塞IO

  • 通过反复轮询请求访问内核,看看是否有数据。

  • 等待阶段是非阻塞的(如果没有数据就返回一个错误代码),拷贝数据这个阶段是阻塞的

  • 非阻塞IO的关键在于减少阻塞等待的时间,使得线程或进程在等待IO操作完成的同时可以去做其他事情

image-20240430202549611

IO多路复用

在Linux系统中,开启了许多个FD,通过使用一个单线程检测各个FD是否完成,进行返回结果。

fd(文件描述符):Linux系统中任意一个资源的打开,包括socket。

实现方式

  • select模式

    1. 初始化fd_set

      1. 定义个存储fd的集合:fd_set

      2. 情况fd_set

      3. 将fd添加到fd_set中

    2. select函数的调用

      1. select函数调用,将fd_set拷贝到内核空间中

      2. 遍历fd_set,没有就绪则休眠

      3. 等待数据就绪被唤醒或超时

    3. 处理结果

      1. 检查返回值,看看有多少的fd准备好了

      2. 集合遍历,看看指定的fd是否在就绪集合中

      3. 处理就绪的fd

  • poll模式

    1. 初始化

      1. 创建pollfd数组,并添加关注的fd信息,数组大小自定义

    2. 调用poll函数

      1. 调用poll函数,将pollfd数组拷贝到内核中,转无上限链表存储

      2. 遍历fd,判断是否就绪

      3. 数据就绪/超时,拷贝pollfd数组到用户空间,返回fd就绪个数n

    3. 处理结果

      1. 用户进行判断n是否大于0

      2. 大于0,遍历poll数组,找到就绪的fd

  • epoll模式

    1. 初始化 epoll_create()

      1. 在内核空间创建一个eventpoll的结构体

      2. 结构体包括:rb_root rbr红黑树,list_head rdlist就绪列表

    2. 添加fd epoll_ctl()

      1. 添加fd添加到epoll的rbr红黑树中,并设置ep_poll_callback

      2. 当callback触发时,把对应的FD添加到rdlist就绪列表中

    3. 检查rdlist列表是否为空,不为空则直接返回就绪的FD数量 epoll_wait()

上述小结:

select模式的问题:

  • 能监听的FD不超过1024

  • 每次select都需要把要监听的fd拷贝到内核空间

  • 每次都要遍历所有的fd来判断就绪状态

poll模式的问题

  • poll利用链表解决了select监听fd上限问题,但仍需遍历所有的fd。如果监听较多,性能下降

epoll解决上述问题:

  • 基于epoll实例中,红黑树保存监听fd,理论上无上限,增删改查性能较高,性能不随着监听的fd数量增多而下降

  • 每个fd只需执行一次epoll_ctl添加到红黑树,之后epol_wait无需传递任何参数,无需重复拷贝fd到内核空间

事件通知机制

当rdlist中有数据可读时,调用epoll_wait可以得到通知,得到事务通知的模式有以下两种:

  1. levelTriggered(LT):当FD有数据可读时,会重复通知多次,知道数据处理完成(默认)

  2. EdgeTriggered(ET):当FD有数据可读时,只会通知一次,不管是数据是否完成

  • ET模式避免了LT模式可能出现的惊群现象

  • ET模式最好结合非阻塞IO读取FD数据,相对比LT复杂点,但是效率更高

惊群现象:当多个套接字(sockets)同时处于等待事务的状态,但实际上只有一个套接字会接收到数据通知时所发生的情况

Web服务中流程

image-20240502134005624

信号驱动IO

写真正的实现了非阻塞,但读时候,仍有阻塞。

用户应用与内核进行了一个绑定,如果没有数据,用户应用可以去做其他的事,但是当有数据的时候,会发送一个通知给用户应用,让他来处理事务

image-20240502134404879

异步IO

用户应用只用调用,内核会自动去做完一切,到最后通知。

但是有一个问题:如果在高并发请求下,疯狂的调用,直接会导致崩溃。所以需要进行调用限流,但是这个限流实现还是有点小麻烦的。

image-20240502134622371

总结

只有异步IO是异步的,其他都是同步。

image-20240502135014261

Redis网络模型

Redis是单线程还是多线程?

  • Redis核心业务部分(命令处理)是单线程

    • 处理客户端请求、执行命令时,Redis 主要依赖于单个线程来完成。这种设计简化了并发控制,提高了 Redis 在处理大量连接和请求时的效率。

  • 整体上Redis 是多线程

    • 在持久化过程中,执行IO操作会利用到多线程

    • 在某些高级特性如 Redis Sentinel、Redis Cluster 中,可能会使用多个线程来处理监视、故障转移等任务。

    • 在一些 Redis 模块中,可能会引入多线程以支持特定的功能或提高性能

    • 持久化操作或者部分计算密集型操作,Redis 会使用额外的线程来执行这些任务,以避免阻塞主线程。

为什么Redis要采用单线程?

  1. 简化设计和实现:单线程模型使得 Redis 的设计和实现变得相对简单,减少了并发控制和同步的复杂性。这降低了开发和维护的成本,并且使得 Redis 的代码更加清晰和易于理解。

  2. 多线程不会提高太大的性能提升:抛开持久化不说,Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟,而不是执行速度,因此多线程并不会带来巨大的性能提升。

  3. 高效利用CPU:虽然 Redis 是单线程的,但它可以利用多个 CPU 核心,并行地处理多个客户端请求。通过使用非阻塞 I/O 和事件驱动的方式,Redis 能够高效地利用 CPU 资源,并在处理高并发时表现出色。

  4. 减少上下文切换和内存开销:单线程模型减少了线程创建和销毁的开销,以及线程上下文切换的成本,从而提高了 Redis 在高并发环境下的性能和稳定性。此外,单线程模型也减少了为每个连接分配线程所需的内存开销。

image-20240502141530213

Redis单线程网络模型流程

image-20240502145202973

网络模型引入多线程

Redis处理时候速度非常快,但是在IO网络请求的速度没有那么快,容易出现网络阻塞,所以在网络读/写的时候加入多线程提高系统效率。

image-20240502145434457

Redis内存策略

过期key处理

  • Redis本身是一个key-value内存存储的数据库——redisDb。

  • key,value都保存在Dict结构中(两个Dict)

    • 一个Dict存储key-value

    • 一个记录key-TTL

结构:

  • dict:存储key-value

  • expires:存储key的TTL

image-20240502153120288

image-20240502153449030

Redis是如何知道一个key是否过期的?

  • 利用两个Dict分别记录key-value和key-ttl

  • 有两种策略发现是否过期,并将该过期key删除

    • 惰性删除:在访问一个key对象时,检查该key的存活事件,如果已过则删除

    • 周期删除:周期性抽样部分过期key,然后执行删除

      • (了解即可)SLOW:设置一个定时任务serverCron(),按照server.hz频率执行过期key清理

      • (了解即可)FAST:每个事件循环前会调用beforeSleep(),执行过期key

内存淘汰机制

内存淘汰:当Redis内存使用达到阙值的时候,Redis主动挑选部分Key删除释放更多内存。

内存淘汰策略

补充一下lru和lfu:

  • lru:最近最少使用。用当前事件减去最后一次访问时间,值越大越先淘汰

  • lfu:最少频率使用。会统计每个key的访问频率,值越小淘汰越优先

  • noeviction:不淘汰任何key,但是内存满了不允许写入新数据(默认)

  • ttl:淘汰最小TTL值

    • volatile-ttl:对设置了TTL的key,淘汰最小的TTL值

  • random:随机

    • volatile-random:对设置了TTL的key,随机进行淘汰

    • allkeys-random:对所有的key,随机进行淘汰

  • lru:最近最少使用的值(淘汰掉最近一段时间内最少被访问的数据)

    • volatile-lru:对设置了TTL的key,采用lru进行淘汰

    • allkeys-lru:对所有的key,采用lru进行淘汰

  • lfu:最少频率使用(淘汰访问频率低的)

    • volatile-lfu:对设置了TTL的key,采用lfu进行淘汰

    • allkeys-lfu:对所有的key,采用lfu进行淘汰

补充:

Redis中队TTL、LRU、LFU策略使用的时候,使用的是抽样部分淘汰

  • 18
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值