文章目录
AFO
AOF日志持久化
先执行写操作,成功之后才写入日志到硬盘
好处:避免额外的检测开销,不会阻塞当前写操作
风险:可能会丢失,可能会阻塞下一个写操作
redis写回操作流程:先写入aof_buf缓冲区,然后调用系统IO转入系统内核写入到系统内核缓冲区,再由内核决定何时写回磁盘
对于写回时机有三个级别:always、everysec、no
三种写回策略由调用fsync()函数的调用时机决定,always是每次写入都调用,everysec是每秒调用,no是从不调用,交给系统来决定
AOF重写机制
当AOF文件满了之后,会启动AOF重写机制,会先写入到一个新的AOF文件,写完之后再对老文件删除,AOF的重写机制一般是在redis后台创建一个新的新的进程来完成。
好处:不会阻塞主线程的写入操作
在重写的时候,如果主进程写入了数据则会同时写入AOF重写缓冲区和AOF缓冲区,在子进程完成重写后将AOF重写缓冲区中的命令追加到新的AOF文件中去
Redis的数据结构
redis用来保存所有键值对的结构,void* key和void* value都是指的对象结构
数据类型和其对应的数据结构
新增:BitMap、HyperLogLog、GEO、Stream
SDS
对于C语言字符串的改进
优点:O(1)的时间复杂度得到长度,可以存储’\0‘,具有二进制安全,不会发生缓冲溢出(缓存不够时,小于1MB翻倍扩容,大于1MB则按1MB往上扩容),flags关键字能够灵活的分配不同大小的数据,节省内存,并且在结构体中取消了编译器默认的优化对齐,而是按实际占用字节对齐
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len;
uint16_t alloc;
unsigned char flags;
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len;
uint32_t alloc;
unsigned char flags;
char buf[];
};
List
但是这种链表有个缺陷就是内存不连续,会出现碎片,而且每个链表都要存储一个头节点,内存的开销比较大
typedef struct listNode {
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
} listNode;
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;
压缩链表
为了解决会出现碎片而设计出来的一种链表,但是它会导致一个很大的问题就是连锁更新:压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。
所以压缩列表一般只在数据量很小的时候使用
quicklist
双向列表和压缩列表的组合,与双向列表的区别就是把listnode进行了改变,只能尽量规避说连锁更新,无法解决
typedef struct quicklistNode {
//前一个quicklistNode
struct quicklistNode *prev; //前一个quicklistNode
//下一个quicklistNode
struct quicklistNode *next; //后一个quicklistNode
//quicklistNode指向的压缩列表
unsigned char *zl;
//压缩列表的的字节大小
unsigned int sz;
//压缩列表的元素个数
unsigned int count : 16; //ziplist中的元素个数
....
} quicklistNode;
listpack
listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。
哈希表
redis中的哈希表采用了链式哈希来避免哈希冲突,还设计了渐进式rehash操作来扩大哈希表:先是分配好一个新的hashtable,然后在rehash操作期间,原表在进行增删改查时,将该索引位置上的所有数据依次转移到新的hashtable中。
当负载因子大于1且没有进行RDB快照或者AOF重写时,进行rehash操作,或者当其大于5时,不管是否RDB或者AOF都rehash
typedef struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemask;
//该哈希表已有的节点数量
unsigned long used;
} dictht;
typedef struct dictEntry {
//键值对中的键
void *key;
//键值对中的值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
//指向下一个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
typedef struct dict {
…
//两个Hash表,交替使用,用于rehash操作
dictht ht[2];
…
} dict;
整数集合
本质就是一块连续的内存空间,允许类型升级,但不允许类型降级
typedef struct intset {
//编码方式INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64
//对应int16_t、int32_t、int64_t。
uint32_t encoding;
//集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
} intset;
跳表
跳表在创建节点的时候,随机生成每个节点的层数,跳表在创建节点时候,会生成范围为[0-1]的一个随机数,如果这个随机数小于 0.25(相当于概率 25%),那么层数就增加 1 层,然后继续生成下一个随机数,直到随机数的结果大于 0.25 结束,最终确定该节点的层数。
RDB快照
redis中的RDB快照是全量快照,即把内存中的所有数据全部复制一份到磁盘当中去,如果太过频繁会带来大量的性能损耗,过低则有风险数据丢失
改进版本就是RDB和AOF混合使用:即AOF文件的前半部分是RDB快照,而后面部分是AOF格式的增量数据
当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
Redis主从复制
主从复制即把主服务器的数据备份一份到从属服务器中去,psync中包含两个数据主服务器runID和复制进度offset,在发送RDB快照过程中执行的写操作会写入到replication buffer缓冲区中,在第一次连接之后会一直保持一个TCP连接,称之为基于长连接的命令传播
当服务器数量增多之后可以将从属服务器变为新一级别的主服务器也可以进行写操作传播
增量复制
如果命令传播突然断开之后再连接上,将执行增量复制offset会被置为-1,在repl_backlog_buffer中有两个数master_repl_offset标志自己写到的位置,slave_repl_offset标志从服务器读到的位置,如果数据在repl_backlog_buffer中则增量复制,否则全量复制
缓存雪崩、击穿、穿透
缓存雪崩
过期解决方法:均匀设置过期时间(在过期时间上加上一个随机数)、互斥锁(发现数据不在时,对数据加锁,保证只有一个来请求,并对锁设置一个超时时间)、双key策略(一个key设置过期时间,一个key不设置)、后台定期更新数据间接使数据永久化(一种是后台检测,一种是到期了就消息队列发送消息给数据库)
redis故障解决方法:服务器熔断(暂停业务直接返回错误,或者请求限流,只处理一部分其他的直接返回错误)、构建可靠的集群
缓存击穿
一般来说解决方法是将热点数据不设置过期时间,后台定时更新,或者使用互斥锁
缓存穿透
一般来说三种情况
非法请求限制:在API接口处就进行处理,直接返回错误避免访问缓存和数据库
缓存空值或者默认值:设置默认值或者空值
使用布隆过滤器直接快速判断是否存在
缓存一致性
Cache Aside策略
「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。还可以给缓存设置一个很短的过期时间来兜底
当删除缓存操作失败时
采用重试机制,引入消息队列,将删除操作加入到消息队列中,然后多次重试删除,如果多次重试失败向业务层发消息
过期删除策略
定时删除:在设置过期时间时,设置一个定时时间,时间到达后,事件处理器自动执行key的删除操作
惰性删除:不主动删除过期key,当进行访问时,发现过期才进行key的删除
定期删除:每隔一段时间,随机选出一些key进行检查,删除其中过期的key
Redis采用惰性删除+定期删除,Redis的定期删除时,如果需要删除的比例超过设定的比重,则会重复该过程,如果不超过等待下一次操作
内存淘汰策略
不进行数据淘汰
noeviction:超过最大内存,不淘汰数据,直接不提供服务
进行数据淘汰
过期数据中选择数据
volatile-random:随机选取一些设置了过期时间的key
volatile-ttl:过期时间最长的淘汰
volatile-lru:所有过期时间key中,过期最久的淘汰
volatile-lfu:过期中,使用最少的淘汰
所有数据中选择数据
allkeys-random:所有数据随机淘汰
allkeys-lru:最久未使用的key
allkeys-lfu:最少使用次数的key
Redis线程
Redis不是单线程,Redis除了接受请求,解析请求,数据读写,返回请求这个主线程之外,还会开启两个后台线程,分别处理关闭文件和AOF刷盘,4.0版本后还有一个lazyfree线程来释放内存,这三个线程都有自己对应的任务队列
单线程模式:epoll初始化,然后主线程进入事件循环函数,首先处理发送数据,然后连接、读事件、写事件
Redis为什么快:Redis操作大部分在内存完成,单线程模型避免多线程竞争,采用多路IO复用
所有数据中选择数据
allkeys-random:所有数据随机淘汰
allkeys-lru:最久未使用的key
allkeys-lfu:最少使用次数的key
Redis线程
Redis不是单线程,Redis除了接受请求,解析请求,数据读写,返回请求这个主线程之外,还会开启两个后台线程,分别处理关闭文件和AOF刷盘,4.0版本后还有一个lazyfree线程来释放内存,这三个线程都有自己对应的任务队列
单线程模式:epoll初始化,然后主线程进入事件循环函数,首先处理发送数据,然后连接、读事件、写事件
Redis为什么快:Redis操作大部分在内存完成,单线程模型避免多线程竞争,采用多路IO复用
Redis6.0后使用的多线程也是用来处理网络请求连接的多线程I/O,对于读写I/O还是单线程
————————————————
参考资料:
https://blog.csdn.net/qq_34827674/article/details/117319951
https://blog.csdn.net/qq_34827674/article/details/123448691
https://blog.csdn.net/qq_34827674/article/details/121654479
https://blog.csdn.net/qq_34827674/article/details/123463175
https://blog.csdn.net/qq_34827674/article/details/123866483
https://blog.csdn.net/qq_34827674/article/details/125593574
https://blog.csdn.net/qq_34827674/article/details/125847274
————————————————