Redis相关梳理
一:五种常用数据类型
简单动态字符串
redis做了一个用作字符串的SDS,除了一些不需要修改的场景,都是用SDS
C字符串的底层实现总是一 个N+1个字符长的数组
sds.h:
struct sdshdr { // buf 中已占用空间的长度 int len; // buf 中剩余可用空间的长度 int free; // 数据空间 char buf[]; };
1 | C字符串 | SDS |
2 | 求长度,需要遍历O(n) | 直接取len就可以O(1) |
3 | 容易造成缓冲区溢出 | |
4 |
减少了内存重新分配次数 (速度要求严苛,避免分配内存耗时多) |
|
5 | 二进制安全 |
4:SDS有free,可以比C字符串多一些预留空间。空间优化策略主要有两种:
- 空间预分配
- SDS进行修改时,会额外获得未使用空间。
- 修改后空间n
- n<1M; free分配n+1byte(空字符)
- n>=1M; free分配1M+1byte
- 惰性空间释放
- SDS进行缩短时,不释放删除的空间,加到free里。
SDS的API
- sdsnew 创建
- sdsempty 创建空的SDS
- sdsfree 释放
- sdslen 获取len
- sdsvail 获取free数量
- sdsdup 创建一个sds副本
- 。。
链表
redis自己构建的链表:
/* * 双端链表节点 */ typedef struct listNode { // 前置节点 struct listNode *prev; // 后置节点 struct listNode *next; // 节点的值 void *value; } listNode;
/* * 双端链表迭代器 */ typedef struct listIter { // 当前迭代到的节点 listNode *next; // 迭代的方向 int direction; } listIter;
/* * 双端链表结构 */ typedef struct list { // 表头节点 listNode *head; // 表尾节点 listNode *tail; // 节点值复制函数 void *(*dup)(void *ptr); // 节点值释放函数 void (*free)(void *ptr); // 节点值对比函数 int (*match)(void *ptr, void *key); // 链表所包含的节点数量 unsigned long len; } list;
字典
也叫:符号表、关联数组、映射,用于保存键值对;
/* * 字典 */ typedef struct dict { // 类型特定函数 dictType *type; // 私有数据 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 // 当 rehash 不在进行时,值为 -1 int rehashidx; /* rehashing not in progress if rehashidx == -1 */ // 目前正在运行的安全迭代器的数量 int iterators; /* number of iterators currently running */ } dict;
理解:
- 字典dict;
- 属性type是指向dictType的;dictType是保存特定类型键值对的函数;redis会为不同的字典设置不同的函数。
- 属性privateData是存储对应类型的可选参数。
- ht是指向dictht的引用;
- 其中table是指向dictEntry的二维引用,有两级。
- 第一级是hash后的值,到阈值就要rehash扩缩容
- dictEntry是哈希节点
- key是键
- value是值
- 还有指向下一个节点的引用next,用于成链。
hash算法
冲突解决
哈希表使用链地址法来解决键冲突:
哈希表节点dictEntry的指针构成一个链,hash相同的就排在当前dictEntry的next
rehash
1.为ht[1]分配空间,与ht[0]比较,扩容则分配
// 新哈希表的大小至少是目前已使用节点数的两倍 // T = O(N) return dictExpand(d, d->ht[0].used*2);
收缩则分配大小等于ht[0]的?
/* * 缩小给定字典 * 让它的已用节点数和字典大小之间的比率接近 1:1 * 返回 DICT_ERR 表示字典已经在 rehash ,或者 dict_can_resize 为假。 * 成功创建体积更小的 ht[1] ,可以开始 resize 时,返回 DICT_OK。 */ int dictResize(dict *d) { int minimal; // 不能在关闭 rehash 或者正在 rehash 的时候调用 if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR; // 计算让比率接近 1:1 所需要的最少节点数量 minimal = d->ht[0].used; if (minimal < DICT_HT_INITIAL_SIZE) minimal = DICT_HT_INITIAL_SIZE; // 调整字典的大小 // T = O(N) return dictExpand(d, minimal); }
2.把ht[0]复制并重新hash计算到ht[1]上
3.把ht[0]释放,ht[1]设置为ht[0]。
hash表的扩容与收缩
哈希表会自动在表的大小的二次方之间进行调整。
在没有bgSave或bgRewriteAOF命令时,负载因子大于1;或者有bgSave或bgRewriteAOF命令时,负载因子大于5;时执行
负载因子= 已保存/哈希表大小
渐进式rehash
跳跃表
skipList
命令:
ZRANGE
ZCARD
有序,按value值排序?
平均O(logN)复杂度
适用于有序集合元素较多或集合中元素是较长字符串等场景。
具体应用:
- 实现有序集合键
- 在集群节点中用作内部数据结构
/* * 跳跃表 */ typedef struct zskiplist { // 表头节点和表尾节点 struct zskiplistNode *header, *tail; // 表中节点的数量 unsigned long length; // 表中层数最大的节点的层数 int level; } zskiplist;
和
/* * 跳跃表节点 */ typedef struct zskiplistNode { // 成员对象 robj *obj; // 分值 double score; // 后退指针 struct zskiplistNode *backward; // 层 struct zskiplistLevel { // 前进指针 struct zskiplistNode *forward; // 跨度 unsigned int span; } level[]; } zskiplistNode;
理解:
- 跳表zskipList;
- 属性header和tail分别指向zskiplistNode的头尾指针。
- zskiplistNode
- 层
- 后退
- 分支
- 成员对象
- zskiplistNode
- level记录层数最大的层数
- 属性header和tail分别指向zskiplistNode的头尾指针。
整数集合
命令:
SADD numbers 1 3 5 7 9
typedef struct intset { // 编码方式 uint32_t encoding; // 集合包含的元素数量 uint32_t length; // 保存元素的数组 int8_t contents[]; } intset;
uint32_t 取值范围 0 ~ 4,294,967,295int8_t 取值范围 -128 ~ 127
contents中的值从小到大排列,并且没有重复元素。
整数集合升级过程:
- 根据新元素的类型,扩展数组空间,为新元素分配空间。
- 将已有的数据转换成相同类型,保持排序。
- 将新元素加到新数组里。
升级之后不支持降级,即使没有当前等级的元素。
压缩表
zipList 是列表键和hash键的底层实现之一。
RPUSH lst 1 3 5 10086 "hello" "world"
对象
- 字符串对象
- 列表对象
- 哈希对象
- 集合对象
- 有序集合对象
每种对象都最少对应上述一种数据类型
不同的对象有不同的特性~(省略)。
二:单机
数据库
struct redisServer{ // ~ // 一个数组保存,保存服务器中所有数据库 redisDb *db; // ~
//根据此数量决定在初始化时创建数据库个数
int dbnum;
} intset;
通过SELECT n 可以切换到不同的库
原理
保存键值对的方法
过期键删除策略
- 定时删除
- 设置过期时间的同时,创建一个定时器,定时器到时间执行对键的删除操作。 --- 定时器是怎么实现的??
- 惰性删除
- 放任过期时间不管,但是每次从键空间中获取键时,都检查取得的键是否过期。过期删除,没过期返回。--这个存在空间的浪费
- 定期删除
- 每隔一段时间,对数据库进行一次检查,删除过期key。检查力度由算法决定。
1.定时删除 对内存最友好 对CPU时间不友好
CPU紧张时,影响服务器的响应时间和吞吐量。
实现定时器,需要用到redis服务器中的时间事件,当前时间事件的实现方式是:无序链表,查找效率为O(N),效率较低。
现阶段来说不实用。
2.惰性删除 对CPU时间友好 对内存不友好
浪费内存,由内存泄漏的危险
3.定期删除
前两种的整合
每个一段时间执行一次删除过期键,减少内存浪费
删除太频繁或执行时间长,会退化成定时删除,节约CPU
所以这个要合理配置。
redis的过期键删除策略
惰性删除 + 定期删除
持久化机制
aof 和 rdb
事件、redis初始化过程等其他
三:分布式
https://www.cnblogs.com/jiangym/p/15881521.html#_label6
Sentinel
主从同步
复制
集群
sentinel
监控
主从选举
四:独立功能
发布订阅
事务
Lua脚本
redis2.6引入Lua脚本
在redis客户端可以直接使用Lua脚本
redis> EVAL "return 'hello world'" 0
"hello world"
Lua环境初始化(对Lua环境进行修改产生的影响)
执行Lua脚本中包含redis命令的伪客户端
Lua脚本的脚本字典
管理脚本的命令SCRIPT FLUSH、SCRIPT EXISTS、SCRIPT LOAD、SCRIPT KILL
EVAL命令和EVALSHA命令
排序 SORT
Sort可以对int或者字符等进行排序
还可以使用 SORT ~ BY ~ 以什么字段为权重排序
sort命令的最简单的执行形式为 SORT <key>
redis> SORT numbsers
1)"1"
2)"2"
3)"3"
命令的详细步骤:
1.创建一个和numbsers列表长度相同的数组,数组的每一项都是redisSortObject结构
2.遍历数组,将Obj指针和列表之间一一对应
3.遍历数组,将obj指向的列表转换成double浮点数,放到u.score里
4.根据u.score属性从小到大排列
5.遍历数组,返回排序结果。
慢查询
redis分布式锁
https://www.cnblogs.com/jiangym/p/15877382.html#_label1_1 锁 和一部分lua脚本
redisson
看门狗 锁续期
https://www.cnblogs.com/jiangym/p/15877382.html#_label1_1
使用
https://www.cnblogs.com/jiangym/p/16017462.html
存储海量数据 100亿
Redis 数据结构 核心数据结构, 部署方式 容灾备份
持久化
RDB
每隔多长时间存储一次 dump.rdb
rdb是默认的持久化方式
快照:
- 产生快照有几种情况:
- 手动bgsave;
- 手动save;
- 根据配置时间自动执行;
- 主从时,从节点发送sync,主节点会bgsave
AOF
aof默认关闭
根据配置方式可以同步
- 1s一次
- 不主动存
aof易读
怎么选择持久化方式
- 同时开启
- 主从模式下,从节点做持久化也行,可以把aof关了
- 混合模式
- redis4.0支持
- rdb可以写到aof中
容灾备份
rdb和aof备份,定时脚本,定时备份这个文件。
混合模式就可以了。
redis优化方案
独立部署
cpu密集型操作,解决子进程开销问题。
硬盘优化
大量写入请求:ssd磁盘
缓存禁用持久化
a缓存失效,B缓存顶上去。对于数据丢失问题,不考虑。则把持久化砍掉。
主从模式,从持久化
正常流程:从节点sync给主节点-》主bgsave-》fork子进程做RDB快照
既然用了主从,关闭主节点持久化。从节点15分钟。save 900 -1
甚至可以禁用aof,主从了已经是高可用,某个节点挂了数据还是有,可以省一大部分资源
(新浪这么做的)
优化fork处理
Redis主从
过期策略
allkeys
volitle
ttl 过期时间
lfu 频率访问低, 引用计数,排序
lru 最近最少 链表 用过之后放头,删除从队尾删
random 随机
不过期
缓存穿透、缓存及穿、缓存雪崩
主从
一般座位缓存,读多写少。
目标:读压力释放出去。
从节点用于读操作。
并发读,压力大,就加从节点。无休止扩展从节点。内部数据冗余。
缺点:
- 数据冗余
- 从节点重启,全量复制主节点,pingpang心跳。
- 存储压力大。(集群解决了这个问题,分片存)
- master写压力大
- 单点故障问题(哨兵解决这个问题。)
主从复制原理
从节点增加配置 slaveof 192.168.1.1 6379
设置好主节点的IP就可以主从复制了。
- 全量复制
- 增量复制
复制流程
全量: slave-》sync命令-》maste -》basave -》 生成rdb-》从加载rdb-》init
异步性
bgsave时,主节点还能有新数据。
会有脏读的问题
从节点也是。
过期key处理
从节点是没有删除的,要接受主节点的del命令
故障及解决方案
数据一致性问题
主从网络延时,主多从少,进行部分重同步。通过指令触发。人为断开也可以。
数据延迟问题
从服务器有偏移量。监听这个偏移量报警。
脏数据
1.忽略,查询场景可以容错。
2.强制读主节点。
3.从节点查询时要判断过期时间。
数据安全性
主挂了又重启了,从节点复制过来一个空。
解决办法: 主节点不自动重启。
性能优化
- 规避全量复制
- 低峰时段挂在从节点
- 选举slave为主节点
- 增大缓冲区
- 规避复制风暴(从节点全去主从同步)
- 选举从节点为主节点
- 或树状复制结构
哨兵
redis2.8开始,引入哨兵,自动化恢复。
sentinel是分布式系统,降低误报率。
至少3个sentinel实例,默认端口26379。(奇数) 健壮
指定配置文件。
缺点:
- 主节点写入压力。
- 写能力。存储能力收到单机的限制。
- 动态扩容复杂。
集群(高可用)
集群概念
一组计算机作为一个整体,提供服务。
- 可扩展
- 高可用
- 负载均衡
- 错误恢复
集群主要分三类
HA 高可用
LBC 负载均衡 nginx
gosip协议连接
架构
10wqps,单机挂了-》主从复制
主从稳定-》sentinel,自动恢复
单节点存储-》集群解决
cluster 无中心结构
每个节点都保存数据,和其他节点连接。
最少3主3从。集群结构。
插槽16384个。
主从,就是高可用性。挂了有备份。
缺点:
无法数据强一致性
搭建复杂
数据分区
分布式存储
增强可用性,维护方便,均衡IO,改善查询性能。
分区算法
一致性hash
扩容和取余,会影响相邻节点。适合规模大的节点。
虚拟槽分区 232
分区力度更小,
哈希槽
16384个槽位
集群高可用
新增节点
新节点的插槽怎么分配?
重新分配上一个环中的数据。
节点宕机
存下一个节点里。
数据倾斜
引入虚拟节点。主机名加编号。node1#1,node1#2,node1#3
相关文件
《redis设计与实现》
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)