redis源码之:扩容后的dictScan遍历顺序与JDK的concurrentHashMap 扩容机制

news2024/9/23 7:22:43

进入正题前,先来复习下关于2次幂的mod运算
设n为2次幂,数a mod n 等价于 a & n-1
从二进制来看,相当于余数为a省去n最高位左侧的所有位(含最高位),保留n右侧所有低位即为余数
如:a = 7(0000_0111),n=4(0000_0100),通过7 & (4-1) 即0000_0111 & 0000_0011 结果为3(0000_0011)

dictScan方法中,对游标计算下表用了一个位逆序运算的方法,总体的算法如下:

        v |= ~m0;
        /* Increment the reverse cursor */
        v = rev(v);
        v++;
        v = rev(v);

在分析这个算法前,我准备先大概说下这个算法的目的:

1、首先这里有个前提,即dict的长度都是2的幂,且每次扩容都是扩大一倍,即size1 = size0*2。
扩容前,某个下标idx的若干数据,其实就是key mod size0后同余数idx若干key,在扩容后,因为是扩大一倍,因此这些同余的key,就会分为两部分,idx,和idx+size0的两部分。
在这里插入图片描述
2、遍历的时候,如果没有扩容,那么所有的数据,都在旧的空间里,只需要遍历旧的空间ht[0]即可,但是如果扩容了,那么已遍历过的下标的数据,可能有部分会分配到idx+size0的位置,如果按下标顺序遍历的话,可能会大量遍历到重复的数据。

假设扩容前空间长度为4,在遍历完idx为0的数据后,发生了扩容,空间扩大到8,,那么原本idx为0的数据,在扩容后,idx就调整为0+4==4,那么新空间idx为4的数据,都是已经遍历过的数据

3、如何尽可能减少数据的重复遍历呢,是不是可以考虑调整遍历的顺序,从而实现扩容后的遍历,跳过新空间中idx+size0下标的位置:

当size=2:
遍历的下标有:0,1

扩容后size=4:
那么原本0下标的数据,在新空间就对应:0,2
原本1下标的数据,在新空间就对应:1, 3
那么按0,2,1,3的顺序遍历,则可在扩容后,跳过已经遍历的数据。

扩容后size=8:
那么按原size=4:0,2,1,3的顺序,新扩容后的空间对应关系就有:
0对应新空间:0,4
2对应新空间:2,6
1对应新空间:1, 5
3对应新空间:3, 7
那么此时的遍历顺序则为0,4,2,6,1,5,3,7

因此,遍历的逻辑为,给定一个下标,在新size下的访问顺序,要按旧size做为模同余的key的顺序进行遍历,二进制的演进如下:
(以下:cursor % size 余数idx小于size/2的称为低位槽,大于或等于size/2小于size的称为高位槽; 掩码位: 即数a,与掩码位数相同的位。)
当size为2时,掩码为1,二进制表示为1,掩码位只有一位;
下标的标识分别为:0,1

当size为4时,掩码为3,二进制表示为11,掩码位两位:
那么原本的下标0,1,调整掩码位后为00 , 01
调整size1 = size0*2后的,原本的idx,分别产生idx与idx+size0两个下标,二进制表示则为下标二进制表示的掩码位的高位(最右的0)置1,如:
00, 高位的0置1,成10,值即为2;
01, 高位的0置1,成11,值即为3.
输出:00,10,01,11

当size为8时,掩码为7,二进制表示为111,掩码位三位:
原本下标为:00,10,01,11,调整掩码位后为000 , 010,001,011
同样掩码位最高位置1:
000 高位的0置1,成100,值即为4;
010 高位的0置1,成110,值即为6
001 高位的0置1,成101,值即为5;
011 高位的0置1,成111,值即为7;
此时的遍历顺序则为0,4,2,6,1,5,3,7

从0到4,从2到6,从1到5,从3到7是比较容易理解的,但是如果从4到2,从6到1呢:从上面的顺序可以看出,在旧空间同余的key,在新空间表现为低位槽,高位槽交替,也就说说如何从高位槽,遍历到下一个低位槽:
可以看出,下一个低位槽, 其实就是上一个低位槽在旧空间时的高位槽,比如长度为8时,低位槽为0(000),高位槽为4(100),那么4的下一个低位槽2(010),其实是长度为4时,低位槽0的高位槽。也就是说,要从高位槽遍历到下一个低位槽,就要将当前掩码位的最高位置位0,然后将次高位置1,意思就是找在扩容前的低位槽2的高位槽,如:高位槽100,则变成010这个低位槽,而这个低位槽则是掩码只有两位时,即是扩容前的低位槽00 的高位槽。
如果本身低位就是2(010),高位槽是6(110),当6掩码位最高位置0后的010在旧空间也是高位槽那么再找下一个低位槽,就要最高位次高位都置0,次次高位置1即为001,以此类推。

也就是说,对于数a, 掩码位的1,表示以这个位作为掩码位最高位时,这个数都是高位槽
如:7(0000_0111) 掩码位111,最高位的1表示掩码位是三个位(size=8)时,该数是高位槽,次高位的1,表示掩码位是两个位(size=4)时,该数是高位槽,次次高位的1,表示掩码位是两个位(size=2)时,该数是高位槽

至此,也只是理论上,实现了调整遍历顺序,达到减少重复遍历的效果,但是怎么用位运算实现上面的理论呢,采用递归的方法,可以实现高位槽低位槽的切换,不过Redis的dictScan方法中使用了一种逆序运算的方法:

1、将下标逆序,从大端编码变成小端编码,如001,转换成100;
2、逆序后+1,100加1后为101
3、再逆序回来,变回大端编码,转换为101,此时的值就是下一个需要遍历的下标

dictScan的这个算法,实现了给掩码位的最高位置1,但是,神奇的事也发生了,使用同样的方式,竟然可以继续从高位槽遍历到下一个低位槽(即掩码次高位置1)。如:

101逆序后仍为101-》逆序后+1,值为110-》110再逆序回来,成011

前面这一大段的内容,就是描述下面这三行代码的逻辑

v = rev(v);
v++;
v = rev(v);

当然实际计算机位运算中,没法直接就知道只运算掩码覆盖的这几位,那么这三行代码前面那句v |= ~m0;就是通过游标与掩码的反码做或运算,标识需要运算的掩码覆盖的位,比如:

size为4,掩码为3,二进制0000 0011,游标为1,二进制0000 0001
v|= ~m0,即为0000 0001 | 1111 1100,得到v = 1111 1101
逆序后为 v=1011 1111
逆序后+1为 v = 1100 0000
再次逆序 v = 0000 0011,值为3,遍历1之后的下一个游标为3.

二进制的运算就是这样总是充满意想不到的神奇。

二、JDK的concurrentHashMap 的扩容
不同于dict扩容时通过ht[0],ht[1]渐进式的扩容迁移,concurrentHashMap 扩容采用表示低位链(由低位节点组成)、高位链(由高位节点构成)在扩容后,直接按高位链低位链迁移到高位idx和低位idx,但是思想是差不多的。不过concurrentHashMap 在迁移时会对扩容前的下标对应的node上锁:可以参考我之前的笔记concurrentHashMap 1.8笔记
这里重点回顾下相似的位运算逻辑判断高位节点和低位节点:

int b = hash & n; 假设n = 8 0000_1000 记值为1的位为runbit为3(右往左从第0位开始数第三位)
hash1 = 7, 0000_0111 & 0000_1000 -> runbit位值为0
hash2 = 15, 0000_1111 & 0000_1000 -> runbit 位值为1
hash3 = 23, 0001_0111 & 0000_1000 -> runbit 位值为0
即runbit 位为1时,肯定是余数+奇数倍的n, runbit 位为0时,肯定是余数+偶数倍的n
以上三个hash的 mod 8均取余数 m = 7 (或 hash &(n-1))
即hash = a*n + m ;
a为偶数时,hash & n 的runbit位为0,记为低位节点hash
a为奇数时,hash & n 的runbit位为1,记为高位节点hash
扩容后,n<<2 为16,0001_0000
低位节点hash mod 16,与扩容前 hash mod 8 两者取余 m 相等 高位节点hash mod16,扩容后取余 等于 m + 8(即 m + 扩大的容量n)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/690041.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux挂载详解

Linux 系统中“一切皆文件”&#xff0c;所有文件都放置在以根目录为树根的树形目录结构中。在 Linux 看来&#xff0c;任何硬件设备也都是文件&#xff0c;它们各有自己的一套文件系统&#xff08;文件目录结构&#xff09;。 因此产生的问题是&#xff0c;当在 Linux 系统中使…

高考完,报了个嵌入式专业~~

正文 大家好&#xff0c;我是bug菌~ 最近高考成绩陆续公布&#xff0c;也是看到了一家欢喜一家忧心&#xff0c;确实像我们在外面打拼过的这群人&#xff0c;就深有体会:"高考确实是人生的一道分水岭"。 不过今天这里主要是介绍一下嵌入式行业相关的一些专业学校&…

一文掌握设计模式(定义+UML类图+应用)

一、引子 从学编程一开始就被告知&#xff0c;要想做一名优秀的程序员两大必要技能&#xff1a;1.源码阅读(JDK、C等底层语言封装) 2.设计模式(使用某种语言优雅的落地典型场景功能)。一般随着工作年限的增长&#xff0c;被迫对底层语言/框架源码阅读的越来愈多&#xff0c;但是…

软件的演练场景编排的主要作用是什么?

在软件开发过程中&#xff0c;演练场景编排是一种重要的方法&#xff0c;旨在提供一个模拟真实环境的平台&#xff0c;帮助开发团队和用户测试和验证软件的功能、性能和适应性&#xff0c;那软件的演练场景编排的主要作用是什么&#xff1f; 软件演练场景编排是指通过创建特定的…

【Vue2.0源码学习】实例方法篇-数据相关方法

文章目录 0. 前言1. vm.$watch1.1 用法回顾1.2 内部原理 2. vm.$set2.1 用法回顾2.2 内部原理 3. vm.$delete3.1 用法回顾3.2 内部原理 0. 前言 与数据相关的实例方法有 3 个&#xff0c;分别是vm.$set、vm.$delete和vm.$watch。它们是在stateMixin函数中挂载到Vue原型上的&am…

MMDeploy SDK使用记录(ncnn)

参考&#xff1a;mmpose/projects/rtmpose at main open-mmlab/mmpose GitHub MMDeploy 提供了一系列工具&#xff0c;帮助我们更轻松的将 OpenMMLab 下的算法部署到各种设备与平台上。目前&#xff0c;MMDeploy 可以把 PyTorch 模型转换为 ONNX、TorchScript 等和设备无关的…

RabbitMQ 能保证消息可靠性吗

系列文章目录 消息队列选型——为什么选择RabbitMQ RabbitMQ 五种消息模型 RabbitMQ 能保证消息可靠性吗 系列文章目录前言一、消息可靠性的定义二、几种不可靠的场景三、防意外丢失1. 消息持久化2. 队列持久化3. 发布确认3.1 简单发布确认3.2 批量发布确认3.3 异步发布确认 4…

Vector - CAPL - CAPL入门 - 01

前面已经介绍了很多CAPL相关的函数极其应用&#xff0c;今天CAPL能够完成的功能来介绍在车载网络测试中都能够帮助测试工程师完成哪些工作&#xff1f;让我们对它有一个最基础的认识。 CAPL在总线中的应用 > 分析特定消息或特定数据 > 分析数据流量 > 创建和修改工具…

智慧班牌系统源码,相关技术:springboot,elmentui ,Quartz,jpa,jwt

电子班牌系统的主要功能包括&#xff1a;班级管理、学生信息管理、教师管理、课程管理、作业管理、考试管理、公告管理、评价管理、学校消息发布等。在班级管理方面&#xff0c;该系统可以实现教师对班级的整体管理以及学生个人信息的管理&#xff0c;包括个人信息、考试成绩、…

【Java】Java核心 72:XML (上)

文章目录 1 XML概述什么是XMLXML作用 2 编写第1个XML文件需求效果步骤 3 XML的组成&#xff1a;声明和元素XML组成文档声明元素&#xff08;标签、标记&#xff09; 4 XML的组成&#xff1a;属性、注释和转义字符属性的语法注释转义字符[实体字符]小结 1 XML概述 什么是XML 英…

rabbitmq设置允许外部访问

rabbitmq默认端口为15672,用户名和密码都为guest,是不允许外部访问的. 允许外部访问设置需要操作两步: 第一步:添加其它用户,guest只能用于本机 第二步:Virtual Host允许添加的用户访问,点击下图红色部分. spring配置 spring:rabbitmq:host: 192.168.101.57port: 5672username…

idea中有个目录不显示,磁盘中是有的

java项目src下有个目录data不显示 通过打开D盘看目录是有的&#xff0c;运行项目的时候报错&#xff0c;找不到目录下的文件。 解决方案&#xff1a; idea -> file -> seetings -> EDitor -> file types 打开页面后右侧显示有ignore files and folders 查看这里面有…

【Visual Studio】关于rc文件预处理器宏

问题 VS工程调试遇到一个问题&#xff1a;明明在 项目\属性&#xff0c;C/C\预处理器 页面定义了宏&#xff0c;为什么rc编译时没有影响&#xff1f; 百度后发现&#xff0c;和下方链接中问题很相似。 https://bbs.csdn.net/topics/50485796https://bbs.csdn.net/topics/50…

【运维】查询数据库每张表的数据及索引占用大小

【SQL】查询数据库每张表的数据及索引占用大小 SELECTa.*,CONCAT( a.总大小 / 1024000000, G ) 总大小G FROM(SELECTTABLE_SCHEMA,TABLE_NAME,sum( DATA_LENGTH ) 数据大小,sum( INDEX_LENGTH ) 索引大小,( sum( DATA_LENGTH ) sum( INDEX_LENGTH ) ) 总大小FROMinformation_s…

C# [unity]求顶点数量不等的两条曲线的中线

好久没写了.最近在尝试重写lgsvl导入地图数据的方式,地图同学提供的opendrive车道线计算不准,所以直接让他们导出经纬度的高精地图json数据,但是这种数据只有车道边界线,没有车道中心线, 基于只是想小改而非大改的前提下,还是要算出车道中心线.搞个小demo传上来,代码写的很拙劣…

宝塔定时任务实现磁盘使用率超阀值后自动发送邮件

服务器磁盘使用空间不足会产生各种不可预知的灾难&#xff0c;服务器上的应用几乎全部不能用&#xff0c;如果没有遇到过磁盘占满的问题&#xff0c;可能很难发现它。 步骤 安装邮件发送工具sendEmail磁盘检测并发送邮件shell脚本宝塔配置计划任务 安装邮件发送工具sendEmail …

【ROS】TF2坐标转换及实战示例

Halo&#xff0c;这里是Ppeua。平时主要更新C&#xff0c;数据结构算法…感兴趣就关注我吧&#xff01;你定不会失望。 文章目录 0.ROS中的坐标转换消息包0.1 geometry_msgs/TransformStamped0.2 geometry_msgs/PointStamped1.静态坐标转换1.1导入所需功能包1.2发布方实现1.3 …

多元分类预测 | Matlab粒子群算法(PSO)优化极限学习机(ELM)的分类预测,多特征输入模型。PSO-ELM分类预测模型

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 多元分类预测 | Matlab粒子群算法(PSO)优化极限学习机(ELM)的分类预测,多特征输入模型。PSO-ELM分类预测模型 多特征输入单输出的二分类及多分类模型。程序内注释详细,直接替换数据就可以用。程序语言为matlab,…

DALL-E2原理解读——大模型论文阅读笔记五

论文&#xff1a;https://cdn.openai.com/papers/dall-e-2.pdf 项目&#xff1a;https://openai.com/dall-e-2 一. 主要思想 利用CLIP提取的文本特征&#xff0c;级联式的生成图片。第一阶段通过prior将文本特征与图像特征进行对齐&#xff0c;第二阶段用扩散模型将视觉特征转…

简单demo演示Tomcat中Servlet

挺好玩的,有利于初学对容器和servlet接口规范的理解 具体代码 package org.apache;import javax.servlet.Servlet; import java.io.FileReader; import java.io.IOException; import java.util.Properties; import java.util.ResourceBundle; import java.util.Scanner;/*** a…