Redis |
描述 |
Redis |
Redis是一个开源的使用C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库 |
数据类型
/*
* SDS
*/
struct sdshdr {
int len;
int free;
char buf[];
};
String |
描述 |
String |
采用SDS结构进行存储 |
sds |
Redis在为一个字符串创建一个sds 对象时,通常会申请比字符串长度更长的字节数组,Redis将字符串保存进这个数组,同时在len这个变量保存字符串的长度,再用free这个变量保存buf尚未使用的字节数量 |
内存分配 |
通过申请更多的内存,减少字符串修改时内存的分配次数 |
二进制安全 |
C字符串不能包含空字符,因为先被程序读入的空字符会被误认为字符串的结束,只能保存纯文本数据,sds 长度是根据len决定的,即使内容中含有空字符串,也对数据的完整性也没有任何影响 |
链表 |
描述 |
单链表 |
链表中的数据是以结点来表示的,每个结点的构成:元素(数据元素的映象) + 指针(指示后继元素存储位置),元素就是存储数据的存储单元,指针就是连接每个结点的地址数据 |
优点 |
任意位置添加删除元素的都非常的快 |
缺点 |
查找方便,不适合随机查找 |
双链表 |
结点即要保存到下一个元素的指针,还要保存一个上一个元素的指针 |
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
image.png
压缩列表 |
描述 |
ziplist |
压缩列表是列表、哈希的底层实现之一,压缩列表可以包含多个节点,每个节点保存一个字节数组或者整数值 |
image.png
压缩列表节点 |
描述 |
previous_entry_length |
记录了压缩列表中前一个节点的长度 |
encoding |
数据类型以及长度 |
content |
节点值 |
List |
总结 |
list |
list = 压缩列表 或者 双链表,双链表广泛运用在List/发布与订阅/慢查询/监控器等 |
/*
* 字典
*/
typedef struct dict {
dictType *type;
void *privdata;
// 哈希表,每个字典包含两个table
//一般情况下字典只使用ht[0]这个哈希表,ht[1]只会在对哈希表进行rehash时才会使用
dictht ht[2];
int rehashidx;
int iterators;
} dict;
typedef struct dictht {
dictEntry **table; // 哈希表数组 ,存储dictEntry
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
/*
* hash表节点
*/
typedef struct dictEntry {
void *key; // 键
union { // 值
void *val;
uint64_t u64;
int64_t s64;
} v;
struct dictEntry *next; // 指向下个哈希表节点,形成链表
} dictEntry
hash |
描述 |
哈希算法 |
当要将一个新的键值对添加到字典里面时,程序会根据键计算出哈希值和索引值,然后再根据索引值将新键值对放到哈希表数组指定的索引上 |
键冲突 |
两个不同的键值对(key相同覆盖,不同放入链表),计算出的哈希值和索引值是一样的,使用链地址法:每个hash节点都有一个next指针,指向下一个元素存储位置,形成一个单链表(取值时直接比较key是否相等) |
字典 |
每个字典包含两张哈希表数组,一般情况下字典只使用ht[0]这个哈希表,ht[1]只会在对哈希表进行rehash时才会使用 |
rehash |
哈希表扩展或者收缩时,将现有哈希表的所有键值对rehash到另外一张哈希表。但是这个rehash动作并不是一次性完成的,而是分多次渐进式完成的(键值对过多时,一次性rehash肯定会消耗服务器大量的计算资源) |
/*
* 整数集合
*/
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[]; // 保存元素的数组
} intse
无序集合 |
描述 |
set |
使用了intset和hashtable两种数据结构存储的 |
整数集合 |
intset存储条件:①结合对象保存的所有元素都是整数值②集合对象保存的元素数量不超过512个; 整数集合数组元素从小到大有序地排列,并且数组中不包含任何重复项 |
跳跃表性质 |
由很多层结构组成 |
每一层都是一个有序的链表 |
最底层(Level 1)的链表包含所有元素 |
如果一个元素出现在 Level i 的链表中,则它在 Level i 之下的链表也都会出现 |
每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素 |
image.png
typedef struct zskiplistNode {
//层
struct zskiplistLevel {
struct zskiplistNode *forward;//前进指针
unsigned int span;//跨度
} level[];
struct zskiplistNode *backward; //后退指针
double score; //分值
robj *obj; //成员对象
} zskiplistNode;
typedef struct zskiplist {
struct zskiplistNode *header, *tail; //表头节点和表尾节点
unsigned long length; //表中节点的的数量
int level; //表中层数最大的节点层数
} zskiplist;
内存回收
typedef struct redisObject {
// ...
// 引用计数
int refcount;
// ...
} robj;
内存回收 |
Redis 构建了一个由引用计数实现的内存回收机制,通过这一机制,程序可以通过跟踪对象的引用技术信息,在适当的时候自动释放对象并进行内存回收 |
对象共享
对象共享 |
Redis 在初始化服务器时,会创建一万个字符串对象,这些对象包含了 0 到 9999 的所有整数值。当服务器需要用到值为 0 到 9999 的字符串对象时,服务器就会使用这些共享对象。此外每被一个建引用对象的引用计数加一 |
redis数据库
redis数据库 |
描述 |
redisServer |
Redis是一个字典结构的存储服务器,默认支持16个数据库 |
redisClient |
可以指向不同数据库,达到切换目标数据库的目的 |
redisDb |
dict保存所有的键值对,每一个建都是字符串,每一个值都可以是任意Redis基本类型 |
struct redisServer {
redisDb *db; //一个数组,保存着服务器中的所有数据库
int dbnum; //根据dbnum属性创建服务器的数据库数量,默认16
};
typedef struct redisClient {
redisDb *db; //这个属性指向了redisDb数组的其中一个元素
} redisClient;
typedef struct redisDb {
dict *dict; //属于redisDb *db数组结构, 保存所有的键值对
} redisDb;
建过期
建过期 |
描述 |
expires |
过期字典,键指向dict的某一个键值对,值保存过期时间 |
过期时间查询 |
过期时间减去当前时间得到键剩余时间:TTL(秒为单位返回键的剩余生存时间) /PTTL(毫秒为单位返回键的剩余生存时间) |
过期键判断 |
当前时间戳跟过期时间戳比较, 当前时间戳大于过期时间戳代表过期 |
typedef struct redisDb {
dict *expires; //过期字典,保存键过期时间
} redisDb;
过期建删除
删除策略 |
描述 |
定时删除 |
在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作 |
惰性删除(对于CPU是最友好的) |
放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键 |
定期删除 |
每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定 |
RDB/AOF过期键处理
过期键处理 |
描述 |
生成RDB |
执行SAVE命令或者BGSAVE命令所产生的新RDB文件不会包含已经过期的键 |
载入RDB |
服务器开启了RDB 功能的情况,主服务器启动时,会检查所有键并忽略过期键,从服务器启动时无论是否过期都会载入RDB(主从同步时,从库RDB会被清空) |
重写AOF |
BGREWRITEAOF:重写AOF文件不会包含已经过期的键 |
AOF持久化运行模式 |
当一个键被惰性或者定期删除,服务器会追加一条DEL命令到现有AOF文件的末尾,显式地删除过期键 |
RDB
RDB |
描述 |
RDB |
RDB用于保存和还原所有字典的键值对 |
SAVE |
由服务器进程直接操作,阻塞其他命令 |
BGSAVE |
服务器子进程操作,不会阻塞 |
自动间隔保存 |
redis容许用户手动配置save选项,当满足条件时,自动触发BGSAVE 命令 |
dirty计数器 |
后数据库每修改一次dirty属性自增1,执行save或者bgsave操作命令之后清空零 |
lastsave |
记录上一次成功执行save或者bgsave操作命令的时间 |
检查保存条件是否满足 |
周期性检查dirty属性和lastsave属性是否满足了savparam条件,满足就执行BGSAVE |
struct redisServer{
struct saveparam *saveparams; //记录保存条件的数组
};
/** redis容许用户手动配置save选项,当满足条件时,自动触发BGSAVE 命令 */
struct savparam{
time_t seconds; //秒数
int changes; //修改数
};
struct redisServer{
long long dirty; //修改计数器
time_t lastsave; //上一次执行保存操作的时间
};
AOF
AOF |
描述 |
AOF |
保存Redis服务器所执行的写命令来记录数据库状态的 |
命令追加 |
服务器在执行完一个写入命令之后,会以协议格式将被执行的写入命令追加到服务器状态的aof_buf缓存区的末尾(如:set 命令) |
文件写入与同步 |
服务器每次结束一个事件循环之前, 它都会调用flushAppendOnlyFile函 数, 考虑是否需要将aof_buf缓冲区中的内容写人和保存到AOF文件里面 |
文件载入与还原 |
因为AOF文件里面包含了重建数据库状态所需的所有写命令,所以服务器只要读人并重新执行一遍AOF文件里面保存的写命令, 就可以还原服务器关闭之前的数据库状态 |
struct redisServer{
sds aof_buf; // aof 缓存区
}:
image.png
image.png
事件
事件 |
描述 |
事件驱动 |
Redis服务器是一个事件驱动器,服务器需要处理以下两类事件:文件事件与时间事件 |
文件事件 |
通过I/O多路复用程序来同时监听多个套接字对应分配处理事件 |
文件事件原理 |
基于Reactor模式 |
时间事件 |
Redis时间事件分为两大类:定时事件与周期性事件 |
时间事件原理 |
Redis服务器会将所有的时间事件放入到一个链表,每当时间事件执行器运行时,它就遍历整个链表,查找所有已经到达的时间事件,并调用相应的事件处理器 |
在3.0的版本中,服务器只有一个serverCron的时间事件
1.更新服务器的各类统计信息,比如时间、内存占用,数据库占用情况等;
2.清理数据库中过期的键值空间;
3.关闭和清理实现的客户端连接;
4.尝试进行AOF或RDB持久化操作;
5.如果是主服务器,会定期的对从服务器进行同步;
6.如果处于集群模式,对集群定期的进行同步和连接测试
image.png
Redis单线程
Redis单线程 |
描述 |
Redis服务器 |
服务器状态结构使用clients链表连接多个客户端状态,新添加的客户端状态会被放到链表末尾 |
struct redisServer {
list *clients; //一个链表,保存所有的客户端状态
};
Redis是单线程的,为何不利用多个CPU ?
官方解释:CPU通常Redis的瓶颈,瓶颈一般是内存或网络IO。
订阅频道
订阅频道 |
描述 |
SUBSCRIBE |
订阅频道,将客户端添加到链表的末尾 |
PUBLISH |
发送消息到频道 |
UNSUBSCRIBE |
它从 pubsub_channels 字典的给定频道(键)中, 删除关于当前客户端的信息 |
struct redisServer {
dict *pubsub_channels; //键代表被订阅的频道, 值代表链表,保存了所有订阅这个频道的客户端
};
def SUBSCRIBE(client, channels): //伪代码
#遍历所有输入频道
for channel in channels:
# 将客户端添加到链表的末尾
redisServer.pubsub_channels[channel].append(client)
def PUBLISH(channel, message): //伪代码
# 遍历所有订阅频道 channel 的客户端
for client in server.pubsub_channels[channel]:
# 将信息发送给它们
send_message(client, message)
订阅评定与订阅模式
客户端A发送消息给news.it频道,客户端C、客户端D同样受到消息(PUBLISH 命令发送信息到某个频道时,订阅频道客户端 与 匹配订阅模式的的客户端同样受到信息)
image.png
订阅模式
订阅模式 |
描述 |
SUBSCRIBE |
订阅模式 |
PUNSUBSCRIBE |
程序会删除 redisServer.pubsub_patterns 链表中, 所有和被退订模式相关联的 pubsubPattern 结构 |
struct redisServer {
list *pubsub_patterns; //一个链表
};
typedef struct pubsubPattern {
redisClient *client; //订阅模式的客户端
robj *pattern; //订阅的模式
} pubsubPattern;
def PUBLISH(channel, message): //完整的PUBLISH伪代码
# 遍历所有订阅频道 channel 的客户端
for client in server.pubsub_channels[channel]:
# 将信息发送给它们
send_message(client, message)
# 取出所有模式,以及订阅模式的客户端
for pattern, client in server.pubsub_patterns:
# 如果 channel 和模式匹配
if match(channel, pattern):
# 那么也将信息发给订阅这个模式的客户端
send_message(client, message)
事物
事物 |
描述 |
事物概念 |
事务提供了一种“将多个命令打包, 然后一次性、按顺序地执行”的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令 |
MULTI |
让客户端从非事务状态切换到事务状态,将这些命令全部放进一个事务队列 |
事务队列 |
一个数组(参数:执行的命令,命令的参数,参数的个数) |
EXEC |
服务器根据客户端所保存的事务队列, 以先进先出(FIFO)的方式执行事务队列中的命令 |
DISCARD |
取消一个事务, 清空客户端的整个事务队列 |
带 WATCH 的事务 |
事务开始之前监视任意数量的键: 当调用 EXEC 命令执行事务时, 如果任意一个被监视的键已经被其他客户端修改了, 那么整个事务不再执行, 直接返回失败 |
watched_keys |
键被监视的键, 值则是一个链表, 链表中保存了所有监视这个键的客户端 |
WATCH 原理 |
将当前客户端和要监视的键在 watched_keys 中进行关联 |
WATCH 触发 |
在任何对数据库键空间进行修改的命令成功执行之后,检查数据库 watched_keys 字典, 查找被修改的客户端并打开REDIS_DIRTY_CAS 标识 |
WATCH 事物下 执行 EXEC 命令 |
REDIS_DIRTY_CAS 标识被打开代表事务的安全性已经被破坏,清空清空事务队列并返回 |
def execute_transaction(): //事务的整个执行过程伪代码
# 创建空白的回复队列
reply_queue = []
# 取出事务队列里的所有命令、参数和参数数量
for cmd, argv, argc in client.transaction_queue:
# 执行命令,并取得命令的返回值
reply = execute_redis_command(cmd, argv, argc)
# 将返回值追加到回复队列末尾
reply_queue.append(reply)
# 清除客户端的事务状态
clear_transaction_state(client)
# 清空事务队列
clear_transaction_queue(client)
# 将事务的执行结果返回给客户端
send_reply_to_client(client, reply_queue)
typedef struct redisDb {
dict *watched_keys; //键被监视的键, 值则是一个链表, 链表中保存了所有监视这个键的客户端
} redisDb;
事物
事务的 ACID 性质 |
描述 |
原子性(Atomicity) |
单个 Redis 命令的执行是原子性的,但 Redis 事务的执行并不是原子性的,当事务失败时,Redis 也不会进行任何的重试或者回滚动作 |
一致性(Consistency) |
Redis的事务并不具有一致性,①入队错误(发送错误命令,EXEC 命令时返回拒绝执行)②执行错误(执行过程错误,也不会影响后面要执行的事务命令)③Redis 进程被终结( Redis 服务器进程在执行事务的过程中被其他进程终结,或者被管理员强制杀死且没有采取任何持久化机制,重启之后的数据库总是空白的) |
隔离性(Isolation) |
Redis 是单进程程序,并且它保证在执行事务时,不会对事务进行中断,事务可以运行直到执行完所有事务队列中的命令为止。因此,Redis 的事务是总是带有隔离性的 |
持久性(Durability) |
事务的持久性由 Redis 所使用的持久化模式决定 |
复制
复制 |
描述 |
复制 |
通过执行SLAVEOF命令或设置slaveof选项,可以让一个服务器去复制另一个服务器 |
旧版复制
旧版复制 |
描述 |
同步(sync) |
同步操作用于将从服务器的数据库状态更新至主服务器当前所处的数据库状态 |
命令传播(command propagate) |
命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态出现不一致时,主服务器会将自己执行的写命令,发送给从服务器执行 |
同步过程
①从服务器向主服务器发送SYNC命令;
②收到SYNC命令的主服务器执行BGSAVE命令,在后台生成一个RDB文件,并使用一个缓冲区记录从现在开始执行的所有写命令;
③当主服务器的BGSAVE命令执行完毕时,主服务器会将BGSAVE命令生成的RDB文件发送给从服务器,从服务器接收并载入这个RDB文件,将自己的数据库状态更新至主服务器执行BGSAVE命令时的数据库状态。
④主服务器将记录在缓冲区里面的所有写命令发送给从服务器,从服务器执行这些写命令,将自己的数据库状态更新至主服务器数据库当前所处的状态。
新版复制
新版复制 |
描述 |
PSYNC |
解决旧版在处理重复制的低效率问题,分为完整重同步和部分重同步,分别用于处理初次复制(与SYNC命令一样)和断线后重复制 |
部分重同步
部分重同步 |
描述 |
部分重同步 |
分为三个部分:①主从服务器的复制偏移量②主服务器复制积压缓冲区③服务器运行ID |
主从服务器的复制偏移量 |
主从服务器会分别维护一个复制偏移量,通过对比偏移量,可以知道主从服务器是否处于一致状态,不一致则执行部分重同步 |
偏移量 |
主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量的值加上N;从服务器每次收到主服务器传播来的N个字节的数据时,就将自己的复制偏移量的值加上N; |
主服务器复制积压缓冲区 |
主服务器维护一个固定长度先进先出的队列,即为复制积压缓冲区,保存一部分最近传播的写命令,且为每个字节记录相应的复制偏移量 |
服务器运行ID |
当断线重连时,用来判断从服务器与上次连接的是否为同一主服务器 |
sentinel
image.png
sentinel |
描述 |
sentinel |
redis主从复制可将主节点数据同步给从节点,一旦主节点宕机,从节点作为主节点的备份可以随时顶上来 |
启动并初始化sentinel |
初始化sentinel分别为5个步骤 |
①初始化服务器 |
Sentinel的本质是一个运行在特殊模式下的Redis服务器,不需要使用数据库,因此不载入RDB或者AOF |
②将普通 Redis 服务器使用的代码替换成 Sentinel 专用代码 |
将普通Redis服务器使用的代码替换成Sentinel专用的代码 ,命令表中部分命令不载入,所以sentinel 部分命令无法使用(如:键值对命令,持久化命令,事物命令,脚本命令) |
③初始化 Sentinel 状态 |
保存了服务器所有和Sentinel功能有关的状态 |
④根据给定的配置文件, 初始化 Sentinel 的监视主服务器列表 |
将sentinel状态的 masters 属性进行初始化,masters 里面保存的是所有被监视的主服务器的信息以及其键值对应保存的是什么内容 |
⑤创建连向主服务器的网络连接 |
创建连向被监视主服务器的网络连接,Sentinel将成为主服务器的客户端,可以向主服务器发送命令,并从命令回复中获取相关的信息 (两个连向主服务器的异步网络连接:命令连接,用于向主服务器发送命令,并接收命令回复,订阅连接,用于订阅主服务器的 sentinel:hello 频道) |
获取主服务器信息 |
Sentinel 默认每十秒一次,向监视的主服务器发送 INFO 命令,并分析 INFO 命令的回复来 获取主服务器当前信息 以及 主服务器属下的所有从服务器信息 |
获取从服务器信息 |
当Sentinel发现主服务器有新的服务器出现时,除了会为这个新从服务器创建相应的实例结构之外,还会创建连接到从服务器的命令连接和订阅连接 |
Sentinel 发送与接收 |
在建立起订阅连接之后 ,通过频道向主服务器和从服务器发送和接收信息 |
检测主观下线 |
发送 PING 命令,并通过实例返回的 PING 命令回复来判断实例是否在线(指的是单个 Sentinel 实例对服务器做出的下线判断) |
检测客观下线 |
当sentinel监视的某个服务主观下线后,sentinel会询问其它监视该服务的sentinel,看它们是否也认为该服务主观下线(超过半数就为主观下线,指的是多个 Sentinel 实例在对同一个服务器做出 SDOWN 判断) |
选举领头sentinel |
当一个主服务器被判断为客观下线时,监视这个下线主服务器的各个Sentinel会进行协商,选举出一个领头Sentinel,并由领头Sentinel对下线主服务器进行故障转移操作 |
故障转移 |
领头Sentinel挑选一个状态良好、数据完整的从服务器,然后发送 SLAVEOF no one 命令,然后将这个从服务器转换成主服务器 |
集群
集群 |
描述 |
集群 |
Redis集群是分布式数据库方案,通过分片来进行数据共享,并提供复制和故障转移功能 |
节点 |
一个Redis 集群通常由多个节点组成,每个节点都是独立的,想要构建成一个包含多个节点的集群,可以向一个节点发送 CLUSTER MEET <ip> <port>命令,可以让节点与ip和port所指定的节点进行握手,握手成功节点就会将ip和port指定的节点添加到当前的集群中 |
启动节点 |
节点的本质还是服务器,服务器会根据 cluster-enabled(redis.conf配置项) 配置选项来决定是否开启集群模式 |
槽 |
Redis集群使用分片方式保存键值对,整个集群分为16384 个槽,每个键值对都属于槽的其中一个 |
槽指派 |
集群槽可以指派给各个节点, 当数据库中的 16384 个槽都有节点在处理时,集群处于上线状态,否则,处于下线状态 |
集群中的执行命令 |
数据库向节点发送键命令,会判断槽是否由当前节点,如果当节点发现键所在的槽并非由自己负责处理的时候,就会向客户端返回一个 MOVED 错误,指引客户端转向正在负责槽的节点 ,如果是则进行执行 |
重新分片 |
Redis集群的重新分片操作可以将任意数量已经指派给原本节点的槽 改为指派给目标节点,并且相关槽所属的键值对也会从源节点移动到目标节点 |
ASK 错误 |
在重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对保存在目标节点中 ------ 当客户端向源节点发送关于键key的命令,源节点先在自己的数据库里查找这个键,如果找到就直接返回执行客户端命令,如果没找到,这个键可能已经被迁移到了目标节点,源节点向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点,并再次发送之前要执行的命令 |
复制与故障转移 |
Redis集群分为主节点与从节点,主节点处理槽,从节点负责复制某节点 |
故障检测 |
集群中的每个节点都会定期地想集群中的其他节点发送 PING 消息,以此来检测对方是否在线 |
故障转移 |
当一个从节点发现自己正在复制的主节点进入了已下线状态,从节点将开始对下线主节点进行故障转移:被选中的从节点将执行 slaveof no one 命令,成为新的主节点,并将这些槽全部指派给自己,新的主节点向集群广播一条PONG消息,告诉集群中的其他节点自己成为了新的主节点 |
redis常见问题
(一)缓存和数据库双写一致性问题
(二)缓存雪崩问题
(三)缓存击穿问题
(四)缓存的并发竞争问题
网友评论