Redis(一):Redis为什么高性能?

681 阅读22分钟

Redis为什么这么快?

  1. Redis是单线程的,避免了多线程的上下文切换和并发控制开销;

  2. Redis大部分操作时基于内存,读写数据不需要磁盘I/O,所以速度非常快;

  3. Redis采用了I/O多路复用机制,提高了网络I/O并发性;

  4. Redis提供高效的数据结构,如跳跃表、哈希表等;

基础数据结构

SDS

Redis的简单动态字符串SDS是可变的,遵循C字符串以1字节空字符结尾,最大长度为512M。

SDS为什么使用1字节空字符结尾呢? 使用1字节空字符结尾可重用C字符串的部分函数。

结构定义

SDS底层使用一个字节数组保存字符串内容,通过len属性可O(1)的复杂度获取字符串长度。

struct sdshdr{
	//字符串长度,即buf[]已使用字节数
	int len;
	//buf[]剩余字节数
	int free;
	//字节数组,用于保存字符串内容
	char buf[];
};

在这里插入图片描述

内存分配策略

SDS采用空间预分配和惰性空间释放来优化SDS的内存分配次数(n次 → 最多n次)。

  1. 空间预分配

空间预分配用于优化字符串的增长操作。当修改SDS需要扩展内存空间时,不仅会分配所需的空间,还会根据len属性分配额外的未使用空间。

  • 如果修改SDS后,len < 1MB,将分配和len同样大小的未使用空间;
  • 如果修改SDS后,len > 1MB,将分配1MB的未使用空间;
  1. 惰性空间释放

惰性空间释放用于优化SDS缩短操作。当缩短SDS时,程序不立即回收未使用的空间,使用free记录未使用空间长度,等待将来使用。(也可调用函数手动释放空间)

SDS和C字符串的区别

  1. 获取字符串长度
  • C字符串需要遍历整个字符串计数统计长度,时间复杂度为O(n);
  • SDS只需要获取sdshdr.len即可,时间复杂度为O(1);
  1. 缓冲区溢出
  • C字符串不记录自身长度,在进行修改时如果没有分配足够的内存可能造成缓冲区溢出;
  • SDS在修改时会先根据sdshdr.free属性校验内存是否足够,如果不够会先进行扩容,再执行修改操作;
  1. 二进制安全
  • C字符串除末尾之外不能包含空字符,否则最先被读入的空字符会被误认为是字符串结尾;(所以C字符串只能保存文本数据,不能用于保存二进制数据)
  • SDS通过sdshdr.len判断字符串是否结束,可以用于保存二进制数据。
  1. 内存分配次数
  • C字符串每次修改操作都需要进行内存重分配;
  • SDS需要最多n次内存重分配;

链表

特点

  1. 双向链表:获取某个节点的前驱节点和后继节点复杂度O(1);
  2. 无环:头节点前驱指针和尾节点后继指针指向NULL;
  3. 插入和删除快,时间复杂度O(1);查找慢,时间复杂度O(n);

结构定义

节点定义:

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;

在这里插入图片描述

字典

特点

字典用于保存键值对,包含的每个键都是唯一的。

结构定义

字典ht[2]是一个包含两个哈希表的数组,一般情况下只使用ht[0],只有在rehash时会使用ht[1]。dict.rehashidx记录目前rehash进度。

typedef struct dict {
    //特定类型操作函数
    dictType *type;
    //私有数据(传给特定类型操作函数的参数)
    void *privdata;
    //哈希表
    dictht ht[2];
    //rehash索引,没有在进行rehash时rehashidx=-1
    long rehashidx;   
} dict;

在这里插入图片描述

哈希表底层基于一个dictEntry数组实现,每一项保存一个键值对,哈希到同一个数组项的节点通过next连接。

typedef struct dictht {
    //哈希表数组
    dictEntry **table;
    //哈希表大小
    unsigned long size;
    //哈希表大小掩码,用于哈希函数计算索引值,sizemask=size-1
    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;

在这里插入图片描述

哈希过程

  1. 根据哈希函数计算键值对键的哈希值;
  2. 根据哈希值对哈希表掩码dictht.sizemask取模计算索引值;
  3. 根据索引值将键值对放入哈希表数组的对应索引位置上;

如何解决哈希冲突? 使用链地址法解决哈希冲突。每个哈希表节点dictEntry都保存一个next指针,得分配到同一个数组项的节点通过next指针连接成一个单向链表。

rehash过程

  1. 为ht[1]分配空间:如果是扩容操作,ht[1]大小等于第一个≥ht[0].used*2的2n;如果是收缩操作,ht[1]大小等于第一个≥ht[0].used的2n;
  2. 将ht[0]上的所有键值对rehash(重新计算键的哈希值和索引值)后放入ht[1];——渐进式rehash:rehash期间每次对字典执行增/删/改/查操作都会将ht[0]在rehashidx索引上的所有键值对rehash到ht[1],rehashidx++;
  3. 当ht[0]上的所有键值对都迁移到ht[1]后,释放ht[0],将ht[1]设置为ht[0],新建一个ht[1]为下次rehash做准备;

1. 什么时候会触发rehash? 根据哈希表的负载因子(dictht.used/dictht.size),在哈希表过大或过小时会触发rehash:

  1. 当服务器没有在执行BGSAVE/BGWRITEAOF命令时,哈希表的负载因子≥1;
  2. 当服务器正在执行BGSAVE/BGWRITEAOF命令时,哈希表的负载因子≥5;

2. 渐进式rehash的好处 将rehash操作分摊到字典的每个增/删/改/查操作上,避免集中rehash带来庞大的计算量而导致服务器停顿。

3. rehash过程中的查找/插入操作

  1. 查找:在rehash过程中会同时使用ht[0]和ht[1],如果要查找某个key,会先在ht[0]中查找,如果没找到,继续在ht[1]中查找。
  2. 插入:在rehash过程中新增的键值对会被保存到ht[1],保证了ht[0]的键值对数量只减不增。

跳跃表

特点

跳跃表基于分值从小到大排序,查找的过程近似二分查找。

结构定义

跳跃表基于有序链表实现,通过在链表的基础上增加多级索引提升查找的效率。跳跃表每一层都是一个链表,最底层链表包含所有元素,链表的每个节点包含2个指针,一个forward指针指向同一链表中该节点的下一个节点,一个backward指向同一链表中该节点的前一个节点。

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;

在这里插入图片描述

操作过程

  1. 查找:从当前最高的level开始,向前查找,如果当前节点的score小于插入节点的score,继续查找下一个节点;反之,则往下一层继续查找(类似于二分查找)。 在这里插入图片描述

  2. 插入:从当前最高的level开始,向前查找,如果当前节点的score小于插入节点的score,继续查找下一个节点;反之,则往下一层继续查找,直到第一层为止。跳跃表插入操作的时间复杂度是O(logN)。

  3. 删除:自上而下查找第一次出现该节点的索引,并逐层找到每一层对应的节点。删除每一层查找到的节点,如果该层只剩下1个节点,删除整个一层(原链表除外)。跳跃表删除操作的时间复杂度是O(logN)。

整数集合

特点

整数集合可以去重且有序的保存整数值。

整数集合通过二分查找法获取元素查找和插入位置定位。

结构定义

整数集合可以保存16位、32位、64位的整数,集合中整数的类型取决于encoding属性。

typedef struct intset {
    //编码方式:int16_t、int32_t、int64_t
    uint32_t encoding;
    //元素数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
} intset;

在这里插入图片描述

类型升级

当插入的新元素的类型比整数集合现有的元素类型长,会触发类型升级。

类型升级的过程:

  1. 根据新元素类型扩展整数集合底层数组intset.contents空间大小,并为新元素分配空间;
  2. 将现有的元素转换成与新元素相同的类型,并有序放到数组正确的位置;
  3. 将新元素添加到数组;

整数集合通过类型升级操作提升了集合的灵活性,不用担心出现类型错误,而且升级操作只会在需要的时候进行,节约内存。

整数集合不支持类型降级操作。

压缩列表

特点

压缩列表是顺序存储的数据结构,为了节约内存而开发的。

结构定义

一个压缩列表可以包含任意个节点,每个节点可以包含一个字节数组或一个整数值。

在这里插入图片描述

  1. zlbytes:记录压缩列表大小;
  2. zltail:尾节点偏移量(通过zltail可以确定尾节点位置);
  3. zllen:记录压缩列表节点数量;
  4. entry:压缩列表节点;
  5. zlend:标记压缩列表末端;

在这里插入图片描述

  1. previour_entry_length:记录前一个节点所占字节数(可以根据当前节点起始地址+previour_entry_length计算前一个节点的起始地址,实现从表尾向表头遍历);
  • 如果前一个节点<254字节,previour_entry_length=1字节;
  • 如果前一个节点>254字节,previour_entry_length≥5字节;
  1. encoding:content数据类型和长度;
  2. content:节点值,可以是一个字节数组或整数值;

连锁更新

压缩列表的每个节点previour_entry_length属性都记录了前一个节点的长度,前一个节点大小变化(增/删/改)可能会导致previour_entry_length需要进行内存重分配(1字节→5字节或5字节→1字节),可能产生连锁反应,导致多个节点的previour_entry_length都需要进行内存重分配。

基础数据类型

Redis使用基本数据结构实现了对象系统,包含字符串对象、列表对象、哈希对象、集合对象、有序集合对象五种类型。可以根据使用场景选择对象的编码,提高Redis的灵活性和效率。

typedef struct redisObject {
    //对象类型(字符串、列表、哈希表、集合、有序集合)
    unsigned type:4;
    //对象编码
    unsigned encoding:4;
	//记录对象最后一次被访问的时间,用于计算对象的空转时长
    unsigned lru:REDIS_LRU_BITS; 
	//对象引用计数,用于内存回收和对象共享
    int refcount;
    //指向底层实现数据结构
    void *ptr;
} robj;

在执行命令时,Redis根据对象类型判断该对象是否能执行执行的命令,通过获取redisObject.type实现。

字符串(string)

Redis字符串是可变的,基于SDS实现。

在Redis中,long、double类型的浮点数会先转为字符串再进行保存,读取时再将字符串转为浮点数。

编码

  1. int:long型整数;
  2. raw:长度>32字节的字符串;
  3. embstr:长度<32字节的字符串,只读;

raw编码会调用2次内存分配函数来创建redisObject结构和sdshdr结构,embstr编码只调用一次内存分配函数分配一块连续的内存空间,空间中一次包含redisObject结构和sdshdr结构。 在这里插入图片描述

编码转换

  1. int编码的字符串保存的不再是整数值,int → raw;
  2. 修改embstr编码的字符串,embstr → raw;

命令

127.0.0.1:6379> set name1 java	# 添加
OK
127.0.0.1:6379> mset name2 c name3 go	# 批量添加
OK
127.0.0.1:6379> keys *	# 获取所有key
1) "name3"
2) "name2"
3) "name1"
127.0.0.1:6379> mget name1 name3	# 批量获取
1) "java"
2) "go"
127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> incr a	# 自增1
(integer) 2
127.0.0.1:6379> incrby a 10	# 自增10
(integer) 12
127.0.0.1:6379> decr a	# 自减1
(integer) 11
127.0.0.1:6379> decrby a 5	# 自减5
(integer) 6

列表(list)

编码

  1. ziplist:使用压缩列表实现,每个节点保存一个列表元素;

在这里插入图片描述

  1. linkedlist:使用双向链表实现,每个节点保存一个字符串类型的列表元素;

在这里插入图片描述

编码转换

列表所有字符串元素长度<64字节且列表包含的元素数量<512个时使用ziplist编码,否则使用linkedlist编码。

命令

127.0.0.1:6379> rpush books java c go python	添加
(integer) 4
127.0.0.1:6379> llen books	# 获取长度
(integer) 4
127.0.0.1:6379> lpop books	# 出队(先进先出)
"java"
127.0.0.1:6379> rpop books	# 出栈(后进先出)
"python"
127.0.0.1:6379> lrange books 0 -1	# 获取所有元素
1) "c"
2) "go"
127.0.0.1:6379> lindex books 1	# 获取索引为1的元素
"go"

哈希表(hash)

编码

  1. ziplist:使用压缩列表实现,同一个键值对的两个节点紧挨在一起,键在前,值在后;

在这里插入图片描述

  1. hashtable:使用字典实现;

在这里插入图片描述

编码转换

哈希表所有键和值得字符串长度<64字节且哈希表包含的键值对数量<512个时使用ziplist编码,否则使用hashtable编码。

命令

127.0.0.1:6379> hset books book1 java	# 添加
(integer) 1
127.0.0.1:6379> hmset books book2 go book3 c	# 批量添加
OK
127.0.0.1:6379> hgetall books	# 获取所有键值对
1) "book1"
2) "java"
3) "book2"
4) "go"
5) "book3"
6) "c"
127.0.0.1:6379> hlen books	# 获取长度
(integer) 3

集合(set)

编码

  1. intset:使用整数集合实现;

在这里插入图片描述

  1. hashtable:使用字典实现,每个键都是字符串类型的集合元素,值为NULL;

在这里插入图片描述

编码转换

集合所有元素都是整数值且集合包含的元素数量<512个时使用intset编码,否则使用hashtable编码。

命令

127.0.0.1:6379> sadd books c	# 添加
(integer) 1
127.0.0.1:6379> sadd books java go python	# 批量添加
(integer) 3
127.0.0.1:6379> smembers books	# 获取所有元素(无序,并不会和插入顺序一致)
1) "java"
2) "python"
3) "go"
4) "c"
127.0.0.1:6379> sismember books html	# 查询某个元素是否存在
(integer) 0
127.0.0.1:6379> sismember books java
(integer) 1
127.0.0.1:6379> scard books	# 获取长度
(integer) 4
127.0.0.1:6379> spop books	# 弹出一个元素
"java"

有序集合(zset)

编码

  1. ziplist:使用压缩列表,每个集合元素使用两个紧挨在一起的节点保存,第一个节点保存元素,第二个节点保存元素的分值;

在这里插入图片描述

  1. skiplist:使用zset结构实现,一个zset结构包含一个跳跃表和一个字典,跳跃表和字典中的元素通过指针共享对象。
  • 跳跃表按分值从小到大保存所有元素,通过跳跃表可以对有序集合进行范围操作;
  • 字典实现对象到分值的映射,字典中的每个键值对都保存了一个集合元素,实现O(1)复杂度查找对象分值;

在这里插入图片描述

编码转换

有序集合所有元素长度<54字节且包含的元素数量<128个时使用ziplist编码,否则使用skpilist编码。

命令

127.0.0.1:6379> zadd books 5 java	# 添加
(integer) 1
127.0.0.1:6379> zadd books 2 c
(integer) 1
127.0.0.1:6379> zadd books 6 go
(integer) 1
127.0.0.1:6379> zadd books 9 html
(integer) 1
127.0.0.1:6379> zrange books 0 10	# 获取正序排名[0,10]的元素
1) "c"
2) "java"
3) "go"
4) "html"
127.0.0.1:6379> zrange books 2 3	# 获取正序排名[2,3]的元素
1) "go"
2) "html"
127.0.0.1:6379> zrevrange books 0 -1	# 获取逆序排名的所有元素
1) "html"
2) "go"
3) "java"
4) "c"
127.0.0.1:6379> zcard books		#获取长度
(integer) 4
127.0.0.1:6379> zscore books java	# 获取指定元素的分值
"5"
127.0.0.1:6379> zrangebyscore books 3 7		#获取分值[3,7]的元素
1) "java"
2) "go"
127.0.0.1:6379> zrem books c	# 删除元素
(integer) 1
127.0.0.1:6379> zrange books 0 -1	# 获取所有元素
1) "java"
2) "go"
3) "html"

总结

特殊数据类型

Geo

Redis 3.2版本以后,提供了基于geohash和有序集合实现地理位置相关功能的Geo模块。

实现原理

Geo基于geohash和有序集合实现,使用有序集合保存位置对象,分值为位置对象经纬度对应的52为geohash值。

命令

127.0.0.1:6379> geoadd users:locations 120.346111 31.556381 user1 120.375821 31.5603682 user2	# 添加地理位置信息
(integer) 2
127.0.0.1:6379> geopos users:locations user1	# 获取user1地理位置
1) 1) "120.34611314535140991"
   2) "31.55637987511895659"
127.0.0.1:6379> geodist users:locations user1 user2 m	# 计算user1和user2位置距离(m:距离以米为单位)
"2850.3519"
127.0.0.1:6379> georadius users:locations 120.375821 31.556381 5 km withcoord withdist withhash asc count 100	#获取指定范围5km范围以内最多100个元素(asc:按距离从近到远排序,desc:按距离从远到近排序,count 100:最多返回100个元素)
1) 1) "user2"
   2) "0.4433"	# withdist:距离目标位置的距离
   3) (integer) 4054421167795118
   4) 1) "120.37582129240036011"	# withcoord :元素的经纬度
      2) "31.5603669915025975"
2) 1) "user1"
   2) "2.8157"
   3) (integer) 4054421060663027
   4) 1) "120.34611314535140991"
      2) "31.55637987511895659"
127.0.0.1:6379> georadiusbymember users:locations user1 5 km withcoord withdist withhash asc count 100	# 获取指定元素5km范围以内最多100个元素
1) 1) "user1"
   2) "0.0000"
   3) (integer) 4054421060663027
   4) 1) "120.34611314535140991"
      2) "31.55637987511895659"
2) 1) "user2"
   2) "2.8504"
   3) (integer) 4054421167795118
   4) 1) "120.37582129240036011"
      2) "31.5603669915025975"

应用场景

  1. 查找附近的人;

HyperLogLog

HyperLogLog是一种概率数据结构,提供不精确的去重计数方案,标准误差是0.81%。

HyperLogLog和set的区别

  1. set存储统计元素,所以元素较多时需要占据大量内存空间;
  2. HyperLogLog不存储统计元素,仅存储存在的标记,最多只需要12K内存空间;

命令

127.0.0.1:6379> pfadd users1 user1 user3 user5 user7	# 添加
(integer) 1
127.0.0.1:6379> pfcount users1	# 统计users1	
(integer) 4
127.0.0.1:6379> pfadd users2 user1 user2 user4
(integer) 1
127.0.0.1:6379> pfcount users2
(integer) 3
127.0.0.1:6379> pfmerge users1 users2	# 将users1和users2合并累加计数,形成新的users1
OK
127.0.0.1:6379> pfcount users1
(integer) 6

应用场景

  1. 统计日/月活用户:每天使用一个HyperLogLog记录日活,使用用户id作为标识,合并当月每天HyperLogLog即可得到月活;
  2. 统计网页访客UV:使用cookie作为标识,相同的客户端访问最多只计数1次;

布隆过滤器

布隆过滤器是一种数据结构。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。

实现原理

布隆过滤器基于一个位数组+多个哈希函数实现。

  1. 插入 向布隆过滤器中添加key时,首先使用多个哈希函数对key进行hash得到一个哈希值,然后再将各个哈希值对位数组长度进行取模运算得到索引值,每个hash函数都会算得一个不同的索引值,再将位数组对应索引位置都置为1。
  2. 查找 向布隆过滤器查找某个key是否存在时,首先使用多个哈希函数对key进行hash得到一个哈希值,然后再将各个哈希值对位数组长度进行取模运算得到索引值,每个hash函数都会算得一个不同的索引值,判断位数组中这几个索引位置的值是否都为 1,只要有一个位为 0,那么说明这个key不存在,如果都为1,并不说明这个key就一定存在,只是极有可能存在,因为这些位被置为1可能是因为其它的key存在所致。

应用场景

1.过滤垃圾邮件; 2.URL去重; 3.防止缓存穿透;

持久化

Redis的持久化方式主要有2种:RDB持久化和AOF持久化。

RDB持久化

RDB持久化可手动执行,也可根据服务器定期执行bgsave。

实现原理

RDB持久化将某个时间点上的数据保存到RDB文件中(快照),再将RDB文件保存到磁盘。在服务器启动时,可以通过载入RDB文件还原数据。

服务器启动时会自动执行RDB文件的载入(RDB文件存在且未开启AOF功能),载入过程中服务器处于阻塞等待状态。

可以通过save/bgsave命令执行RDB持久化操作。

save命令

执行save命令会使Redis服务器进程会阻塞等待直到RDB文件创建完毕,在服务器进程阻塞期间,服务器不能处理任何命令请求。

bgsave命令

bgsave命令会派生出一个Redis服务器进程的子进程,由子进程负责创建RDB文件,当RDB创建完成后,子进程向父进程发送信息,服务器进程在此期间可以继续处理命令请求并轮询等待子进程的信号。

在这里插入图片描述

1. 为什么bgsave要使用子进程而不是用线程? 利用操作系统的写时复制机制,避免不必要的内存拷贝。

2. 自动执行bgsave 服务器会根据配置的save选项(保存条件)定期执行bgsave,save选项的内容以【秒数-修改数】格式保存在redisServer.saveparam数组中。每次调用serverCron()函数时会检查save选项保存的条件是否满足(满足save选项的任一条件),如果满足就执行bgsave命令。

  • 根据redisServer.dirty(上一次成功执行save/bgsave命令后服务器数据执行了多少次修改)获取修改次数;
  • 根据redisServer.lastsave(上一次成功执行save/bgsave命令的时间)获取间隔时间。

RDB文件结构

在这里插入图片描述

  1. REDIS:RDB文件的开头是一个5字节的“REDIS”字符串,在服务器启动载入文件时校验是否为RDB文件;
  2. db_version:4字节的字符串整数,记录RDB文件的版本号,如006代表RDB文件的版本为第6版;
  3. database:各个数据库保存的键值对数据;

  • SELECTDB:1字节,标记接下来要读取的是数据库号码;
  • db_number:数据库号码,读入db_number后会执行select命令切换数据库;
  • key_value_pairs:数据库中所有键值对,未过期键会记录键类型、键、值,过期键会额外保存过期键标志、过期时间;
  1. EOF:1字节,标志RDB文件正文内容结束。
  2. check_sum:8字节无符号整数的校验和,通过REDIS+db_version+database+EOF计算得出,在载入RDB文件时用于校验RDB文件是否损坏。

AOF持久化

AOF持久化以Redis命令请求协议的格式(纯文本)保存服务器执行的写命令,在服务器启动时,可以通过执行AOF文件中的写命令还原数据。

执行过程

  1. 命令追加:服务器每执行完一个写命令,会将写命令追加到redisServer.aof_buf缓冲区(字符串)末尾;
  2. 文件写入:每次执行serverCron()函数时将redisServer.aof_buf缓冲区中的数据写入AOF文件中;
  3. 文件同步:根据服务器配置的持久化行为判断是否需要AOF文件刷入磁盘;
  • always:将redisServer.aof_buf缓冲区中所有数据写入AOF文件并将AOF文件刷入磁盘。最慢,但是安全性最高;
  • everysec:默认值,将redisServer.aof_buf缓冲区中所有数据写入AOF文件,如果距离上一次将AOF文件刷入磁盘时间超过1s,则再次将AOF文件刷入磁盘。较快,但是出现故障会丢失1s的命令数据;
  • no:将redisServer.aof_buf缓冲区中所有数据写入AOF文件,由操作系统决定何时将AOF文件刷入磁盘。最快,但是出现故障会丢失上次AOF文件同步之后的数据;

在操作系统中,write()系统调用先将写入的数据保存在内核缓冲区中,再刷入磁盘。

如何载入AOF文件

服务器启动载入AOF文件时,只需要将AOF文件中的所有命令执行一遍就可以还原数据。

  1. 创建一个不带网络连接的伪客户端(Redis命令只能在客户端上下文中执行);
  2. 通过伪客户端执行AOF文件中的命令;

AOF重写

服务器运行的越久,AOF文件中的内容将会越多,文件体积越来越大,使用AOF文件还原数据耗时也就越长,通过AOF重写能解决AOF文件膨胀的问题。

什么是AOF重写?

Redis会创建一个新的AOF文件替代现有的AOF文件,新旧两个AOF文件保存的数据相同,但是新的AOF文件中不包含冗余命令,所以新的AOF文件通常会比旧的AOF文件体积小很多。

AOF重写过程

AOF重写不需要操作当前AOF文件。

  1. rewriteaof命令会派生出一个Redis服务器进程的子进程,由子进程负责读取服务器当前数据,并为每个键值对生成一条命令写入新的AOF文件中;
  2. 服务器会将在子进程重写期间执行的写命令保存到AOF重写缓冲区redisServer.aof_rewrite_buf_blocks中,也就是在AOF重写期间服务器每执行一个写命令,都需要保存进AOF缓冲区和AOF重写缓冲区;
  3. 当子进程完成AOF重写后,向父进程发送一个信号;
  4. 父进程接收到子进程的信号,将AOF重写缓冲区取中的所有内容添加到新的AOF文件中,并对新的AOF文件改名,覆盖现有的AOF文件;(这个过程父进程是阻塞不接收客户端请求的)

比较AOF和RDB

  1. RDB保存某一刻的数据快照,全量备份;
  2. RDB的bgsave命令通过多进程的写时复制机制来实现持久化;
  3. AOF通过缓冲区保存写命令后再写入文件,增量备份;
  4. AOF更新频率高于RDB,如果服务器开启了AOF,会优先使用AOF文件还原数据;