您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
Redis为什么这么快?
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
Redis为什么这么快?
自猿其说Tech
2022-07-06
IP归属:未知
13760浏览
计算机编程
首先Redis是一个使用ANSI C编写的、开源的、支持网络的、基于内存的、可选持久化的键值对存储系统。 ### 1 Redis的发家史 - 2009年由 Salvatore Sanfilippo(Redis之父)发布初始版本 - 2013年5月之前,由VMare赞助 - 2013年5月-2015年6月,由Pivotal赞助 - 2015年6月起,由Redis Labs赞助 根据db-engines.com上的排名,到目前为止Redis依然是最流行的键值对存储系统。 ![](//img1.jcloudcs.com/developer.jdcloud.com/f1591f2a-e360-4f3f-bbf2-f6ba4a19a83720220706155218.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/ed6fd222-be2b-4ecb-b8c1-5fc134bb8ac820220706155225.png) ### 2 Redis主要版本 - 2009年5月发布Redis初始版本 - 2012年发布Redis 2.6.0 - 2013年11月发布Redis 2.8.0 - 2015年4月发布Redis 3.0.0,在该版本Redis引入集群 - 2017年7月发布Redis 4.0.0,在该版本Redis引入了模块系统 - 2018年10月发布Redis 5.0.0,在该版本引入了Streams结构 - 2020年5月2日发布 6.0.1(稳定版),在该版本中引入多线程、RESP3协议、无盘复制副本 - 2022年1月31日发布 7.0 RC1,在该版本中主要是对性能和内存进行优化,新的AOF模式 ### 3 Redis 有多快? Redis中带有一个可以进行性能测试的工具 redis-benchmark,通过这个命令,我们就可以模拟多个客户端同时发起请求的场景,并且可以检测Redis处理这些请求所需要的时间。 根据官方的文档,Redis 已经在超过 60000 个连接上进行了基准测试,并且在这些条件下仍然能够维持 50000 q/s。同样的请求量如果打到MySQL上,那肯定扛不住,直接就崩掉了。 With high-end configurations, the number of client connections is also an important factor. Being based on epoll/kqueue, the Redis event loop is quite scalable. Redis has already been benchmarked at more than 60000 connections, and was still able to sustain 50000 q/s in these conditions. As a rule of thumb, an instance with 30000 connections can only process half the throughput achievable with 100 connections. Here is an example showing the throughput of a Redis instance per number of connections; ![](//img1.jcloudcs.com/developer.jdcloud.com/9063dcdf-0f58-495d-8333-3845808db99c20220706155306.png) ### 4 Redis为什么可以这么快? 那是什么原因造就了Redis可以具有如此高的性能?主要分为以下几个方面 ![](//img1.jcloudcs.com/developer.jdcloud.com/9381128a-2782-4290-946a-b9ba13d07ca720220706155318.png) #### 4.1 基于内存实现 Mysql的数据存储持久化是存储到磁盘上的,读取数据是内存中如果没有的话,就会产生磁盘I/O,先把数据读取到内存中,再读取数据。而Redis则是直接把数据存储到内存中,减少了磁盘I/O造成的消耗。 ![](//img1.jcloudcs.com/developer.jdcloud.com/00acfdef-6eda-44ed-ad34-dd9dc999d93620220706155337.png) #### 4.2 高效的数据结构 合理的数据结构,就是可以让你的应用/程序更快。Mysql索引为了提高效率,选择了B+树的数据结构。先看下Redis的数据结构&内部编码图: ![](//img1.jcloudcs.com/developer.jdcloud.com/6964d3f9-4617-4fc0-8d6e-08041d2516f120220706155353.png) ##### 4.2.1 SDS简单动态字符串 Redis没有采用原生C语言的字符串类型而是自己实现了字符串结构-简单动态字符串(simple dynamic string) ![](//img1.jcloudcs.com/developer.jdcloud.com/f14a4183-e950-43c7-8087-36ec83cd274420220706155409.png) ![](//img1.jcloudcs.com/developer.jdcloud.com/e7332a27-c1d0-4d5b-a912-e38e3b979c9920220706155417.png) SDS与C语言字符串的区别: - 获取字符串长度:C字符串的复杂度为O(N),而SDS的复杂度为O(1)。 - 杜绝缓冲区溢出(C语言每次需要手动扩容),如果C字符串想要扩容,在没有申请足够多的内存空间下,会出现内存溢出的情况,而SDS记录了字符串的长度,如果长度不够的情况下会进行扩容。 - 减少修改字符串时带来的内存重分配次数 - - 空间预分配, - - - 规则1:修改后长度< 1MB,预分配同样大小未使用空间,free=len; - - - 规则2:修改后长度 >= 1MB,预分配1MB未使用空间。 - - 惰性空间释放,SDS 缩短时,不是回收多余的内存空间,而是free记录下多余的空间,后续有变更,直接使用free中记录的空间,减少分配。 ##### 4.2.2 embstr & raw Redis 的字符串有两种存储方式,在长度特别短时,使用 emb 形式存储 (embeded),当长度超过 44 时,使用 raw 形式存储。 ![](//img1.jcloudcs.com/developer.jdcloud.com/68ded0b4-5a1e-4a98-9de0-846a75d7f85a20220706155828.png) 为什么分界线是 44 呢? 在CPU和主内存之间还有一个高速数据缓冲区,有L1,L2,L3三级缓存,L1级缓存时距离CPU最近的,CPU会有限从L1缓存中获取数据,其次是L2,L3。 ![](//img1.jcloudcs.com/developer.jdcloud.com/3abf1275-51ca-4cf9-be6c-eaf16a1ff2cc20220706160030.png) L1最快但是他的存储空间也是有限的,大概64字节,抛去对象固定属性占用的空间,以及‘\0’,剩余的空间最多也就是44个字节,超过44字节L1缓存就存不下了。 ![](//img1.jcloudcs.com/developer.jdcloud.com/cb36fbcd-7eef-430e-84a6-33421c6b852420220706160113.png) ##### 4.2.3 字典(DICT) Redis 作为 K-V 型内存数据库,所有的键值就是用字典来存储。字典就是哈希表,比如HashMap,通过key就可以直接获取到对应的value。而哈希表的特性,在O(1)时间复杂度就可以获得对应的值。 ```objective-c //字典结构数据 typedef struct dict { dictType *type; //接口实现,为字典提供多态性 void *privdata; //存储一些额外的数据 dictht ht[2]; //两个hash表 long rehashidx. //渐进式rehash时记录当前rehash的位置 } dict; ``` 两个hashtable通常情况下只有一个hashtable是有值的,另外一个是在进行rehash的时候才会用到,在扩容时逐渐的从一个hashtable中迁移至另外一个hashtable中,搬迁结束后旧的hashtable会被清空。 ![](//img1.jcloudcs.com/developer.jdcloud.com/1170c0bf-9463-44a3-bd1e-5e869553349520220706160426.png) ```objective-c hashtable的结构如下: typedef struct dictht { dictEntry **table; //指向第一个数组 unsigned long size; //数组的长度 unsigned long sizemask; //用于快速hash定位 unsigned long used; //数组中元素的个数 } dictht; typedef struct dictEntry { void *key; union { void *val; uint64_t u64; int64_t s64; double d; //用于zset,存储score值 } v; struct dictEntry *next; } dictEntry; ``` ![](//img1.jcloudcs.com/developer.jdcloud.com/f01af544-dec1-440c-9db3-2c87ad77dd5520220706160631.png) #### 4.2.4 压缩列表(ziplist) redis为了节省内存空间,zset和hash对象在数据比较少的时候采用的是ziplist的结构,是一块连续的内存空间,并且支持双向遍历 ![](//img1.jcloudcs.com/developer.jdcloud.com/ff778089-e534-437e-a010-9f5bc9a42ea220220706161034.png) ##### 4.2.5 跳跃表 - 跳跃表是Redis特有的数据结构,就是在链表的基础上,增加多级索引提升查找效率。 - 跳跃表支持平均 O(logN),最坏 O(N)复杂度的节点查找,还可以通过顺序性操作批量处理节点。 ![](//img1.jcloudcs.com/developer.jdcloud.com/93f07f6c-d5e6-4303-9720-a6e5e018015320220706161150.png) #### 4.3 合理的数据编码 Redis 支持多种数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是redis设计者总结优化的结果。 - String:如果存储数字的话,是用int类型的编码;如果存储非数字,小于等于39字节的字符串,是embstr;大于39个字节,则是raw编码。 - List:如果列表的元素个数小于512个,列表每个元素的值都小于64字节(默认),使用ziplist编码,否则使用linkedlist编码 - Hash:哈希类型元素个数小于512个,所有值小于64字节的话,使用ziplist编码,否则使用hashtable编码。 - Set:如果集合中的元素都是整数且元素个数小于512个,使用intset编码,否则使用hashtable编码。 - Zset:当有序集合的元素个数小于128个,每个元素的值小于64字节时,使用ziplist编码,否则使用skiplist(跳跃表)编码 #### 4.4 合理的线程模型 首先是单线程模型-避免了上下文切换造成的时间浪费,单线程指的是网络请求模块使用了一个线程,即一个线程处理所有网络请求,其他模块该使用多线程,仍会使用了多个线程,使用多线程,如果没有一个良好的设计,很有可能造成在多线程数增加的时候前期吞吐率增加,后期吞吐率反而增长没有那么明显了。 多线程的情况下通常会出现共享一部分资源,当多个线程同时修改这一部分共享资源时就需要有额外的机制来进行保障,就会造成额外的开销 ![](//img1.jcloudcs.com/developer.jdcloud.com/c491c954-60fb-41eb-b4de-6dea86ae597620220706161523.png) 另外一点则是I/O多路复用模型,在不了解原理的情况下,我们类比一个实例:在课堂上让全班30个人同时做作业,做完后老师检查,30个学生的作业都检查完成才能下课。如何在有限的资源下,以最快的速度下课呢? - 第一种:安排一个老师,按顺序逐个检查。先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡住,全班都会被耽误。这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。这种方式只需要一个老师,但是耗时时间会比较长。 - 第二种:安排30个老师,每个老师检查一个学生的作业。 这种类似于为每一个socket创建一个进程或者线程处理连接。这种方式需要30个老师(最消耗资源),但是速度最快。 - 第三种:安排一个老师,站在讲台上,谁解答完谁举手。这时C、D举手,表示他们作业做完了,老师下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。这种方式可以在最小的资源消耗的情况下,最快的处理完任务。 ![](//img1.jcloudcs.com/developer.jdcloud.com/4f48e385-c73d-4f7f-9b60-957354182e1820220706161540.png) 多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。 ### 5 使用场景 ![](//img1.jcloudcs.com/developer.jdcloud.com/09dfbe93-c8cd-488d-aa27-f076c438c19020220706161558.png) ### 6 参考文档 - DB-Engines Ranking https://db-engines.com/en/ranking - Redis benchmarks https://redis.io/topics/benchmarks/ - Redis中的IO多路复用机制 https://www.cnblogs.com/reecelin/p/13538382.html ------------ ###### 自猿其说Tech-JDL京东物流技术与数据智能部 ###### 作者:张令
原创文章,需联系作者,授权转载
上一篇:浅谈分布式事务及解决方案
下一篇:浅谈仓储UI自动化之路
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号