02 | 数据结构:快速的Redis有哪些慢操作?
02 | 数据结构:快速的Redis有哪些慢操作?
讲述:蒋德钧
时长19:07大小17.50M
键和值用什么结构组织?
为什么哈希表操作变慢了?
集合数据操作效率
有哪些底层数据结构?
不同操作的复杂度
小结
每课一问
赞 392
提建议
精选留言(244)
- Kaito2020-08-04两方面原因: 1、内存利用率,数组和压缩列表都是非常紧凑的数据结构,它比链表占用的内存要更少。Redis是内存数据库,大量数据存到内存中,此时需要做尽可能的优化,提高内存的利用率。 2、数组对CPU高速缓存支持更友好,所以Redis在设计时,集合数据元素较少情况下,默认采用内存紧凑排列的方式存储,同时利用CPU高速缓存不会降低访问速度。当数据元素超过设定阈值后,避免查询时间复杂度太高,转为哈希和跳表数据结构存储,保证查询效率。展开
作者回复: 回答的很好! 我再提个小问题,如果在数组上是随机访问,对CPU高速缓存还友好不?:)
共 46 条评论488 - 注定非凡2020-08-25一,作者讲了什么? 1,Redis的底层数据结构 二,作者是怎么把这事给讲明白的? 1,讲了Redis的数据结构:数据的保存形式与底层数据结构 2,由数据结构的异同点,引出数据操作的快慢原因 三,为了讲明白,作者讲了哪些要点?有哪些亮点? 1,亮点1:string,list,set,hast,sortset都只是数据的保存形式,底层的数据结构是:简单动态字符串,双向链表,压缩列表,哈希表,跳表,整数数组 2,亮点2:Redis使用了一个哈希表保存所有的键值对 3,要点1:五种数据形式的底层实现 a,string:简单动态字符串 b,list:双向链表,压缩列表 c,hash:压缩列表,哈希表 d,Sorted Set:压缩列表,跳表 e,set:哈希表,整数数组 4,要点2:List ,hash,set ,sorted set被统称为集合类型,一个键对应了一个集合的数据 5,要点3:集合类型的键和值之间的结构组织 a:Redis使用一个哈希表保存所有键值对,一个哈希表实则是一个数组,数组的每个元素称为哈希桶。 b:哈希桶中的元素保存的不是值的本身,而是指向具体值的指针 6,要点4:哈希冲突解决 a:Redis的hash表是全局的,所以当写入大量的key时,将会带来哈希冲突,已经rehash可能带来的操作阻塞 b:Redis解决hash冲突的方式,是链式哈希:同一个哈希桶中的多个元素用一个链表来保存 c:当哈希冲突链过长时,Redis会对hash表进行rehash操作。rehash就是增加现有的hash桶数量,分散entry元素。 7,要点5:rehash机制 a:为了使rehash操作更高效,Redis默认使用了两个全局哈希表:哈希表1和哈希表2,起始时hash2没有分配空间 b:随着数据增多,Redis执行分三步执行rehash; 1,给hash2分配更大的内存空间,如是hash1的两倍 2,把hash1中的数据重新映射并拷贝到哈希表2中 3,释放hash1的空间 8,要点6:渐进式rehash a:由于步骤2重新映射非常耗时,会阻塞redis b:讲集中迁移数据,改成每处理一个请求时,就从hash1中的第一个索引位置,顺带将这个索引位置上的所有entries拷贝到hash2中。 9,要点7 :压缩列表,跳表的特点 a:压缩列表类似于一个数组,不同的是:压缩列表在表头有三个字段zlbytes,zltail和zllen分别表示长度,列表尾的偏移量和列表中的entry的个数,压缩列表尾部还有一个zlend,表示列表结束 所以压缩列表定位第一个和最后一个是O(1),但其他就是O(n) b:跳表:是在链表的基础上增加了多级索引,通过索引的几次跳转,实现数据快速定位 四,对于作者所讲,我有哪些发散性思考? 五,在将来的哪些场景中,我能够用到它? 六,评论区收获 1,数组和压缩列表可以提升内存利用率,因为他们的数据结构紧凑 2,数组对CPU高速缓存支持友好,当数据元素超过阈值时,会转为hash和跳表,保证查询效率展开共 23 条评论455
- 我的黄金时代2020-08-19来自互联网 :因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。 另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。展开共 7 条评论107
- 张德2020-08-24同问 老师 如果渐进式哈希操作 如果有一个value操作很长时间段都没被查到 那渐进式哈希会持续非常长的时间吗 还是会在空闲的时候 也给挪到扩容的hash表里面
作者回复: 渐进式rehash执行时,除了根据键值对的操作来进行数据迁移,Redis本身还会有一个定时任务在执行rehash,如果没有键值对操作时,这个定时任务会周期性地(例如每100ms一次)搬移一些数据到新的哈希表中,这样可以缩短整个rehash的过程。
共 8 条评论93 - 不能扮演天使2020-08-03Redis的List底层使用压缩列表本质上是将所有元素紧挨着存储,所以分配的是一块连续的内存空间,虽然数据结构本身没有时间复杂度的优势,但是这样节省空间而且也能避免一些内存碎片;
作者回复: 赞!所以不同的设计选择都是有背后的考虑。
共 5 条评论70 - 那时刻2020-08-06请问老师关于zset的问题,您在文中提到Sorted Set内部实现数据结构是跳表和压缩列表。但是我从zset代码看到这样的注释 The elements are added to a hash table mapping Redis objects to scores. At the same time the elements are added to a skip list mapping scores to Redis objects 按照注释应该还有hash table来额外存储数据吧,这样在zset里查找单个元素,可以从O(logN)降低为O(1)。不知道我理解的是否正确?展开
作者回复: 看得非常仔细,赞一个! zset有个ZSCORE的操作,用于返回单个集合member的分数,它的操作复杂度是O(1),这就是收益于你这看到的hash table。这个hash table保存了集合元素和相应的分数,所以做ZSCORE操作时,直接查这个表就可以,复杂度就降为O(1)了。 而跳表主要服务范围操作,提供O(logN)的复杂度。
共 5 条评论53 - 宙斯2020-12-14老师你好,hash表是全局的,这里的全局怎样理解?是‘存放key的数组全局只有一个‘是这样理解吗?
作者回复: 这个全局是指Redis数据库中的所有key和value,是由一个哈希表来索引的。通过在这个哈希表中查询key,就可以找到对应的value。然后根据value的具体类型(例如Hash,Set,List等),再通过value的底层数据结构来读取具体的value数据,例如List通过双向链表来读取数据。
27 - 刘忽悠2020-08-03redis底层的数据压缩搞的很细致,像sds,根据字节长度划分的很细致,另外采用的c99特性的动态数组,对短字符串进行一次性的内存分配;跳表设计的也很秀,简单明了,进行范围查询很方便;dict的扩容没细看,但是看了一下数据结构,应该是为了避免发生扩容的时候出现整体copy; 个人觉得老师应该把sds,dict等具体数据结构的源码也贴上
作者回复: dict的渐进式rehash是为了避免扩容时的整体拷贝,这会给内存带来较大压力。 SDS我们后面还会再聊:)
共 2 条评论21 - 米 虫2020-08-04要是加餐中来一偏,一个redis指令的执行过程,那大局观就更深刻了。
作者回复: 我们后面的课程会涉及这个过程,再耐心等等哈
19 - 牛牛2020-08-13老师、早安, 今天的问题 @Kaito 同学回答的很好、尝试回答下: 数组上随机访问是否对cpu缓存友好 ? 数组对cpu缓存友好的原因是: cpu预读取一个cache line大小的数据, 数组数据排列紧凑、相同大小空间保存的元素更多, 访问下一个元素时、恰好已经在cpu缓存了. 如果是随机访问、就不能充分利用cpu缓存了, 拿int元素举例: 一个元素4byte, CacheLine 假设64byte, 可以预读取 16个挨着的元素, 如果下次随机访问的元素不在这16个元素里、就需要重新从内存读取了. 想请教几个问题: 1. rehash是将访问元素所在索引位置上的所有entries都 copy 到 hash表2 ? 如果有索引位置一直没访问到、它会一直留着 hash表1 中 ? 如果是, 再次rehash时这部分没被挪走的索引位置怎么处理 ? 如果不是、那什么时候(时机)触发这部分位置的rehash呢 ? 2. rehash过程中、内存占用会多于原所占内存的2倍 ? (ht2的内存是ht1的2倍, 原ht1的空间还未释放) 我记得redis设计实现上说 ht2的内存会与ht1实际使用的键值对的数量有关, 扩容好像是 >= ht1.used * 2 的第一个 2^n; 缩容好像是 >= ht1.used 的第一个 2^n 3. rehash完成之后, hash表1 留作下次rehash备用、但会把占用的内存释放掉, 这么理解对不 ? 4. rehash时 为什么是 `复制`, 而不是 `移动`, 这个是有什么考虑吗 ? 我的理解: 移动需要释放原空间, 每个元素都单独释放会导致大量的碎片内存、多次释放也比一次释放效率更低. 不知道是不是考虑错了~~~展开共 11 条评论14
- dingjiayi2020-08-05老师好,请问 rehash 期间,新的 操作请求(增删改查)到达,redis 是如何处理的?共 6 条评论13
- 小飞同学2020-08-04小问题: 1.rehash的时候为啥存储位置会发生变化?一个对象的hashCode始终是一样的。 还是说rehash是对槽进行取模 2.跳表找元素没太看明白,是二分查找么?怎么感觉找33那个元素不止3次 课后一问: 因为数组占用内存连续,不需要随机读取。同时碎片化问题也不需要考虑。 希望得到老师的解答,谢谢展开
作者回复: 一个对象保存到哈希桶时,实际是要用它的hash值对哈希桶个数取模的,所以rehash时,桶个数发生了变化,那么对象的存储位置也会有所变化。 跳表的查找通过不同层的指针来跳转,指针比较多时,类似于二分查找了。
9 - 曾轼麟2020-08-03intset 和 ziplist 如果直接使用确实是时间复杂度上不是很高效,但是结合Redis的使用场景,大部分Redis的数据都是零散碎片化的,通过这两种数据结构可以提高内存利用率,但是为了防止过度使用这两种数据结构Redis其实都设有阈值或者搭配使用的,例如:ziplist是和quicklist一起使用的,在quicklist中ziplist是一个个节点,而且quicklist为每个节点的大小都设置有阈值避免单个节点过大,从而导致性能下降9
- QFY2020-08-06老师,关于rehash那里有两个问题: 1.redis切换全局hash表是否是在将原有hash表的全部内容拷贝完成后切换 2.如果在rehash过程中,已拷贝过的位置后面又有新的冲突值过来了该怎么办共 6 条评论8
- 刘忽悠2020-08-03至于问题答案,采用压缩列表或者是整数集合,都是数据量比较小的情况,所以一次能够分配到足够大的内存,而压缩列表和整数集合本身的数据结构也是线性的,对cpu的缓存更友好一些,所以真正的执行的时间因为高速缓存的关系,速度更快
作者回复: 提个小问题哈,如果压缩列表也是随机跳着访问,对CPU缓存还友好不?
8 - 游弋云端2020-08-11Redis 采用了渐进式 rehash,那么什么时候进行新的全局Hash表的切换呢?当旧的Hash表格的负载因子达到一定值的时候?共 3 条评论6
- Nemo2020-08-07各种数据结构访问和修改的时间复杂度:https://www.bigocheatsheet.com/共 1 条评论6
- 二哥不再迷茫2021-01-15跳表快速查询33过程中疑问, 一级索引, 1, 在一级索引中比较1和33的大小,1小于33, 2,继续比较11和33的大小,11比33小, 3,继续比较27和33的大小, 27比33小, 4,继续比较50和33的大小,50比33大, 5,从33位置进入底层数据,比较33和33大小,相等,找到33. 5次比较大小,应该是5次查找到33吧。 同理二级索引需要5次查找到33。 不是应该这样吗?老师展开共 2 条评论5
- Lywane2020-08-17rehash不仅全局有,单独的值为HASH类型的数据也会有吧
作者回复: 是的,哈希表这个结构,整个数据库空间用它来保存键值对,同时HASH类型的键值对也用它作为底层结构。所以,rehash也是两种情况下都有的。
5 - 陈靖2020-08-10老师的项目,是要把这些数据结构全部撸一遍吗?5