1、简介和应用java
Redis是一个由ANSI C语言编写,性能优秀、支持网络、可持久化的K-K内存数据库,并提供多种语言的API。它经常使用的类型主要是 String、List、Hash、Set、ZSet 这5种。redis
String:缓存、限流、计数器、分布式锁、分布式Session算法
Hash:存储用户信息、用户主页访问量、组合查询数据库
List:微博关注人时间轴列表、简单队列数组
Set:赞、踩、标签、好友关系缓存
Zset:排行榜安全
再好比电商在大促销时,会用一些特殊的设计来保证系统稳定,扣减库存能够考虑以下设计:网络
经过上面的应用场景能够看出Redis是很是高效和稳定的,那Redis底层是如何实现的呢?数据结构
2、Redis的对象redisObject并发
当咱们执行set hello world命令时,会有如下数据模型:
sds:键key“hello”是以SDS(简单动态字符串)存储,后面详细介绍。
redisObject:值val“world”存储在redisObject中。实际上,redis经常使用5中类型都是以redisObject来存储的;而redisObject中的type字段指明了Value对象的类型,ptr字段则指向对象所在的地址。
redisObject对象很是重要,Redis对象的类型、内部编码、内存回收、共享对象等功能,都须要redisObject支持。这样设计的好处是,能够针对不一样的使用场景,对5中经常使用类型设置多种不一样的数据结构实现,从而优化对象在不一样场景下的使用效率。
不管是dictEntry对象,仍是redisObject、SDS对象,都须要内存分配器(如jemalloc)分配内存进行存储。jemalloc做为Redis的默认内存分配器,在减少内存碎片方面作的相对比较好。好比jemalloc在64位系统中,将内存空间划分为小、大、巨大三个范围;每一个范围内又划分了许多小的内存块单位;当Redis存储数据时,会选择大小最合适的内存块进行存储。
前面说过,Redis每一个对象由一个redisObject结构表示,它的ptr指针指向底层实现的数据结构,而数据结构由encoding属性决定。好比咱们执行如下命令获得存储“hello”对应的编码:
字符串对象的底层实现能够是int、raw、embstr(上面的表对应有名称介绍)。embstr编码是经过调用一次内存分配函数来分配一块连续的空间,而raw须要调用两次。
简单动态字符串(SDS),这种结构更像C++的String或者Java的ArrayList,长度动态可变:
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[]; // ’\0’空字符结尾
};
复制代码
get:sdsrange---O(n) set:sdscpy—O(n) create:sdsnew---O(1) len:sdslen---O(1)
常数复杂度获取字符串长度:由于SDS在len属性中记录了长度,因此获取一个SDS长度时间复杂度仅为O(1)。
预空间分配:若是对一个SDS进行修改,分为一下两种状况:
一、SDS长度(len的值)小于1MB,那么程序将分配和len属性一样大小的未使用空间,这时free和len属性值相同。举个例子,SDS的len将变成15字节,则程序也会分配15字节的未使用空间,SDS的buf数组的实际长度变成15+15+1=31字节(额外一个字节用户保存空字符)。
二、SDS长度(len的值)大于等于1MB,程序会分配1MB的未使用空间。好比进行修改以后,SDS的len变成30MB,那么它的实际长度是30MB+1MB+1byte。
惰性释放空间:当执行sdstrim(截取字符串)以后,SDS不会立马释放多出来的空间,若是下次再进行拼接字符串操做,且拼接的没有刚才释放的空间大,则那些未使用的空间就会排上用场。经过惰性释放空间避免了特定状况下操做字符串的内存从新分配操做。
杜绝缓冲区溢出:使用C字符串的操做时,若是字符串长度增长(如strcat操做)而忘记从新分配内存,很容易形成缓冲区的溢出;而SDS因为记录了长度,相应的操做在可能形成缓冲区溢出时会自动从新分配内存,杜绝了缓冲区溢出。
4、List
List对象的底层实现是quicklist(快速列表,是ziplist 压缩列表 和linkedlist 双端链表 的组合)。Redis中的列表支持两端插入和弹出,并能够得到指定位置(或范围)的元素,能够充当数组、队列、栈等。
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;
复制代码
rpush: listAddNodeHead ---O(1) lpush: listAddNodeTail ---O(1) push: listInsertNode ---O(1) index : listIndex ---O(N) pop: ListFirst/listLast ---O(1) llen: listLength ---O(N)
4.1 linkedlist(双端链表)
此结构比较像Java的LinkedList,有兴趣能够阅读一下源码。
与双端链表相比,压缩列表能够节省内存空间,可是进行修改或增删操做时,复杂度较高;所以当节点数量较少时,可使用压缩列表;可是节点数量多时,仍是使用双端链表划算。
4.2 ziplist(压缩列表)
当一个列表键只包含少许列表项,且是小整数值或长度比较短的字符串时,那么redis就使用ziplist(压缩列表)来作列表键的底层实现。
5、Hash
Hash对象的底层实现能够是ziplist(压缩列表)或者hashtable(字典或者也叫哈希表)。
hashtable哈希表能够实现O(1)复杂度的读写操做,所以效率很高。源码以下:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 老是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
typedef struct dictEntry {
void *key;
union {void *val;uint64_t u64;int64_t s64;} v;
// 指向下个哈希表节点,造成链表
struct dictEntry *next;
} dictEntry;
typedef struct dictType {
// 计算哈希值的函数
unsigned int (*hashFunction)(const void *key);
// 复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
// 销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;
复制代码
上面源码能够简化成以下结构:
Redis中的字典使用hashtable做为底层实现的话,每一个字典会带有两个哈希表,一个平时使用,另外一个仅在rehash(从新散列)时使用。随着对哈希表的操做,键会逐渐增多或减小。为了让哈希表的负载因子维持在一个合理范围内,Redis会对哈希表的大小进行扩展或收缩(rehash),也就是将ht【0】里面全部的键值对分屡次、渐进式的rehash到ht【1】里。
6、Set
Set集合对象的底层实现能够是intset(整数集合)或者hashtable(字典或者也叫哈希表)。
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
复制代码
sadd: intsetAdd---O(1) smembers: intsetGetO(1)---O(N) srem: intsetRemove---O(N) slen: intsetlen ---O(1) intset底层实现为有序,无重复数组保存集合元素。 intset这个结构里的整数数组的类型能够是16位的,32位的,64位的。若是数组里全部的整数都是16位长度的,若是新加入一个32位的整数,那么整个16的数组将升级成一个32位的数组。升级能够提高intset的灵活性,又能够节约内存,但不可逆。
7.ZSet
ZSet有序集合对象底层实现能够是ziplist(压缩列表)或者skiplist(跳跃表)。
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;
复制代码
zadd---zslinsert---平均O(logN), 最坏O(N) zrem---zsldelete---平均O(logN), 最坏O(N) zrank--zslGetRank---平均O(logN), 最坏O(N)