Redis一文全总结

Redis测试和调试

Redis是一个开源(BSD许可)的内存中的数据结构存储系统,常用作数据库、缓存和消息中间件。代码简洁,实现高效,值得学习和总结,其特点包括以下特点:

  1. 性能高
  2. 丰富的数据类型
  3. 支持事务
  4. 内建replication及集群
  5. 支持持久化
  6. 单线程,原子性操作

可通过自带的性能测试工具Redis-benchmark进行测试,命令如下:

redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]

若要源码调试,需要:

  1. 修改配置文件中的daemon为yes
  2. 禁用gcc编译优化,将makefile文件OPTIMIZATION?=-O2修为-O0
  3. gdb redis-server [conf]进行调试

数据结构

简单动态字符串

redis自定义简单动态字符串sds,其数据结构如下:

struct __attribute__ ((__packed__)) 
	sdshdr8 { 
	uint8_t len; /* 数据长度 */ 
	uint8_t alloc; /* 去掉头和null结束符,有效长度+数据长度*/ 
	unsigned char flags; /* 3 lsb of type, 5 unused bits,小端*/ 
	char buf[]; 
};

在这里插入图片描述使用自定义sds的优点及相对C字符串的改进:

  • O(1)复杂度获取字符长(C语言字符串是变O(n))
  • 避免缓冲区溢出(C语言不支持扩容且易溢出)
  • 减少字符串修改带来的内存频繁重分配次数
  • 二进制操作安全,可处理任意格式二进制数据(C不支持二进制数据)
  • 以’\0’结尾,使其兼容部分C字符串函数

为减少字符串修改带来的内存频繁重分配次数,包括:

  • 空间扩容(sdsMakeRoomFor):
    • 当前有效长度>=新增长度,直接返回
    • 新增后长度<预分配的长度(1024*1024),扩大一倍
    • 新增后长度>=预分配的长度,每次加预分配长度
    • 更新后判断新旧类型是否一致,一致用remalloc否则malloc+free
  • 空间缩容(sdsRemoveFreeSpace):执行trim时,采用惰性空间释放即:不会立即回收空间,只是进行移动和标记,并修改数据长度。真正的删除被放在后续操作中如tryObjectEncoding

链表

底层是双向链表,节点结构体和链表结构体如下
在这里插入图片描述
在这里插入图片描述
内存结构如下:
在这里插入图片描述
常用API函数如下:

  • lpush:向链表左侧压入元素
  • rpush:向链表右侧压入元素
  • lpop:弹出链表左侧第一个元素
  • rpop:弹出链表右侧第一个元素
  • llen:获得链表长度
  • lrange:按索引范围获得值

哈希表

C没有哈希表故自定义实现,使用拉链法解决哈希冲突,主要结构体包括:dictht(哈希表)、dictEntry(表项,也就是键值对)、dict(给外层调用的哈希表结构,包含两dictht方便rehash),具体如图:
在这里插入图片描述
dictht中的size每次都是<=2^s来进行分配,sizemask用于计算当前的key位于哪个dictEntry,通过hash & dict->ht[x].sizemask计算,见下面siphash算法
在这里插入图片描述
在这里插入图片描述
dict中的rehashindex表示是否在进行rehash,默认-1表示未进行,否则表示当前dt[0]中tb的某个键值对entry在rehash,dictht在第一次初始化时只会启用ht[0],ht[1]在整个dict扮演着临时存储的作用

siphash算法

默认的hash算法是siphash算法,计算哈希值和索引值:

  1. 使用字典设置的哈希函数,计算键 key 的哈希值 hash = dict->type->hashFunction(key)
  2. 使用哈希表的sizemask属性和第一步得到的哈希值,计算索引值 index = hash & dict->ht[x].sizemask
    但hash冲突时,使用拉链法,如下图:
    在这里插入图片描述

负载因子和rehash

负载因子用来评估键冲突的概率,其中redis中负载因子会根据是否在进行持久化而不同:

  • 非持久化:factor=d->ht[0].used/d->ht[0].size>=1
  • 持久化: factor=d->ht[0].used/d->ht[0].size>dict_force_resize_ratio(5)
    由于空间扩大或缩小,之前的键在老表的存储位置,在新表中就不一定,需重新计算并把老元素迁移到新表元素的过程叫rehash,触发如下图:
    在这里插入图片描述
    由于rehash过程比较消耗性能,因此需优化:
  • 一:渐进式rehash指rehash不是一次性完成。因为若Hash表key太多,导致rehash操作需长时间进行,阻塞服务器,故将rehash操作分散在后续的每次增删改查中(以桶为单位)
  • 二:第一类存在问题:若服务器长时间空闲,哈希表长期用0和1两个表。为解决该问题,在serverCron函数中,每次拿出1ms时间执行Rehash操作,每次步长为100

字典迭代器

字典迭代器dictIterator如图:
在这里插入图片描述
字典迭代器包括两种模式:

  • 安全模式:支持边遍历边修改(字典key的过期判断),支持字典的添加、删除、查找等操作,但是不支持rehash操作(避免重复遍历)
  • 非安全模式:只支持只读操作,使用字典的删除、添加、查找等方法会造成不可预期的问题,如重复遍历元素或者漏掉元素,但支持rehash操作

总之:避免元素的重复遍历或遍历过程中需处理元素,必须用安全模式,如bgaofwrite、bgsave、keys。允许遍历过程出现个别元素重复,用非安全模式

使用场景

  1. 原生字符串类型:简单直观,但内存占用量大,信息内聚性差,故实践中很少用
  2. 序列化字符串类型:将信息序列化后用一个键保存,简化编程,可提高内存效率,但每次更新都会反序列和序列化,有性能消耗
  3. .哈希类型:每个信息用一对field-value,但是只用一个键保存。简单直观,但在ziplist和hashtable中会消耗更多内存

跳表

本质是并联的有序链表,加速链表查找,相比平衡树易于实现,且效率更高,结构体如下:
在这里插入图片描述

在这里插入图片描述
跳表插入、查找、删除:
在这里插入图片描述
在这里插入图片描述
根据跳表的示意图可知,增、删、查效率跟level有关,level计算公式如下:
在这里插入图片描述
由此可知:level每增加一层概率为p,redis中p为0.25,因此level=1,概率为1-p,level=2,概率为p(1-p),节点平均层为1(1-p)+2(1-p)p+3pp(1-p)+…=1/(1-p)=4/3

集合:

包含整数集合和普通集合,整数集合如图:
在这里插入图片描述
encoding表元素类型,取值int64、int32、int16,默认为int16。length表元素的个数,content用动态数组按需扩容或缩容分配空间,并按从小到大顺序保存元素,因此有序查找用二分查找

插入与升级

当插入的元素类型大于当前intset类型时,为防止溢出会进行升级操作,注意不支持降级
在这里插入图片描述
示例如图,插入65535=1111 1111 1111 1111,共16位
在这里插入图片描述
升级优点:

  1. 提升灵活性,可通过自动升级底层数组适应任意类型新元素,不必担心类型错误
  2. 节约内存避免浪费,不同类型用不同类型的空间存储

压缩列表

压缩列表是由一系列特殊编码的连续内存块组成的顺序型数据结构,可包含任意多个节点,适合存小对象和长度短的数据,核心结构体如下:

在这里插入图片描述
zlbytes记录压缩列表占用字节数,zltail记录压缩列表表尾节点离压缩列表起始地址多少字节,通过该偏移量无需遍历就可确定表尾节点,zllen记录压缩列表包含的节点数量,zlend标记压缩列表末端
在这里插入图片描述
prevlen记录的是前一个节点字节长,可据此快速定位前一个节点起始位置,支持反向遍历,encoding记录节点的content属性所保存数据的类型和长度
对节点进行操作时,需将压缩版的entry转换成zlentry以节省内存

Redis对象

redis底层抽象出一个redisObject,结构体如下:
在这里插入图片描述

  • type:对应type命令,存储redis对象,取值有:
    在这里插入图片描述
  • encoding:对应object encoding命令,记对象底层数据结构,取值有
    在这里插入图片描述
  • lru:对象最后一次被命令程序访问的时间
    在这里插入图片描述
  • refcount:对应object refcount命令,共享对象都存在sharedObjectsStruct结构体,如常见字符串、10000个字符串对象,值分别是[0~9999]的整数值等
  • ptr:模拟多态,可存储任何类型的对象

字符串对象

会根据实际情况选择int、embstr、raw三种encoding形式
在这里插入图片描述

字符串对象使用场景

缓存功能,使用场景是mysql存储,redis做缓存

  • 计数器,如点赞次数,视频播放次数。
  • 共享session
  • 限流,如60s内不能超过5次

列表对象

在这里插入图片描述
尽管插入删除复杂度为O(n),但不需内存拷贝,因此仍高效,访问两端元素复杂度为O(1),每片entry节点内存连续且顺序存储,可由二分查找以 O(logn) 复杂度定位
列表对象操作有阻塞和非阻塞。 当所有列表中不存在给定key,或key中包含的是空列表,BLPOP或BLPOP命令将被阻塞,直到client将新数据加到任意key的列表中,才会解除阻塞

列表对象使用场景

列表对象可使用不同命令组合成不同数据结构,以消息队列举例

  • 消息队列:lpush+brpop=Message Queue,生产者lrpush往左侧插元素,消费者brpop阻塞式的“抢”尾部元素,多个消费者保证负载均衡和高可用性
  • 栈:lpush+lpop=Stack
  • 队列:lpush+rpop=Queue
  • 有限集合:lpush+ltrim=Capped Collection

哈希对象

同样根据实际情况底层选择ziplist和dict实现哈希对象
在这里插入图片描述
由ziplist转dict的操作不可逆。尽可能用ziplist来作hash底层。长度应在1000内,否则存取时间复杂度在O(n)到O(n^2),会导致CPU消耗严重

集合对象

同样根据实际情况底层选择intset和dict实现集合对象
在这里插入图片描述

集合对象使用场景

标签:主要用于社交里面感兴趣的内容(注意尽量保证一个事务下完成),注意set不允许重复,支持交并补

有序集合对象

同样根据实际情况底层选择intset和dict实现集合对象
在这里插入图片描述
同样ziplist转skiplist不可逆。两参数也可在配置文件中修改

有序集合对象使用场景

排行榜:如网站排行榜,排行维度是多方面:时间、播放量、得赞数等

网络模型

事件模型

Reactor模式指由多个客户端并发请求服务端服务。每个服务由一个单独的事件处理程序调度特定服务的请求。调度由管理已注册事件处理程序的启动调度程序执行,redis是单线程的reactor
在这里插入图片描述
优缺点:

  • 优点:
    • 响应快,不必为单个同步操作所阻塞
    • 可扩展性,可扩展reactor实例个数利用CPU资源
    • 可复用性,reactor与具体事件处理逻辑无关,便于复用
  • 缺点:
    • 共享同一个reactor长时间读写,影响响应,可考虑thread-per-connection

Redis网络模型

网络库主要关心的事件:文件事件、定时事件及信号。redis中前两个用I/O复用统一管理,信号由信号处理函数异步处理,事件处理框架如图:
在这里插入图片描述

  • 定时事件:redis支持的是周期任务事件,即执行完后不会删除,而是重新插入链表。定时器用链表管理,新定时任务插入链表表头
  • 信号:initserver中注册信号处理函数sigShutdownHandler,在处理函数中主要将shutdown_asap置为1,若之前已经为1,直接exit,否则在serverCron函数的prepareForShutdown中收尾

redis通信协议

Redis基于Redis Serialization Protocol协议通信。其本质是文本协议,实现简单、易于解析。
在这里插入图片描述

redis命令格式

命令使用的是redisCommand数据结构来管理
在这里插入图片描述

  • arity:限制命令的个数,包含命令本身
  • sflag:字符串方式设置命令的属性
  • flags:将sflags字符串转成整型,多个属性之间用"|"运算,内部自动解析,见函数populateCommandTable
  • redisCommandProc:函数指针类型,指向命令实现函数

redis流程

单机版大致流程图
在这里插入图片描述

持久化

Redis优点内存读取快但容易丢失,断电后内存数据消失,但redis支持持久化,包括RDB(Redis Data Base)、AOF(Append Only File)

RDB

RDB思想是把当前进程数据生成快照保存到硬盘,保存数据库的键值对。触发RDB持久化过程分为手动触发和自动触发
在这里插入图片描述
流程如图
在这里插入图片描述
自动触发是在serverCron中,根据参数来进行判断
在这里插入图片描述

AOF

日志的方式记录每次写命令,重启时重新执行文件中的命令达到恢复的目的。主要解决数据持久化的实时性,目前是Redis持久化的主流方式

  • 写入到AOF文件的命令都以RESP协议纯文本格式保存,可直接改
  • 只保存写命令(pubsub除外)
  • 支持aof重写
    执行流程如图
    在这里插入图片描述

AOF重写

随着服务器运行,AOF文件会越来越大,导致还原所需时间越多。重写后的aof具备如下特点:

  1. 过期的键不会写入
  2. 重写用最终数据生成,新AOF文件只保留最终数据写入命令
  3. 多条写命令可以合并为一个
  4. 单独开辟一个子进程执行rewrite

同样AOF重写包括手动触发(bgrewriteaof)和自动触发
bgrewriteaof流程如图

在这里插入图片描述
自动触发是通过配置文件中appendonly yes开启自动写入策略:同步、每秒、no等同步策略

过期处理对比

在这里插入图片描述

优缺点对比

RDB

  • 优点:
    • 文件紧凑,一个二进制文件,适用于备份,全量复制等场景。
    • 父进程不进行任何IO操作
    • Redis启动时恢复速度快
  • 缺点:
    • 无法做到实时持久化/秒级持久化。可能会丢失数据
    • 兼容性的问题

AOF

  • 优点:
    • 提供更灵活的策略,来平衡性能和可靠性。
    • 追加模式,容错性强,写到一半宕机或者错误,可以快速恢复
    • 优先使用AOF
  • 缺点:
    • 对于相同数量的数据集而言,AOF文件通常要大于RDB文件
    • 恢复速度慢于rdb

缓存

缓存淘汰

本身缓存固定但数据大于缓存量,如何选择缓存数据,默认实现LRU

  • FIFO
  • LRU:最近最少使用,双向链表+哈希表实现,用链表结点组织数据,最近长访问排在前,哈希表存储key和值链表结点,保证有序
  • LFU:最不经常使用,结点和被访问次数并串成链表,最多访问次数放首,访问次数最少放尾,淘汰最少次数结点

过期删除

缓存都有有效期,为避免前端读老数据,缓存会设置过期时间,到期则删除,策略是定期删除=主动+惰性

  • 主动删除:到达指定的删除间隔后主动删除,易于理解,通过回调函数删除,设置合理间隔会节省内存,缺点是redis忙时设置睡眠时间碰巧到期,主动删除会额外增加redis负担
  • 惰性删除,程序取值时看数据是否已经过期若没则返回,否则删除,优点是资源占用小,缺点是某些数据长期霸占在内存不被删除

缓存一致

业务数据从数据库走,数据库读取基于硬盘,通过缓存加快读取,需保证缓存的数据和数据库中数据相同,即缓存一致。多并发时,多写一读可能不一致。注意可在小范围时间内数据不一致,但在并发请求后最终要一致。
两种读写流程

  1. 先删除缓存在更新数据库
    在这里插入图片描述

会出现一些问题,客户1时刻1删除缓存,时刻2完成,客户2时刻2查询,查询失败,从数据库查询并更新缓存,由于写数据库比读更慢,故时刻3客户2读到并更新缓存,而时刻4后时刻5又有客户3查询,读到的客户2更新到缓存的老数据,导致后面都是老数据

为解决该问题,提出延时双删
在这里插入图片描述

时刻1客户删除数据,时刻2完成删除,时刻2客户2查询失败,时刻3查询数据库,并更新缓存,时刻4由于缓存时老数据,客户1完成数据库修改,因此删除缓存,时刻5客户发现缓存失败读数据库,后面保证一致性

  1. 先更新数据库在删除缓存
    在这里插入图片描述

比1好但也会不一致,发生概率低,如时刻1客户查询失败,时刻2查询数据库,客户2时刻2发生数据修改,更新数据库在删除缓存,但更新后,客户1(网络阻塞等)比客户2请求慢,但server拿到旧数据,写入缓存的也是老数据,写入缓存的老数据但数据库中是新数据,时刻5后面读取的都是缓存的老数据。

缓存击穿

若对某数的查询突然特别大(不是热点数据),缓存无该数据,但数据库有,请求就会到后端数据库,若达到mysql上限就会宕机。一般缓存都会设置过期时间,故缓存击穿较常见
在这里插入图片描述

解决方法:

  1. mysql角度,减少击穿后的直接流量(锁)
  2. redis角度,设置热点数据永不过时,或后台启动异步线程,把数据回写到缓存层

缓存穿透

查询缓存和数据库都不存在的数据,解决办法:

  1. 拦截非法查询请求
  2. 缓存空对象(value为空)
  3. 布隆过滤器,快速判断数据是否存在某个集合

布隆过滤器:设计k个不同hash函数,把数据映射到指定位置置为1,查询时通过hash函数判断对应位置是否全为1,只要有1位为0则一定是不存在,否则极大概率存在,但不一定存在
在这里插入图片描述

缓存雪崩

一大批被缓存的数据同时失效,此时对于这批数据请求就全打到数据库,导致数据库宕机,注意缓存击穿时单点,雪崩是多点,解决方法类似雪崩:

  1. 从mysql角度:减少并发量
  2. Redis角度:设置永不过期热点、分析失效时间尽量让失效时间点分散、缓存预热(即上线前,根据当天情况,将热数据直接加载到缓存系统(最可能时开机时间,刚开始服务时,没有缓存,所以服务上线前,缓存预热))

集群

前面是单机服务器运行方法,若多个装redis的机器协同合作,帮助分担压力,官方提供如下的解决方案,先确定基本概念

  • 服务器运行ID:唯一确定主库身份
  • 复制偏移量:主节点传输了的字节数
  • 复制挤压缓冲区:复制挤压缓冲区是FIFO队列,存储了最近主节点的数据修改命令

主从复制

客户端请求,主库写,从库读,主从之间数据同步。
在这里插入图片描述
主库从库上线后,不急着直接进行复制,先进行握手进行信息验证。主和从中配置文件中写入master和slave字段,以及ip和端口,上线后进行握手
在这里插入图片描述

握手完成后,从库向主库发送PSYNC命令,开启数据同步,并发送主库ID(网断后,可能认为之前主库不可靠,设置新的主库),复制偏移量offset(有可能中间断网,因此是第二次/三次连接)
在这里插入图片描述
主库会根据从库发送信息判断,并告诉从库是全量复制/断线后重复复置
在这里插入图片描述

全量复制

初次复制后的同步。主库执行BGSAVE,生产对象RDB文件开辟缓冲区记录RDB文件实行过程中,收到的新数据命令。RDB产生后,主库发给从库,从库通过rdb恢复数据

命令传播阶段

主库状态改变(如增加新数据,修改原始数据)为使得从库和主库数据状态一致,主库将会把数据变更命令发给从库,从库收到后执行命令

断线后复制

从库与主库短线重连后复制,此过程以来服务器运行ID,复制偏移量,复制挤压缓冲区

哨兵

本质上讲,从代码层面上讲,哨兵是不提供数据服务的redis服务器。哨兵数据结构如下:
在这里插入图片描述
哨兵启动流程如图
在这里插入图片描述

哨兵对主库从库统一监控,若主库宕机,哨兵组进行投票,从从库中重新选出主库
在这里插入图片描述

如图一主库三从库,每个机器都会隔段时间(10s)向哨兵发送心跳。哨兵若一定时间收不到就默认断联。当某哨兵发现主库断连,会将其标注为主观下线,并通知其他哨兵连接主库。半数哨兵确认连接不上后,就会标记该主库为客观下线,并执行故障转移(从从库中挑个当主库)

通过选举协议,从所有哨兵中选择一节点作为老大,主持负责新主库的选举工作。选取完成后,哨兵会对该从库发送slaveof no one,该从库就变成主库。同时向其他slave发送新主库的ip端口
在这里插入图片描述
若此时主库重新上线就会被哨兵降级为从库,从属于新主库
在这里插入图片描述

集群

redis提供的分布式数据库解决方案,自动将数据切分成多个节点存储,即使一部分节点宕机也可继续数据库操作
分区策略:采用虚拟槽,所有键通过CRC16校验函数,对16384取模,决定数据分配到哪个槽位,每个节点负责部分槽数据的存储,该节点可结合主从复制模式,将分配给它的数据进行复制
查询策略:每个节点都会存储整个集群节点信息,这些信息被称元信息,包括各节点的草数据、各节点master和slave状态、各节点是否存活
元数据信息的传播:gossip协议,每个节点周期性每隔一定时间,把自己的信息通过该协议选择k个邻接节点散播出去
集群查询逻辑:由于集群可能发生数据的扩容和缩容,因此数据可能发生迁移,查询逻辑如图
在这里插入图片描述

总结

本文详细总结redis基本数据结构、redis对象及使用场景、网络模型、持久化、缓存、集群等知识

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值