Redis 原理详解

写在前面

redis 通常被用于做数据缓存,一些场景下,甚至可以直接使用 redis 作为存储,相信随着内存价格的进一步降低,以及redis 稳定性的提升,越来越多的场景会使用redis。

性能

QPS 大概几万 QPS

应用注意

大key问题

参考

rename 指令

rename 效率问题

hash tag

解决集群场景下 rename 或者集合操作时,两个 key 可以分布在同一个 slot 上。
如:{user}:aaa与{user}:bbb 这两个集合可以确保在同一个slot上。reference

基础

第一章 字符串

 书籍的第一章永远都是被读的最多的。
 redis 的字符串和C相比有很多的提升,在安全性和性能上都有很大的进步,但是由于会预先分配空间,可能会造成一定的空间浪费。(sds simple dynamic string)

struct sdshdr {
	int len;
	int free;
	char buffer[];  	` 
}

上述结构的好处

  • len的定义
    • 使得数组长度在常数时间内返回
    • 避免数组访问越界
    • 二进制安全,解决了C语言不能存储空字符的问题,sds会将每一个字节都当作是字节本身进行存储,不做任何过滤。存进去什么,读出来就是什么。
  • free的定义
    • 预留额外的空间,避免频繁内存分配,策略:小于1M分配当次填充结束后分配和len相同的 free,大于1M 每次会多分配1M
    • 缩短后不会立即释放空间,而是存在free里面等下次使用
  • 使用空字符 \0结尾
    • 可以复用C语言的一些接口

第二章 链表

 第二章被阅读的量,由第一章的长度和难度决定。

 链表用于链表键,发布订阅,慢查询,监视器,保存客户端状态,构建客户端的输出缓冲区,

 redis 的链表是双向链表。

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 listNode {
    struct listNode *prev;
    struct listNode *next;
    void *value;
} listNode;
  • 由于支持多态(可以存储多种数值类型),每个链表要有独立的复制,释放,节点值对比的函数。

第三章 字典

 读到第三章是一个突破。

 字典是通过hash表来实现,因此并没有什么顺序。hash表存储结构

struct dict {
    dictType *type;

    dictEntry **ht_table[2];
    unsigned long ht_used[2];

    long rehashidx; /* rehashing not in progress if rehashidx == -1 */

    /* Keep small vars at end for optimal (minimal) struct padding */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
    signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};

typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;            // int double 直接存储。存储指针多一层浪费
    } v;
    struct dictEntry *next;     /* Next entry in the same hash bucket. */
    void *metadata[];           /* An arbitrary number of bytes (starting at a
                                 * pointer-aligned address) of size as returned
                                 * by dictType's dictEntryMetadataBytes(). */
} dictEntry;

 hash存储的过程是

  • 先将key通过哈希函数计算出应该存储的位置,冲突使用链表解决。
  • 如果未进行BGSAVE或者BGREWRITEAOF,那么负载因子 used/size 大于1 时,rehash,
  • 如果进行BGSAVE或者BGREWRITEAOF,则负载因子在5的时候rehash,因为进行上述操作会建立子进程,而由于OS的写时复制,会导致内存占用的增加。
  • rehash是一个渐进的过程,不会立即完成,rehashIndex 会随着每次增删改查而前进一个,即将旧hash中的数值向新的表中移动一个,直到移动完。rehash开始的时候,将trehashidx置为0,然后增加,直到把所有的数据跑完,然后在置为-1。

第四章 跳跃表

 当你能够坚持到第四章的时候,你就有将书看完的希望。但是如果计划不好,你就会越来越焦虑。

 每个节点中维持多个指向其他节点的指针。从而达到快速访问节点的目的。
 主要用在有序结合里面。具有接近于平衡树的性能,实现更为简单的,。

typedef struct zskiplistnode {
	robj *obj;
	double score;
	zskiplistnode* backward;   // 指向前向节点
	struct zskiplistlevel {
		zskiplistnode * forward;  //前进指针,访问表尾方向的节点
		unsigned int span;  //记录前进指针节点与当前节点的距离
	} level[];
} skiplistnode;

查找过程:

  • 先根据 level 数组中最顶层的节点(索引大的,对应的 score 也大),判断如果target 更大,则移动到顶层节点;否则判断下一个 level。

插入过程:

  • 随机生成一个1-32的数作为level数组的大小。越大的数出现的概率越小(幂次定律)。
  • 用于新节点插入会打断前向节点到后面节点的 level 链接,因此要修改对应的前向节点的数据
  • 由于节点是从头开始查找的,因此不会存在生成节点 level 过高,导致找不到前向节点的问题。

遍历

  • level 数组的 0 号元素就是后续节点。按照 level[0] 遍历相当于一个数组。

跳转的距离,用来排序,当搜索某一个节点时,将顺序访问的跳跃数加起来,就可以得到节点的排名。

typedef struct zskiplist {
	zskiplistnode * head;
	zskiplistnode * tail;
	unsigned long length;
	int level;   		//表中层数最大的节点的层数。
} zskiplist

跳跃表查找过程:https://blog.csdn.net/yang_yulei/article/details/46275283
跳跃表代码解析:https://blog.csdn.net/u013536232/article/details/105476382

第五章 整数集合

 整型数作为最简单的结构总是得到人们的青睐,但是简单的东西往往不会让人成长。
 整型集合用在元素都为整型,而且元素数量不多的情况下。

typedef struct intset {
	uint32_t encoding;
	uint32_t length;
	uint8_t contents[];
} intset;

encoding 表示contents里面存储的整型数是几位的,从而将其中的数据按照正确的方式解析出来。

升级

整型集合在存储的时候会采用能将当前数据存储起来的最小的整型,当存储的数据大于这个值的时候,就会根据这个新的数值的大小乘以length,分配一块更大的空间,然后按照从小到大的顺序将数据写到新表当中。

降级

当大的元素从表中删除的时候不会降级,因为很难统计表中最大的元素是多大。遍历显然会增加成本。

第六章 压缩列表

 压缩列表,不知道压缩的含义,想必在元素都为小整型,或者短小的字符串的时候,对比hash键和列表(push pop)键采用压缩列表来实现是一个好的选择,

//压缩列表是一种顺序型数据结构,表头后面跟着的就是节点。
typedef struct ziplist {
	uint32_t bytes;      	//列表占用的总的字节数,用于内存的重新分配或者计算列表末端(0xFF)的位置
	uint32_t tail; 			//获取列表最后一个元素的位置(地址)
	uint16_t length; 		//列表中节点的个数
} ziplist;

tydef struct ziplistnode {
	void previous_entry_length;  		//可能是1或者5, 1 在前面节点小于254 的时候,5的第一位为0xfe 后面四位表示前面节点的实际大小
	uint32_t encoding;		//表示数据是什么整型,或者字符串
	uint8_t contents[];		//实际存储的内容
} ziplistnode;

连锁更新

想想当挨着的节点大小都在254附近,其中一个节点前面加了一个长度大于254的节点,导致这个节点的previous_entry_length 增加为5字节,这样也会导致后面的节点的该字段增长,直到最后。叫做连锁更新

第七章 总结章 对象

 前面介绍了基础的数据结构,在实际的使用中,redis将这些数据结构封装成对象,存储实际的数据。

struct redisObject {
 unsigned type:4;
 unsigned encoding:4;
 void * ptr;
 int refcount;
 unsigned lru:22;
 }

其中type有 string,list,hash,set,zset五种,redis中的键都是string类型的,而五种类型的表对应着着五种类型的对象。
这五种类型的底层就是前面所述的数据结构,每种类型可能对应着一种或者多种数据结构,redis会根据实际情况,选择一种,并在需要的时候进行切换。
encoding可能的值有

INT,  EMBSTR, RAW, HT, LINKEDLIST, ZIPLIST, INTSET, SKIPLIST

refcount 记录一个redisObject 的引用数量。使用较多的是redisObject是一个整数对象的时候,redis会让整数对象共享同一块内存,通过增加引用计数的方式保证对于对象的引用。在redis初始化的时候,会初始化0-9999的整数对象。不共享字符串存储的原因是,字符串比较消耗O(N) 的时间。

lru 最后访问时间
获取键对象的空转时长就是通过这个值得到的, 如果设置了maxmemory选项,当内存达到了最大值的时候,达到空转时长的键值就会被释放,释放的含义并不是删除。在AOF还存储着,只是从内存中暂时去除。

string

string can use three encoding methods, int, embstr, raw ,
if the value can store with long int, string will store it with int.
when you save a string and length is less than 39 bytes, then the encoding method will be embstr.
The advantage of embstr:

  • only once memory allocate rather than twice of raw(one for redisObj, one for sdshdr)
  • the redisObj and sdshdr is store in the same memory block, that is good for cache.
  • once memory allocate means once memory destroy.
    但是使用embstr 是一个只读的结构,当对他进行append操作的时候,会转换成raw进行存储。但是对于业务中常用的操作是不怎么使用append的,一般会使用set覆盖掉旧的值,因此问题也不大,
    需要注意的是不可以对使用embstr和raw编码的string使用incr 这样的命令,会报错的,我们能使用string进行自增操作都是因为我们最开始使用的就是int编码的string。

list

list can use two encoding methods, ziplist, linkedlist.
when satisfy the two condition:

  • all member length is less than 64 bytes
  • the length of list is <= 512.
    the list will use ziplist, if not, the list will use or change to linkedlist.

hash

hash can use two encoding methods, ziplist, hashtable.
the condition of using ziplist is same as list, but the member is replaced by the key and value of hash.
如果使用的压缩列表,会先存储 field 到压缩表中,然后紧挨着存储value。

set

set can use two encoding methods, intset and hashtable,
the condition of using intset is

  • all members must be int
  • the number of members is <= 512.
    when use hashtable, the key is member of set, the value is null.

zset

zset can use two encoding methods, ziplist and (hashtable and skiplist)
the condition of using skiplist is:

  • the member <= 64 bytes
  • the num of members is <=128
    在不满足上述条件的时候会使用两种结构来存储:
    hashtable用来存储一个成员到分值的映射,从而可以在O(1)时间内返回成员的分值。跳跃表可以保证在O(logN)的时间内找到元素,同时提供顺序访问元素的能力。单独使用哪一种数据结构都不能实现这样的效率。
    而采用实际数据对象的存储一份,哈希表和跳跃表中都是用引用的方式,使得实际存储量并没有大幅度上升, 因此是一种较好的平衡。

单机数据库

第八章 数据库

 已经开启了第二部分,就意味着有很大希望读完整本,坚持就会进步。

redis服务器

一个redis服务器,可以提供给多个redis客户端访问,对应着一个IP和PORT,在一个redis服务器下,可以创建多个数据库。

struct redisServer {
redisDb * db;  		//数组,保存着所有的数据库
int dbnum;

struct saveparam *saveparams; 	//数组,用于保存RDB持久化的配置参数
long long dirty;				//记录上次bgsave到现在数据的修改
time_t lastsave;		//记录上次修改的时间。

sds aof_buf; 		//AOF持久化缓冲区
}

redis数据库

redis实际存储键值对的地方,可以通过select命令切换数据库。

struct redisDb {
 dict *dict;
 dict *expires;

dict *watched_keys;  //元素为redisClient 的链表,存储监控对应案件的客户端
}

数据库使用字典来存储数据库中的键值对,值为redisObj的指针。
dict 数据库键空间,用于保存数据库的所有键值对。
expires 过期字典。保存数据库中所有键的过期时间。键值对应键空间中的键的指针。值为long long 型的 endTime, 毫秒精度的unix时间戳。

过期键

redis对于过期键的处理

过期键处理有三种方式

  • 定时器,定时删除,但是对于每个键设置一个定时器,不现实。
  • 在访问到该键的时候删除,可能导致不被访问的键一直删除不掉。
  • 定时遍历删除。redis使用的方案是,顺序遍历数据库,随机选择一定数量的键,检查是否过期,过期删除。每次遍历的数据库数量和随机选择键的数量是可以配置的。

实际在访问的时候会先进行惰性删除,在访问的时候如果键存在还会检查下过期时间,决定后续操作。算个双重保障吧。

过期键和redis持久化

RDB持久化,过期键不会存储到RDB文件中。而在读取RDB文件的时候,要区分主服务器读取还是从服务器读取。
主服务器过期键不会读出,也就是在写和读的时候都会检测过期键。
从服务器会直接将RDB中的键全部拷贝出来,数据一致性在主从同步的时候保证。
AOF持久化,写的时候不会检查,而是直接将redis中的键写进去,在发生删除的时候,将删除信息追加到AOF后面。
对AOF进行重写的时候,和RDB类似,会进行过期键检查,不保存那些过期的键。

过期键和主从

只有主服务器会检测并删除过期键,从服务器不会检测过期键。
主服务器发现过期键,删除的时候会通知从服务器,此时从服务器才删除过期键。
如果访问从服务器中已经过期的键值,从服务器也会返回键对应的值,这里有点奇怪,要注意一下。

数据库通知

客户端可以订阅指定操作,如del,hdel等,服务器会在指定操作发生时,向客户端发送消息(通过向指定的IP和端口发送),通知客户端;
客户端也可以订阅指定的键值的操作,服务器会在指定键值发生变动的时候通知客户端。

服务器可以配置那些操作可以被通知,哪些类型的键值被通知,

第九章 RDB 持久化

 总是有一些东西吸引着你,想要拖慢你,比如一些视频,明天再做的想法,休息一会没有关系等。此时最重要的不是竭力去克制,消灭这些想法,而是要确定好目标,给自己安排一些必然要做的事情。
 内存中的数据访问速度更快,但是数据丢失一直是一个无法回避的事情。持久化是一种好的方式,为想要速度而数据完整性不是那么重要的场景而生。RDB持久化,就是将数据库转化成RDB文件存储在硬盘中,以避免因为进程挂掉导致的数据丢失。

RDB的保存

SAVE和BGSAVE

SAVE 在执行过程中会阻塞服务器的其他请求,直到执行完毕。
BGSAVE 在执行过程,不影响服务器的正常写入和读出操作。
两个命令都是调用rdbsave命令,但是后者是在子进程中运行,在运行结束后通知父进程。
BGSAVE 会影响下面命令的执行:

  • SAVE 会直接拒绝该命令的执行,避免同时两个进程调用rdbsave函数
  • BGSAVE 直接拒绝,原因和上面一样
  • BGREWRITEAOF 因为两个指令都会调用IO读写,同时进行不是一个好主意。
自动间隔性保存

redis的save配置用于指定自动间隔保存的条件,如:

save 900 1
save 300 10
save 60 10000

分别表示 900秒发生一次改变,300秒内发生10次改变,60秒发生10000次改变。
使用者配置上述配置后,redis会将其读入到配置数组中,并将配置数组存储在redisServer结构体中

 struct saveparam {
	time_t seconds;
	int changes;
};

同时,redis在运行过程中,会记录上次保存的时间lastsave,和从上次保存开始数据库的修改dirty。
redis服务器会运行一个定时任务serverCron,每隔200ms,检测是否有配置项满足,如果满足则调用 BGSAVE并清空dirty,更新lastsave。

RDB文件的格式

REDISdb_versiondatabasesEOFcheck_sum
54多个字节18
databases表示一个redisServer中,按顺序存入的多个数据库,每个数据库的结构如下(多个数据库按照顺序重复。):
SELECTDBdb_numberkey_value_pairs
11或2或5多个字节
key _value_pairs
EXPIRETIME_MSmsTYPEkeyvalue
181多个字节多个字节
其中TYPE是value的类型指示,可能是 5 种数据结构的具体实现,如字符串、集合、跳跃表、哈希、压缩列表、整数集合,一些实现的存储结构大致相同,如集合和列表。
key 就是对应字符串
如果是无过期时间的,前面没有EXPIRETIME_MS和ms
字符串在RDB中的结构

在上面介绍过,字符串有三种结构,int,embstr,raw。后两者存储在rdb中的结构是相同的。
如果是int形式的字符串,格式如下:
| ENCODING| integer|
|–|–|–|
|1|1或2或4(可能包含long因为前面在讲到string的时候,说明存储使用的long)|
如果是RAW,且没有开启RDB文件压缩功能
| len| string|
|–|–|–|
|1|对应len长度的字符串|
如果是RAW,却开启了RDB文件压缩,且字符串长度大于20个字节

REDIS_RDB_ENC_LZFcompressed_lenorigin_lencompress_string
111多个
common其余类型

同样其余类型也是以字符串类型为基础,如集合类型和列表类型
|len|members|
|–|–|–|–|
|1|多个对象|
每个对象都是一个上面的字符串结构
对于hash由于有key,每个member都是 key | value 两个字符串
对于zset 由于有score,每个member都是一个 member|score 的结构

special 其余类型

对于intset和ziplist,因为是连续存储的,所以可以直接转化为一个字符串,然后将整个列表按照一个字符串来存储。

RDB的加载

首先会检测是否开启了AOF持久化,如果开启了,优先选择AOF,此时不会使用RDB持久化的内容。如果没有开启,则自动加载RDB文件的内容,加载过程服务器是阻塞的,不接受其他请求。

第十章 AOF持久化

 重要的是要越来越好,越来越努力。不要让自己慢慢沉沦下去,向上的路不好走,要一直坚持下去,才能不负过去的时光。
 生活中充满着选择,工作中也是一样,同样的事情,有多种多样的解决方案,遇到问题首先要尽可能多的提供方案,方案多了才能够进行对比,同时要知道少有完美无缺的方案,总是要进行取舍。
 RDB和AOF就是持久化的两种解决方案,可以看到他们的基础出发点对于某一个场景是好的,但是都存在缺点。前者从全局上复制数据库,需要根据配置结合定时检测将新的内容更新到备份文件中,文件结构比较复杂,但是还原效率高。后者采用命令追加的方式,每次操作的数据量小,还原效率偏低。同时也可以看到对于两种方案都有优化。这大概就是从事工程研究的人所必须面对的问题。

Append Only File

每次服务器执行完一个命令后都会调用 flushAppendOnlyFile() ,此函数的行为由appendfsync 的值决定:

  • always 每次执行完一个命令都将aof_buf缓冲区的所有内容写入并同步到AOF文件。
  • everysec 每次都将缓冲区内容写入到AOF文件,但是此次距离上次同步大于1秒才同步
  • no 只写入到AOF文件,但是不同步,同步由操作系统决定。
    写入的含义是写入到文件的内存缓冲区,因为操作系统操作文件也会有一个缓冲区,写入文件实际上是写入到文件在内存中的缓存。同步则是将文件在内存中的缓冲区的内容同步到磁盘

数据还原

通过构造一个没有网络连接的伪客户端,一次读出AOF中的redis命令,并让服务端执行,从而恢复数据。

AOF重写

为了避免随着redis的运行AOF文件越来越大,设置了AOF重写功能。

AOF重写会直接从redis里面读取内容,然后转换成命令,存储在一个新的AOF文件中。读取内容是按照key一次读取的,如果对应的是集合,列表,hash一个key中可能包含很多的子元素的,会根据REDIS_AOF_REWRITE_ITEMS_PER_CMD 中的值(一般为64),将对此key的操作,拆分成多条命令。

AOF后台重写

由于重写会造成服务端的阻塞,所以一般开一个子进程进行重写,因为此时服务器的父进程还要处理请求,为了避免数据不一致(重写过程中,父进程的数据更改,是无法影响到子进程的),会将重写过程中的请求放在AOF重写请求缓冲区中,当重写子进程完成,会向父进程发送信号,此时父进程将重写缓冲区的内容追加到新的AOF文件后面,并在原子时间内,替换旧的AOF文件。完成重写。
这里的AOF重写缓冲区和AOF缓冲区是不同的,后者是为旧的AOF服务的,在重写完成前继续和旧的AOF文件合作,保证数据的完整

第十一章 事件

 不要胡思乱想,事情发生的时候就去解决,保持专注,不要杞人忧天。
 有些事情是周期性的,为周期性的事情做好准备。
Redis的两种事件(redis是事件驱动程序)

  • 文件事件,file event 就是对套接字的连接,可读,可写状态的抽象。实际上redis 服务器和客户端,服务器和其他服务器的通信都是通过套接字实现的。而我们知道对套接字的操作和对文件标识符的操作是类似的。
  • 时间事件,time event,redis中一些任务是定时执行的,即为时间事件。如serverCron,定时200ms执行,检测是否需要更新RDB。

文件事件

文件时间处理器是单线程运行的,当同是有多个I/O事件发生的时候,会将他们放在一个队列里面,按顺序分配给文件事件分派器。

文件事件处理器架构

文件事件处理器

  • redis给套接字定义了两种状态 AE_READABLE, AE_WRITABLE
  • I/O多路复用对于是对一些常用的I/O多路复用函数库的封装,如select,epoll等。在程序编译的时候,会选择当前系统中性能最好的函数库作为redis的多路复用程序。
  • 文件事件分配器,调用和套接字状态绑定的处理器,一种套接字状态可能先后绑定到不同的处理器上。
  • 处理器,处理器负责执行对应的操作,如写数据,回复数据等。
一次请求的过程

首先服务器端监听套接字的 AE_READABLE 状态要处于监听状态,并绑定连接应答处理器,等待客户端发起连接的请求,请求发生后连接应答处理器处理,将AE_READABLE状态和命令请求处理器绑定,在客户端发起请求后,执行对应操作。在需要回复的时候,将AE_WRITABLE和命令回复处理器绑定,当客户端准备好后,回复。

时间事件

redis 将未发生的时间事件放在一个无序链表中。在redis的主函数里面(是一个循环),首先会遍历这个链表,计算出最近的一个时间事件还剩多久发生t_remain,并生成一个时间变量,然后程序进入到I/O多路复用的阻塞函数中(如果t_remain小于0,则该函数不阻塞直接返回)。这样做的好处是:

  • 可以在等待时间事件发生的时候处理请求
  • 减少对于时间事件的轮询次数
  • 因为传入了时间发生的剩余时间,可以保证阻塞时间返回。
时间事件的流程

首先要将时间事件的发生时间(when,毫秒的unix时间戳)、处理器注册到上面说的链表中,注册函数会自动为时间事件分配一个递增的ID。
事件发生时,会调用处理函数,如果处理函数返回AE_NOMORE 则说明此时间事件不再循环执行。否则返回值表示,时间将在多少毫秒后再次到达,如果处理器不返回WE_NOMORE,服务器会自动更新无序链表中的时间事件的when属性,从而保证下次执行。

总结

redis 主程序对于时间事件和文件事件的处理都是原子的,不存在抢占,因此时间事件和文件事件都应该保证不过多的占用资源。在文件事件中,如果一次操作进行过多的读写,那么就会中断,等到下次循环的时候执行。时间事件会将耗时的任务放在子进程或子线程中运行。

serverCron事件

作为redis中唯一的一个定时事件,承担着很多任务

  • 处理过期键
  • 释放无效的客户端连接
  • 处理RDB和AOF持久化
  • 更新统计信息,时间,内存占用,数据库占用
  • 如果是主服务器,对从服务器进行定期同步
  • 集群模式,对集群进行定期的同步和连接测试。

第十二章 客户端

 进步不是说说而已,要持续的专注和努力。
redis服务器为每一个客户端维护一个结构体 redis.h/redisClient, 用于保存客户端的状态信息,所有客户端的结构体使用一个链表来保存。

服务器结构

typedef struct redisServer {
list *clients; //客户端列表,每一个元素都是一个redisClient结构的指针。
redisClient *lua_client; //用来执行lua脚本中redis命令的伪客户端,伴随服务器生命周期
} redisServer;

AOF 伪客户端

在AOF执行完后,退出。

客户端结构

 可以使用 Client 命令,结合 list, setname,等参数进行redisClient 结构的管理。

typedef struct redisClient {
	int fd;		// -1 表示伪客户端,执行AOF还原数据库或执行LUA文件中的Redis命令时会创建。其余为大于零表示连接的套接字描述符。
	string name;	//客户端的名字,可用来区分客户端名称, Client setname/getname 可进行管理
	int flags;			//包含多种标志,比如 客户端为主服务器,客户端为从服务器,事务相关(客户端正在执行事务,事务执行失败,watch的键是否被修改),强制将命令写入到AOF(对于不修改数据的命令一般不写入,但对于发布订阅相关的可能影响客户端状态,需要写入)强制同步命令到各个客户端(如载入LUA文件)
	sds queryBuf; 		//将客户端的请求存起来,用于转换成argv, argc
	robj *argv;		//[0] 保存命令, 往后保存参数。执行命令时原理见注解
	int argc;			//argv 数组的大小
	redisCommand *cmd;
	char buf[REDIS_REPLY_CHUNK_BYTES];		//一般为16K 用来存 ok 这些小的返回数据。
	int bufPos;		//
	list *reply;  		//元素为字符串对象。返回值缓冲,为一个string的列表。
	int authenticated;  	// 1 表示认证通过  0 表示不通过,此时这能执行
	
	time_t ctime;	//客户端建立连接的时间
	time_t lastinteraction;		//最后的交互时间
	time_t obuf_soft_limit_reached_time;		//上次输出缓冲达到软件限制大小的时间
} redisClient;
注:
  • 命令执行:
    argv[0] 中的命令,不分大小写的到一个字典(key为sds, value为 redisCommend 结构的指针。该结构包含命令实现函数,命令参数,已经最近访问情况的统计信息。

客户端的操作

创建

在连接建立后,服务器会为其创建一个redisClient结构塞到服务器的客户端列表中。

关闭

以下条件导致服务器关闭客户端:

  • client kill 命令
  • 客户端断开连接
  • 客户端发送不符合协议格式的命令请求
  • 空转时间过长

第十三章服务器

 我不知道为什么时间过得这么快,荒废自己真的是一件简单,痛苦,又无法挽回的事情。

命令执行过程

  • 客户端将输入的命令转换为 协议的格式,发送给服务器
  • 服务器先存在输入缓冲器,然后解析到 argv 和 argc
  • 根据 argv[0] 在命令的字典里,找到命令的 redisCommand。
  • 校验命令是否可以执行。参数是否正确;内存是否足够;服务器所进行的操作或所处状态是否不允许执行;命令是否存在等。
  • 调用具体的实现函数,执行。
  • 实现函数将输出结果写入缓冲区,并为客户端套接字关联 命令回复处理器。
  • 执行日志,持久化,同步数据等后续操作。
  • 客户端套接字可写,读取数据,转换为展示格式。此时服务端清空缓冲区,等待下次请求。
typedef strunt redisCommand {
	char * name;		//命令的名字
	redisCommandProc * proc;		//指定操作函数地址
	int arity;	//	用于检查命令参数个数
	char *sflags;	//标志位,标志命令是   读,写,内存占用型 等特性。
	int flags;		// 将 sflags 二进制话,方便操作符运算
	long long calls;		//服务器执行此命令的次数
	long long millisconds;		//服务器执行此命令的总时长
} redisCommand;

serverCron

功能
  • 更新系统时间缓存
  • 计算每秒运行指令数
  • 更新峰值内存使用
  • 处理关闭服务器
  • 检测是否可进行重写AOF
  • 客户端管理,踢出超时客户端,缩减客户端缓冲区
  • 服务器管理,删除过期键
  • 进行AOF持久化。
相关的redisServer属性
typedef struct redisServer {
    //为减少获取系统时间产生的系统调用,serverCron
	time_t unixtime;		
	long long mstime;		//
	unsgined lruclock:22; 	//  LRU - Least Recently Used
	
	long long ops_sec_last_sample_time;   	//上次采样时间
	long long ops_sec_last_sample_ops;    //上次采样时,系统执行的命令数
	long long ops_sec_samples[REDIS_OPS_SEC_SAMPLES];   //环形数组,缓存最近16次采样结果
	int ops_sec_idx;

	size_t stat_peak_memory;

	int shutdown_asap;

	int aof_rewrite_scheduled;
	pid_t aof_child_pid;
	pid_t rdb_child_pid;

	int cronloops;

	dict *publish_channels;		//订阅频道的客户端列表

	list *pubsub_patterns;

	multiState mstate;  //存储事务命令
	
	dict *repl_scriptcache_dict;  		//存储lua 脚本同步状态
​	dict *lua_scripts;			//存储 lua 脚本 sha1 与命令的对应
} redisServer;

服务器启动

初始化状态 initServerConfig
  • 初始化服务ID
  • 运行频率
  • 默认配置文件路径
  • 运行架构
  • 端口号
  • LRU时钟
  • 持久化初始条件
  • 创建命令表
载入配置项

redis支持启动时指定用户配置 或 配置文件,用户指定的配置将覆盖前面初始化的状态。这也是为什么先初始化状态,然后初始化服务器的数据结构

初始化数据结构 initServer
  • clients 链表
  • db 数组,包含所有数据库
  • 频道订阅的字典
  • 模式订阅的链表
  • lua 运行环境
  • slowlog 用于保存慢查询日志的属性
其他操作
  • 设置sigterm 处理函数
  • 创建共享对象,如统一回复字符的字符串,1-10000的数字字符串
  • 打开监听,等待连接
  • 设置serverCron时间事件
  • 还原数据库,先检查是否打开了AOF,没有则使用RDB
  • 进入事件循环。

发布订阅

 忍耐和自律是一种良好的品质。能够有效抵抗时间不够的感觉。

发布订阅频道

实现方式

redisServer 结构体中保存一个字典,key为订阅的频道名, value为 redisClient 结构体的一个链表,保存着订阅该频道的客户端列表。有客户端调用发布信息指令后,服务器会向列表中的客户端推送消息。

使用方式
//订阅
subscribe channelName;
unsubscribe channelName;
//发布
publish channelName;

发布订阅模式

实现方式

redisServer中保存一个列表pubsub_patterns,列表元素为一个结构体

typeder struct pubsubpattern {
	redisClient *client;
	robj* pattern;
} pubsubpattern;

当一个客户端订阅一个模式是,会创建一个这样的结构体放到 pubsub_patterns 链表的后面。发布频道消息的时候,在将消息发送给订阅者之后,还会遍历模式的列表,并将详细发送给匹配的客户端。

使用方式
//订阅
psubscribe new.*;
punsubscribe new.*;
//发布
publish channelName;

管理发布订阅

使用 pubsub 当前只用于查询

//管理发布订阅
pubsub channels [pattern]   //返回满足模式的频道,通过遍历频道列表实现
pubsub numsub [channel1] [channel2]   //返回各个频道的订阅者数量,即返回客户端列表的长度

//管理发布订阅模式
pubsub numpat           返回订阅模式链表的长度。

事务

事务的目的是让将客户端的一系列命令打包起来,在exec后一起执行,在执行打包的命令时,任何其他客户端的命令都不执行,直到打包命令执行完毕,返回包中各个命令的结果。
watch 的作用是检测在 multi 和 exec 之间,指定的键是否有修改,如果有修改,exec 将不执行事务命令包,直接返回 nil, 表示错误。

  • redis的事务不支持回滚操作
  • redis 对于错误的命令,在添加到事务命令包的时候就会识别;执行时错误的指令也不会执行,因此是满足一致性的。
  • redis是单线程,因此满足隔离性。
  • redis的持久性,除非保证数据写入到持久化文件中,否则不能满足持久性。

ACID

事务的 ACID 特性

  • Atomicity 原子性,事务的指令要么都执行,要么都不执行,redis具备
  • Consistency 一致性,数据满足按照数据库规定的格式,不会出现错误的数据
  • Isolation 隔离性,指服务器运行多个事务的时候,他们之间不会相互影响。
  • Durability 持久性。当一个事务执行完毕后,它的结果被保存到永久的存储介质中,不会丢失。

使用方式

watch keyName
multi
command1   // like   set keyName1 hello
command2
exec

实现原理

watch

在RedisDB中,存储一个字典 watched_keys,当有修改的键值的命令执行时,会检查这个字典,如果键值存在于其中,那么会置位对应客户端链表中的客户端的 REDIS_DIRTY_CAS 标志位。在exec执行时,会检查这个标志位。

命令存储

redis 会将命令字符串解析后,存储起来,而不是直接存储命令的字符串。

typedef struct multiState {
	multiCmd *command;   // 一个列表。  7t'f
	int count;
} multiState;

typedef struct multiCmd {
	robj **argv;
	int argc;
	struct redisCommand *cmd;
}

副作用的函数不仅仅只是返回了一个值,而且还做了其他的事情:

1、修改了一个变量

2、直接修改数据结构

3、设置一个对象的成员

4、抛出一个异常或以一个错误终止

5、打印到终端或读取用户输入

6、读取或写入一个文件

7、在屏幕上画图

lua脚本

 不努力成长就容易被淘汰,不要安于现状,不要颓废。

 lua脚本看起来挺流行的样子,感觉所有语言在实现基础功能的时候都没有太大的区别,不过就是函数库丰富的运行效率低,不怎么丰富的运行效率高。可能站在非 if else 程序员的角度上会有更大的不同吧。

typedef struct redisServer {
​ dict *repl_scriptcache_dict;
​ dict *lua_scripts;
}

redis 创建 lua 运行环境

  • lua_open 创建一个基础环境
  • 引入常用的 lua 库,如数学库,字符串处理库,序列化库(如 json)这些方便对 redis 运行结果进行处理
  • 设置 redis 全局函数表格, 包含 redis.call, redis.pcall 使得 lua 可以调用 redis 的指令,以及日志相关的,SHA1(一种校验和计算函数),返回错误信息的函数。
  • 替换到 lua 的随机数生成函数, random, randomseed 因为 lua 脚本的随机数生成函数有副作用,导致不同的服务器返回的结果不同,可能导致数据不一致。
  • 排序辅助函数,因为 一些 redis 命令即使参数一样也可能获得不同的结果,为了避免数据不一致,redis 会将结果排序
  • redis.pcall 错误报告辅助函数,当 lua 脚本运行 redis 命令出错时,用于打印更加详细的信息。
  • 保护 lua 的全局变量。只能保护 不允许设置全局变量,不保护 禁止修改。
  • 最后将 lua 环境保存在 redisServer 的属性当中。

指令执行过程

在执行脚本之前,服务器还要创建一个伪客户端。执行过程为:

  • Lua 将 redis.call redis.pcall 的指令,传给伪客户端
  • 伪客户端传给命令执行器
  • 命令执行器将结果返回给 伪客户端
  • 伪客户端返回给 lua 环境。

redisServer 中的两个字典

传入的 lua 指令首先会计算 sha1 校验和,并将其作为 key ,实际的指令为值,存储起来。

由于 lua 文件要同步给从服务器,所以要记录是否完全同步给所有从服务器,replc_lua_script就是实现这个作用的,key 为 sha1 ,value 标识是否完全同步。如果 value 标识为完全同步,主服务器在同步的时候会采取措施保证从服务器能够找到需要执行的指令,如将evalsha 转换为 eval 传递给从。

lua 脚本执行的命令

eval

上传并执行 lua 指令
可以传入双引号包含的 lua 命令;也可以通过指定脚本路径,上传脚本。
不论采用哪种方式,对于 redis 来说都是处理命令序列。服务端不存在保存文件路径,而且仔细想想也没有办法保存路径。

evalsha

通过指令的校验和来执行执行,前提是通过 eval 或 script load 上传过,同时你保存了返回的校验和。

script load

加载 lua 指令,但不执行。

script flush

清除之前执行的所有脚本数据,相当于重新构建 lua 环

script exists

根据 sha1 判断对应指令是否存在

script kill

对于超过 lua_limit_time 的脚本,会监控 script kill 命令,以便用户可以终结掉命令。但是对于已经写入的数据不会处理

shutdown nosave

对于超过 lua_limit_time 的脚本,会监控 shutdown 命令,以便用户可以终结掉命令。对于已经写入的数据可以不保存,以防止出现数据问题。

排序

 有关于过去的记忆深刻而纯粹,每当想起都会泛起一阵阵涟漪,终究还是一句业已逝去。

 尽管 redis 有排序功能,但是我们多数还是选择,把数据下载下来自己排序。

选项

sort 可以结合数据库中的其他键值做排序,如:

sort student by *_score

student 是一个集合的键,其中可能包含 peter,tom, 而库中存在 peter_score, tom_score 键,上面命令可以根据这两个分值键对 student 进行排序,并返回但不修改 student 中的数据顺序。

可以使用 asc,desc, alpha(按照字母表排序)修饰 sort。

可以使用 by 指定采用哪些键排序。

可以使用 Get(拉取其余键的值),并使用store 存储, limit 限制长度

sort students ALPHA get *_name *_score store student_name_score limit 0 4

选项执行顺序

先排序,限制排序结果集的长度,获取外部键,保存,返回结果集合。

改变指令的顺序不会影响上面的执行顺序,但是多个 GET 的顺序要注意,多个GET之间的排列顺序会影响最后的结果。

二进制

 看起来简单的东西,有时候还挺麻烦的。

指令

setbit, getbit, bitcount, topbit 类似于set, get 操作,参数不同,bitcount计算值为1 的bit数。topbit用于进行位运算,并将结果存储在指定的键值中。

setbit keyName 3 1 

上面指定了keyName 键的 第三位为1.

关于bitcount

如果 100MB的空间内都是二进制,那么统计这么多个二进制位的为 1 的位数,将是一个数十亿级别的操作。redis 采用了查表和计算汉明重量的算法,将复杂度降低了两个数量级。

慢查询日志

通过设置 slowlog-log-slower-than 指定执行时间超过多少微秒的指令为慢指令,对应执行时间超过这个值的将会被存储在慢查询日志。可以方便对redis进行优化。
通过设置 slowlog-max-len 指定慢查询日志最多存储多少,慢查询日志采用一个队列,先进先出的方式,保存和删除日志。

监视器

通过redisClient 中的标志位,标志客户端是否处于监控状态,同时维护一个监控列表,在服务器收到请求时,不仅会执行,还会发送给监视器。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值