只有光头才能变强git
最近在学Redis,我相信只要是接触过Java开发的都会听过Redis这么一个技术。面试也是很是高频的一个知识点,以前一直都是处于了解阶段。秋招事后这段时间是没有什么压力的,因此打算系统学学Redis,这也算是我从零学习Redis的笔记吧。程序员
本文力求讲清每一个知识点,但愿你们看完能有所收获。github
首先,确定是去官网看看官方是怎么介绍Redis的啦。https://redis.io/topics/introduction面试
若是像我同样,英语可能不太好的,可能看不太懂。没事,我们Chrome浏览器能够切换成中文的,中文是咱们的母语,确定没啥压力了。Eumm...redis
读完以后,发现中文也就那样了。算法
一大堆没见过的技术:lua(Lua脚本)、replication(复制)、Redis Sentinel(哨兵)、Redis Cluster(Redis 集群),固然咱们也会有看得懂的技术:transactions(事务)、different levels of on-disk persistence(数据持久化)、LRU eviction(LRU淘汰机制)..数据库
至少官方介绍Redis的第一句应该是能够很容易看懂:"Redis is an open source (BSD licensed),in-memory data structure store, used as a database,cache and message broker."segmentfault
Redis是一个开源的,基于内存的数据结构存储,可用做于数据库、缓存、消息中间件。数组
就我我的认为:学习一种新技术,先把握该技术总体的知识(思想),再扣细节,这样学习起来会比较轻松一些。因此咱们先以“内存”、“数据结构”、“缓存”来对Redis入门。浏览器
从上面可知:Redis是基于内存,经常使用做于缓存的一种技术,而且Redis存储的方式是以key-value
的形式。
咱们能够发现这不就是Java的Map容器所拥有的特性吗,那为何还须要Redis呢?
参考资料:
若是咱们的网站出现了性能问题(访问时间慢),按经验来讲,通常是因为数据库撑不住了。由于通常数据库的读写都是要通过磁盘的,而磁盘的速度能够说是至关慢的(相对内存来讲)
若是学过Mybaits、Hibernate的同窗就能够知道,它们有一级缓存、二级缓存这样的功能(终究来讲仍是本地缓存)。目的就是为了:不用每次读取的时候,都要查一次数据库。
有了缓存以后,咱们的访问就变成这样了:
本文不会讲述命令的使用方式,具体的如何使用可查询API。
Redis支持丰富的数据结构,经常使用的有string、list、hash、set、sortset这几种。学习这些数据结构是使用Redis的基础!
"Redis is written in ANSI C"-->Redis由C语言编写
首先仍是得声明一下,Redis的存储是以key-value
的形式的。Redis中的key必定是字符串,value能够是string、list、hash、set、sortset这几种经常使用的。
但要值得注意的是:Redis并没有直接使用这些数据结构来实现key-value
数据库,而是基于这些数据结构建立了一个对象系统。
Redis中的每一个对象都由一个redisObject结构来表示:
typedef struct redisObject{ // 对象的类型 unsigned type 4:; // 对象的编码格式 unsigned encoding:4; // 指向底层实现数据结构的指针 void * ptr; //..... }robj;
简单来讲就是Redis对key-value
封装成对象,key是一个对象,value也是一个对象。每一个对象都有type(类型)、encoding(编码)、ptr(指向底层数据结构的指针)来表示。
下面我就来讲一下咱们Redis常见的数据类型:string、list、hash、set、sortset。它们的底层数据结构到底是怎么样的!
简单动态字符串(Simple dynamic string,SDS)
Redis中的字符串跟C语言中的字符串,是有点差距的。
Redis使用sdshdr结构来表示一个SDS值:
struct sdshdr{ // 字节数组,用于保存字符串 char buf[]; // 记录buf数组中已使用的字节数量,也是字符串的长度 int len; // 记录buf数组未使用的字节数量 int free; }
例子:
SDS与C的字符串表示比较
对于链表而言,咱们不会陌生的了。在大学期间确定开过数据结构与算法课程,链表确定是讲过的了。在Java中Linkedxxx容器底层数据结构也是链表+[xxx]的。咱们来看看Redis中的链表是怎么实现的:
使用listNode结构来表示每一个节点:
typedef strcut listNode{ //前置节点 strcut listNode *pre; //后置节点 strcut listNode *pre; //节点的值 void *value; }listNode
使用listNode是能够组成链表了,Redis中使用list结构来持有链表:
typedef struct list{ //表头结点 listNode *head; //表尾节点 listNode *tail; //链表长度 unsigned long len; //节点值复制函数 void *(*dup) (viod *ptr); //节点值释放函数 void (*free) (viod *ptr); //节点值对比函数 int (*match) (void *ptr,void *key); }list
具体的结构如图:
Redis的链表有如下特性:
void *
指针来保存节点值,能够保存各类不一样类型的值声明:《Redis设计与实现》里边有“字典”这么一个概念,我我的认为仍是直接叫哈希表比较通俗易懂。从代码上看:“字典”也是在哈希表基础上再抽象了一层而已。
在Redis中,key-value
的数据结构底层就是哈希表来实现的。对于哈希表来讲,咱们也并不陌生。在Java中,哈希表实际上就是数组+链表的形式来构建的。下面咱们来看看Redis的哈希表是怎么构建的吧。
在Redis里边,哈希表使用dictht结构来定义:
typedef struct dictht{ //哈希表数组 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩码,用于计算索引值 //老是等于size-1 unsigned long sizemark; //哈希表已有节点数量 unsigned long used; }dictht
咱们下面继续写看看哈希表的节点是怎么实现的吧:
typedef struct dictEntry { //键 void *key; //值 union { void *value; uint64_tu64; int64_ts64; }v; //指向下个哈希节点,组成链表 struct dictEntry *next; }dictEntry;
从结构上看,咱们能够发现:Redis实现的哈希表和Java中实现的是相似的。只不过Redis多了几个属性来记录经常使用的值:sizemark(掩码)、used(已有的节点数量)、size(大小)。
一样地,Redis为了更好的操做,对哈希表往上再封装了一层(参考上面的Redis实现链表),使用dict结构来表示:
typedef struct dict { //类型特定函数 dictType *type; //私有数据 void *privdata; //哈希表 dictht ht[2]; //rehash索引 //当rehash不进行时,值为-1 int rehashidx; }dict; //----------------------------------- typedef struct dictType{ //计算哈希值的函数 unsigned int (*hashFunction)(const void * key); //复制键的函数 void *(*keyDup)(void *private, const void *key); //复制值得函数 void *(*valDup)(void *private, const void *obj); //对比键的函数 int (*keyCompare)(void *privdata , const void *key1, const void *key2) //销毁键的函数 void (*keyDestructor)(void *private, void *key); //销毁值的函数 void (*valDestructor)(void *private, void *obj); }dictType
因此,最后咱们能够发现,Redis所实现的哈希表最后的数据结构是这样子的:
从代码实现和示例图上咱们能够发现,Redis中有两个哈希表:
key-vlaue
数据Redis中哈希算法和哈希冲突跟Java实现的差很少,它俩差别就是:
下面来具体讲讲Redis是怎么rehash的,由于咱们从上面能够明显地看到,Redis是专门使用一个哈希表来作rehash的。这跟Java一次性直接rehash是有区别的。
在对哈希表进行扩展或者收缩操做时,reash过程并非一次性地完成的,而是渐进式地完成的。
Redis在rehash时采起渐进式的缘由:数据量若是过大的话,一次性rehash会有庞大的计算量,这极可能致使服务器一段时间内中止服务。
Redis具体是rehash时这么干的:
跳跃表(shiplist)是实现sortset(有序集合)的底层数据结构之一!
跳跃表可能对于大部分人来讲不太常见,以前我在学习的时候发现了一篇不错的文章讲跳跃表的,建议你们先去看完下文再继续回来阅读:
Redis的跳跃表实现由zskiplist和zskiplistNode两个结构组成。其中zskiplist保存跳跃表的信息(表头,表尾节点,长度),zskiplistNode则表示跳跃表的节点。
按照惯例,咱们来看看zskiplistNode跳跃表节点的结构是怎么样的:
typeof struct zskiplistNode { // 后退指针 struct zskiplistNode *backward; // 分值 double score; // 成员对象 robj *obj; // 层 struct zskiplistLevel { // 前进指针 struct zskiplistNode *forward; // 跨度 unsigned int span; } level[]; } zskiplistNode;
zskiplistNode的对象示例图(带有不一样层高的节点):
示例图以下:
zskiplist的结构以下:
typeof struct zskiplist { // 表头节点,表尾节点 struct skiplistNode *header,*tail; // 表中节点数量 unsigned long length; // 表中最大层数 int level; } zskiplist;
最后咱们整个跳跃表的示例图以下:
整数集合是set(集合)的底层数据结构之一。当一个set(集合)只包含整数值元素,而且元素的数量很少时,Redis就会采用整数集合(intset)做为set(集合)的底层实现。
整数集合(intset)保证了元素是不会出现重复的,而且是有序的(从小到大排序),intset的结构是这样子的:
typeof struct intset { // 编码方式 unit32_t encoding; // 集合包含的元素数量 unit32_t lenght; // 保存元素的数组 int8_t contents[]; } intset;
intset示例图:
说明:虽然intset结构将contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,contents数组的真正类型取决于encoding属性的值:
从编码格式的名字咱们就能够知道,16,32,64编码对应能存放的数字范围是不同的。16明显最少,64明显最大。
若是原本是INTSET_ENC_INT16的编码,想要存放大于INTSET_ENC_INT16编码能存放的整数值,此时就得编码升级(从16升级成32或者64)。步骤以下:
另一提:只支持升级操做,并不支持降级操做。
压缩列表(ziplist)是list和hash的底层实现之一。若是list的每一个都是小整数值,或者是比较短的字符串,压缩列表(ziplist)做为list的底层实现。
压缩列表(ziplist)是Redis为了节约内存而开发的,是由一系列的特殊编码的连续内存块组成的顺序性数据结构。
压缩列表结构图例以下:
下面咱们看看节点的结构图:
压缩列表从表尾节点倒序遍历,首先指针经过zltail偏移量指向表尾节点,而后经过指向节点记录的前一个节点的长度依次向前遍历访问整个压缩列表。
再次看回这张图,觉不以为就很好理解了?
在上面的图咱们知道string类型有三种编码格式:
embstr和raw的区别:
编码之间的转换:
在上面的图咱们知道list类型有两种编码格式:
&&
总数量少于512个||
总数量大于512个ziplist编码的列表结构:
redis > RPUSH numbers 1 "three" 5 (integer) 3
linkedlist编码的列表结构:
编码之间的转换:
在上面的图咱们知道hash类型有两种编码格式:
&&
键值对总数量小于512||
键值对总数量大于512ziplist编码的哈希结构:
hashtable编码的哈希结构:
编码之间的转换:
在上面的图咱们知道set类型有两种编码格式:
&&
总数量小于512||
总数量大于512intset编码的集合结构:
hashtable编码的集合结构:
编码之间的转换:
在上面的图咱们知道set类型有两种编码格式:
&&
总数量小于128||
总数量大于128ziplist编码的有序集合结构:
skiplist编码的有序集合结构:
有序集合(sortset)对象同时采用skiplist和哈希表来实现:
编码之间的转换:
本文主要讲了一下Redis经常使用的数据结构,以及这些数据结构的底层设计是怎么样的。总体来讲不会太难,由于这些数据结构咱们在学习的过程当中多多少少都接触过了,《Redis设计与实现》这本书写得也足够通俗易懂。
至于咱们在使用的时候挑选哪些数据结构做为存储,能够简单看看:
key-value
若是你们有更好的理解方式或者文章有错误的地方还请你们不吝在评论区留言,你们互相学习交流~~~
参考博客:
参考资料:
一个坚持原创的Java技术公众号:Java3y,欢迎你们关注
原创技术文章导航: