Redis简介
Redis是一个开源的使用ANSI-C编写的、可基于内存也可以持久化的key-value型数据库。
特点:由于是基于内存存储,所以Redis的操作比通常基于硬盘存储的数据库如Mysql要快速,同时Redis提供多种不同的数据结构,能够适应各种不同的场景。
Redis数据结构与对象
SDS简单动态字符串
Redis没有直接使用C字符串,而是定义了一个名为sds的简单动态字符串,说他简单,是因为他只在C字符串的基础上添加了一个字符串长度len,不包含字符串末尾无意义字符的最大容量alloc和一个符号位flags,而动态是说sds字符串容量可以扩大或缩小,并且sds字符串可以兼容部分C字符串的string函数。
typedef char *sds;
其中说明一下flags,sds的flags有五种类型,分别是sdshdr5、sdshdr8、sdshdr16、sdshdr32和sdshdr64,后面的数字说明sds字符串的最大长度,而sdshdr5比较特殊,在sds.h中对sdshdr5的定义里只包含了flags和buf[],在Redis源码中,作者对sdshdr5有一句注释,/* Note: sdshdr5 is never used, we just access the flags byte directly.,这里说sdshdr5并没有使用,而是直接访问sdshdr5类型的flags位。在长度小于32位的键值对中,键的底层是sdshdr5,没有len和alloc或许说明这个底层实现的数据是不变的,所以不需要len和alloc,具体参考【Redis源码分析】
/* Note: sdshdr5 is never used, we just access the flags byte directly.
* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {
unsigned char flags; /* 3 lsb of type, and 5 msb of string length */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* used */
uint8_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {
uint16_t len; /* used */
uint16_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {
uint32_t len; /* used */
uint32_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {
uint64_t len; /* used */
uint64_t alloc; /* excluding the header and null terminator */
unsigned char flags; /* 3 lsb of type, 5 unused bits */
char buf[];
};
sds采取预分配冗余空间的方式来减少内存频繁分配。
对比C字符串,sds还有一个特点,就是二进制安全。因为C字符串的一个特性就是字符必须遵循某种编码规范(如ANSII),并且C字符串以空字符结尾,这对于图片或视频转换的二进制数据来说,基本不太可能遵守,所以C字符串无法保证存储的二进制数据的安全性。对于sds字符串,因为sds有一个len属性,所以可以不用借助空字符来判断字符串的结尾,可以保证二进制数据的安全。
list链表
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;
head为头节点,tail为尾节点,len值存储了链表长度,dup方法作用是复制当前节点,free方法作用是释放当前节点,match则用来比较当前节点和指定节点。
ListNode的定义如下:
typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;
包含一个指向前面节点的指针,一个指向后一节点的指针,还有一个节点值。
由于是双向链表,所以Redis链表的遍历可以从头到尾,也可以从尾到头,在ListIter中定义的direction表示遍历的方向。
typedef struct listIter {
listNode *next;
int direction;
} listIter;
dict字典
Redis的字典(dict)的实现如下:
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) */
};
dictType里包含一系列dict操作:
typedef struct dictType {
uint64_t (*hashFunction)(const void *key);
void *(*keyDup)(dict *d, const void *key);
void *(*valDup)(dict *d, const void *obj);
int (*keyCompare)(dict *d, const void *key1, const void *key2);
void (*keyDestructor)(dict *d, void *key);
void (*valDestructor)(dict *d, void *obj);
int (*expandAllowed)(size_t moreMem, double usedRatio);
/* Allow a dictEntry to carry extra caller-defined metadata. The
* extra memory is initialized to 0 when a dictEntry is allocated. */
size_t (*dictEntryMetadataBytes)(dict *d);
} dictType;
dictEntry可以是一个指针,可以是一个无符号整型数,也可以是一个有符号整形数和双精度浮点数:
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} 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;
Redis的字典解决地址冲突的方法是链地址法,将同一个地址的键以单链表的形式连接起来。同时,由于在处理字典的时候,dict的大小会随之增大或减小,所以可能需要对dict进行重新散列,以扩容为例,原先的dict只有8个hash地址,现在需要拓展为16个,这时就将ht_table[0]里的元素一个一个重新散列到ht_table[1]里(因为每次只操作一个table),而Redis为了减小一次性rehash的内存消耗,所以采用渐进式rehash的方法,将每次rehash操作分摊到对字典的增删查改上(在rehash过程中的添加元素操作是在ht_table[1]上执行的,这保证了ht_table[0]的元素个数只减不增),如果查找ht_table[0],但ht_table[0]不包含要查找的元素,这时就去ht_table[1]里查找,当所有元素都rehash完成之后,将ht_table[0]释放,将ht_table[1]设置为ht_table[0],并重新分配一个新的ht_table[1]。
zskiplist跳跃表
跳跃表是Redis的基本数据类型的有序集合SortedSet的底层实现,其数据结构实现如下:
typedef struct zskiplist {
struct zskiplistNode *header, *tail;
unsigned long length;
int level;
} zskiplist;
zskiplist有一个指向头节点和一个指向尾节点的指针,length存储跳跃表长度,level表示除头结点外的跳跃表节点中的最高层级。
对于zskiplistNode的定义如下:
typedef struct zskiplistNode {
sds ele;
double score;
struct zskiplistNode *backward;
struct zskiplistLevel {
struct zskiplistNode *forward;
unsigned long span;
} level[];
} zskiplistNode;
ele是跳跃表节点的值,跳跃表按每个节点的score值的大小排序(如果score值相等,则按ele排序),每个节点除头节点的下一个节点外,都有一个指针backward指向前一个节点,跳跃表节点的每一层都有一个指向后面一个或第N个节点,span表示跳跃了多少个节点,比如节点2的level2指向节点5的level2,所以span值为3。
下图为一个跳跃表示例:
intset整数集合
intset是只包含整数的一个集合,它是集合键的底层实现之一,对intset的定义如下:
typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;
contents是intset集合的元素数组,并且contents里的元素按从小到大排序,int8_t并不是contents元素的真实类型,contents元素的类型由encoding决定,length是contents个数。
要特别注意的是,往比如INTSET_ENC_INT16类型的contents中添加类型为INTSET_ENC_INT32的元素,首先会将contents扩容,比如原先数组有3个元素,长度为3*16,现在添加一个32位的整数,先会将contents扩容为4*32的大小,然后再依次将数组中的元素移动到相应的位置,最后修改length和encoding的值。
ziplist压缩列表
ziplist是一种内存紧凑型链表,他的数据结构如下图所示:
使用ziplist的目的是为了紧凑内存空间,节省内存,但有两个缺点:
一是查找中间节点的复杂度较高,因为ziplist中只保存了最后一个节点的偏移量,所以只有查找靠近两端的节点较快,平均时间复杂度为O(n);
二是因为每个元素都保存着上一个元素的长度这一信息,所以当执行插入操作时,可能会导致插入节点后面节点的prevlen占用的内存空间发生改变,比如某个节点的prevlen为254,占用一个字节,当插入一个长度为300的节点后,修改prevlen为300,需要占用两个字节的空间,而这个节点的长度也因此改变,后续节点也可能会因当前节点长度的改变而改变,从而发生一系列的更新,即连锁更新。
所以就发展出来quicklist这一数据结构。
quicklist快速列表
quicklist可以看成是ziplist和linkedlist的结合,每一个quicklistNode都指向一个ziplist,当有新增元素要插入时,quicklist会判断ziplist容量是否已满,否则就插入到下一个quicklist节点,这一举措能减少对ziplist插入元素后引发连锁更新的情况的发生,但并不能完全避免。
因此,在Redis 5.0后又推出了一个新的代替ziplist的数据结构,并且这个数据结构能完全避免连锁更新的情况的发生。
listpack紧凑列表
相较于ziplist,listpack最大的不同是每个listpack entry不保存前一个元素的长度,而是保存自己的长度,数据结构如下:
/* Each entry in the listpack is either a string or an integer. */
typedef struct {
/* When string is used, it is provided with the length (slen). */
unsigned char *sval;
uint32_t slen;
/* When integer is used, 'sval' is NULL, and lval holds the value. */
long long lval;
} listpackEntry;
因为不用保存前一个元素的长度,所以前一个元素的改变并不会对当前元素造成多少影响,与此同时,遍历元素可以将元素自身长度作为偏移量来使用,以达到遍历的目的。
Redis Object
Redis基本数据类型有五种,分别是strings、list、hash、set(无序集合)和sorted set(有序集合),后续还新增了HyperLogLogs(基数统计)、Geospatial(地理空间对象)、bitmaps(位图)、stream。
以下先介绍一下五种基本对象类型。
strings字符串类型
list列表类型
hash哈希类型
set无序集合类型
sorted set有序集合类型
单机数据库的功能
RDB持久化
AOF持久化
Redis事件
多机数据库的实现
主从结构
在Redis多机数据库中,可以对数据库建立联系,可以设置一个数据库为主机,多个数据库为从机。用户可以使用slaveof命令让一台服务器去复制另一台服务器,被复制的服务器为主机,对主服务器复制的服务器为从机。
设置主从结构的好处在于,可以实现对数据库数据的读写分离,主机用于写数据,用户只能在从机上读取数据,同时,由于每台机器上的数据都是一致的,所以设置多台从机也能保证读数据的容错率,比如用户读取从机1上的数据,这时从机1宕机了,用户可以接着去从机2读数据,因为数据都是一致的,所以对于用户来说结果都一样。
实现主从结构的关键在于数据同步,在Redis中有两种同步策略,一是完整重同步(sync),另一种是部分重同步(psync),以下介绍两种重同步策略的不同。
完整重同步:主服务器将当前数据库状态以RDB文件记录下来,发送给从服务器,这段时间内主服务器接收到的写命令则被记录到缓冲区中,等待从服务器接收并加载完RDB文件后,主服务器再将缓冲区保存的所有写命令发送给从机,等主机和从机数据库状态保持一致后,主从服务器之间以命令传播(主机接受到一个写命令,在执行之后将命令发送给从机)的形式继续维持数据库状态一致。
部分重同步: