点击▲关注 “数据和云”   给公众号标星置顶

更多精彩 第一时间直达

本文内容思维导图如下:

原文:https://my.oschina.net/liughDevelop/blog/2236771

作者:我叫刘半仙

一、简介和应用

Redis是一个由ANSI C语言编写,性能优秀、支持网络、可持久化的K-K内存数据库,并提供多种语言的API。它常用的类型主要是 String、List、Hash、Set、ZSet 这5种

Redis在互联网公司一般有以下应用:

  • String:缓存、限流、计数器、分布式锁、分布式Session

  • Hash:存储用户信息、用户主页访问量、组合查询

  • List:微博关注人时间轴列表、简单队列

  • Set:赞、踩、标签、好友关系

  • Zset:排行榜

再比如电商在大促销时,会用一些特殊的设计来保证系统稳定,扣减库存可以考虑如下设计:

上图中,直接在Redis中扣减库存,记录日志后通过Worker同步到数据库,在设计同步Worker时需要考虑并发处理和重复处理的问题。

通过上面的应用场景可以看出Redis是非常高效和稳定的,那Redis底层是如何实现的呢?

二、Redis的对象redisObject

当我们执行set hello world命令时,会有以下数据模型:

  • dictEntry:Redis给每个key-value键值对分配一个dictEntry,里面有着key和val的指针,next指向下一个dictEntry形成链表,这个指针可以将多个哈希值相同的键值对链接在一起,由此来解决哈希冲突问题(链地址法)。

  • 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”对应的编码:

redis所有的数据结构类型如下(重要,后面会用):

三、String

字符串对象的底层实现可以是int、raw、embstr(上面的表对应有名称介绍)。embstr编码是通过调用一次内存分配函数来分配一块连续的空间,而raw需要调用两次。

int编码字符串对象和embstr编码字符串对象在一定条件下会转化为raw编码字符串对象。embstr:<=39字节的字符串。int:8个字节的长整型。raw:大于39个字节的字符串。

简单动态字符串(SDS),这种结构更像C++的String或者Java的ArrayList<Character>,长度动态可变:

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由于记录了长度,相应的操作在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。

四、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,有兴趣可以阅读一下源码。

从图中可以看出Redis的linkedlist双端链表有以下特性:节点带有prev、next指针、head指针和tail指针,获取前置节点、后置节点、表头节点和表尾节点的复杂度都是O(1)。len属性获取节点数量也为O(1)。

与双端链表相比,压缩列表可以节省内存空间,但是进行修改或增删操作时,复杂度较高;因此当节点数量较少时,可以使用压缩列表;但是节点数量多时,还是使用双端链表划算。

4.2  ziplist(压缩列表)

当一个列表键只包含少量列表项,且是小整数值或长度比较短的字符串时,那么redis就使用ziplist(压缩列表)来做列表键的底层实现。

ziplist是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块(而不是像双端链表一样每个节点是指针)组成的顺序型数据结构;具体结构相对比较复杂,有兴趣读者可以看 Redis 哈希结构内存模型剖析。在新版本中list链表使用 quicklist 代替了 ziplist和 linkedlist:

quickList 是 zipList 和 linkedList 的混合体。它将 linkedList 按段切分,每一段使用 zipList 来紧凑存储,多个 zipList 之间使用双向指针串接起来。因为链表的附加空间相对太高,prev 和 next 指针就要占去 16 个字节 (64bit 系统的指针是 8 个字节),另外每个节点的内存都是单独分配,会加剧内存的碎片化,影响内存管理效率。

quicklist 默认的压缩深度是 0,也就是不压缩。为了支持快速的 push/pop 操作,quicklist 的首尾两个 ziplist 不压缩,此时深度就是 1。为了进一步节约空间,Redis 还会对 ziplist 进行压缩存储,使用 LZF 算法压缩。

五、Hash

Hash对象的底层实现可以是ziplist(压缩列表)或者hashtable(字典或者也叫哈希表)。

Hash对象只有同时满足下面两个条件时,才会使用ziplist(压缩列表):1.哈希中元素数量小于512个;2.哈希中所有键值对的键和值字符串长度都小于64字节。

hashtable哈希表可以实现O(1)复杂度的读写操作,因此效率很高。源码如下:

typedef struct dict {// 类型特定函数dictType *type;     // 私有数据void *privdata;     // 哈希表dictht ht[2];    // rehash 索引// 当 rehash 不在进行时,值为 -1int 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 - 1unsigned 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;

上面源码可以简化成如下结构:

这个结构类似于JDK7以前的HashMap<String,Object>,当有两个或以上的键被分配到哈希数组的同一个索引上时,会产生哈希冲突。Redis也使用链地址法来解决键冲突。即每个哈希表节点都有一个next指针,多个哈希表节点用next指针构成一个单项链表,链地址法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位。

Redis中的字典使用hashtable作为底层实现的话,每个字典会带有两个哈希表,一个平时使用,另一个仅在rehash(重新散列)时使用。随着对哈希表的操作,键会逐渐增多或减少。为了让哈希表的负载因子维持在一个合理范围内,Redis会对哈希表的大小进行扩展或收缩(rehash),也就是将ht【0】里面所有的键值对分多次、渐进式的rehash到ht【1】里。

六、Set

Set集合对象的底层实现可以是intset(整数集合)或者hashtable(字典或者也叫哈希表)。

intset(整数集合)当一个集合只含有整数,并且元素不多时会使用intset(整数集合)作为Set集合对象的底层实现。

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(跳跃表)。

当一个有序集合的元素数量比较多或者成员是比较长的字符串时,Redis就使用skiplist(跳跃表)作为ZSet对象的底层实现。

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)

skiplist的查找时间复杂度是LogN,可以和平衡二叉树相当,但实现起来又比它简单。跳跃表(skiplist)是一种有序数据结构,它通过在某个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。


资源下载

关注公众号:数据和云(OraNews)回复关键字获取

2018DTCC , 数据库大会PPT

2018DTC,2018 DTC 大会 PPT

DBALIFE ,“DBA 的一天”海报

DBA04 ,DBA 手记4 电子书

122ARCH ,Oracle 12.2体系结构图

2018OOW ,Oracle OpenWorld 资料

产品推荐

云和恩墨Bethune Pro企业版,集监控,巡检,安全于一身,你的专属数据库实时监控和智能巡检平台,漂亮的不像实力派,你值得拥有!

云和恩墨zData一体机现已发布超融合版本和精简版,支持各种简化场景部署,零数据丢失备份一体机ZDBM也已发布,欢迎关注。

Redis这么快你知道吗?相关推荐

  1. 字节跳动面试真题:Redis为什么快?

    前言 大家好,有位伙伴面试了字节(四年半工作经验),分享下面试真题,大家一起加油哈. 说说Redis为什么快 Redis有几种数据结构,底层分别是怎么存储的 Redis有几种持久化方式 多线程情况下, ...

  2. 面试官问我为什么Redis这么快,我.......

    开课开课~ 面试官:为什么项目中用Redis? 我:当然是因为Redis好啊 面试官:emmm.....那Redis哪里好? 我:因为Redis快啊. 面试官:(这小伙子有点彪啊...)那为什么Red ...

  3. 一文深入了解 Redis 内存模型,Redis 的快是有原因的!

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 来源:编程迷思 cnblogs.com/kismetv/p/865 ...

  4. 五大点,搞懂单线程的Redis到底快在哪里

    目录 前言 一.开发语言 二.纯内存访问 三.数据结构简单 四.单线程 五.非阻塞多路I/O复用机制 前言 Redis是一种基于键值对(Key-Value)的NoSQL数据库,Redis的Value可 ...

  5. 四个大点,搞懂 Redis 到底快在哪里?

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! 来源:http://1t.click/jnt >>阿里云8月最新优惠,点击查看& ...

  6. redis 内存不足 排查_一文深入了解 Redis 内存模型,Redis 的快是有原因的!

    前言 一.Redis内存统计 二.Redis内存划分 1.数据 2.进程本身运行需要的内存 3.缓冲内存 4.内存碎片 三.Redis数据存储的细节 1.概述 2.jemalloc 3.redisOb ...

  7. 面试还搞不懂Redis,快看看这40道面试题!| 博文精选

    作者| 程序员追风 责编 | Carol 出品 | CSDN云计算(ID:CSDNcloud) 近年来,微服务变得越来越热门,越来越多的应用部署在分布式环境中.常用的分布式实现方式之一就有 Redis ...

  8. 单线程多线程_面试系列 redis为什么快amp;单线程amp;多线程

    redis为什么这么快 C语言实现,执行速度快 纯内存操作,数据读写在内存中,异步持久化到磁盘 丰富和高效的数据结构 基于非阻塞的I/O多路复用机制 单线程避免了上下文切换 Redis单线程 redi ...

  9. 比 Redis 还快,更省内存,惊爆了!

    点击关注公众号:互联网架构师,后台回复 2T获取2TB学习资源! 上一篇:Alibaba开源内网高并发编程手册.pdf 最近,一位前谷歌.前亚马逊的工程师推出了他创作的开源内存数据缓存系统 Drago ...

  10. 比 Redis 还快,更省内存,开源,惊爆了!

    今年年中,一位前谷歌.前亚马逊的工程师推出了他创作的开源内存数据缓存系统 Dragonfly,用 C/C++ 编写,基于 BSL 许可(Business Source License)分发. 根据过往 ...

最新文章

  1. Mybatis获取插入记录的自增长ID
  2. cad菜单栏快捷键_拒绝效率低下,教你瞬间提升10倍!老师傅珍藏多年CAD快捷键...
  3. 分布式架构高可用架构篇_04_Keepalived+Nginx实现高可用Web负载均衡
  4. linux echo 变量 字符串,echo命令 – 输出字符串或提取Shell变量的值
  5. [PHP] 遗传算法求函数最大值一般实现
  6. JVM逃逸分析(同步省略、标量替换、栈上分配)
  7. machine learning (2)-linear regression with one variable
  8. My97 DatePicker获取自定义日期的前一天
  9. ocp linux 基础要点
  10. 论文浅尝 | 从树结构的长短期记忆网络改进语义表示
  11. EditorMD + handsome 编辑器插件整合
  12. html如何在本页路径上拼接参数,Html图片路径拼接参数实现打印导出
  13. SPFA ----模板 O(kE) (k一般不超过2)
  14. cygwin/msys: fatal error LNK1104: 无法打开文件“kernel32.lib”
  15. 数据库基础(4)函数依赖公理和推论(Armstrong公理),属性闭包和求候选键的技巧
  16. FFMPEG音视频解码流程MP4音视频文件流读取(转)
  17. 802.11n无线网卡驱动linux,Ubuntu 16.04 无线网卡驱动安装 80211
  18. 采用加速度传感器的角度测量仪系统硬件电路设计_温度传感器电路
  19. python在线朗读-python朗读软件
  20. matlab怎么表示x的平方,用matlab算多项式x平方

热门文章

  1. 如何在Linux上运行Windows软件?
  2. devops的五个要素_DevOps诗歌大满贯:DevOps艺术的五首诗
  3. asp教室使用_为什么要在教室中使用开放式硬件?
  4. (2)Node.js介绍
  5. 编程范式之命令式与函数式
  6. Bootstrap 字体图标使用案例
  7. android 视频恢复软件,视频恢复软件免费版
  8. 视觉SLAM十四讲学习笔记-第四讲---第五讲学习笔记总结---李群和李代数、相机
  9. Git笔记(13) 分支管理
  10. 计算机健康教育应用的意义,【计算机信息论文】计算机信息在心理健康教育中的实效性(共2561字)...