MySQL的代码中实现了一个Lock Free的Hash结构,称作LF_Hash。MySQL的不少模块使用了LF_Hash,比如Metadata Lock就依赖于它。但由于使用的方法不正确,导致了bug#98911和bug#98624。理解LF_Hash的实现细节,可以帮助我们用好LF_Hash。

LF_HASH的基本特点动态扩展

初始化时bucket的数量是1. 每个bucket平均拥有的元素(Element)是1个。因此当元素的总数量超过bucket的数量时,就会自动分裂。每次分裂增加一倍的buckets.Lock Free

lf_hash采用Lock Free的方式实现,为了保证多线程操作的安全。lf_hash实现了一个叫做pin的东西来保证多线程操作的安全性。lf_hash的操作都需要通过pin来保护。因此lf_hash提供了获取pin和释放pin的函数。lf_hash自己维护了一个pin的动态数组。

内存管理

lf_hash元素的内存都是lf_hash分配和管理的。用户的数据需要拷贝到lf_hash创建的元素中。

LF_HASH的基本操作

插入元素

// 获取一个LF_PINS对象LF_PINS *pins = lf_hash_get_pins();// 给元素分配内存,并拷贝用户数据到元素中,并插入到Hash链表中lf_hash_insert(lf_hash_object, pins, user_data);// 释放LF_PINS对象lf_hash_put_pins(pins);

删除元素

// 获取一个LF_PINS对象LF_PINS *pins = lf_hash_get_pins();// 删除指定key的元素lf_hash_delete(lf_hash_object, pins, key, key_length);// 释放LF_PINS对象lf_hash_put_pins(pins);

查询元素

// 获取一个LF_PINS对象LF_PINS *pins = lf_hash_get_pins();// 返回指定key的第一个元素,这个元素对象会被pin住,使用完要unpin。// 被pin住的元素不能被其他线程从hash链表中移除el = lf_hash_search(lf_hash_object, pins, key, key_length);// 使用查找到的元素。...// unpin当前元素lf_hash_search_unpin(pins);// 释放LF_PINS对象lf_hash_put_pins(pins);

LF_HASH的基本结构

lf_hash的基本结构如下图所示:所有的元素维护在一个全局排序链表里

同一个bucket的所有元素排在一起

每个bucket有一个指针,指向这个bucket的所有元素的Head

元素排序

为了能够做到将每个bucket的元素排列到一起,lf_hash根据元素hash的反转值进行排序。并且要求bucket的数量必须是2的倍数。

元素Hash的反转值

和其他Hash Table一样, LF_HASH也是通过hash(key)得出一个32bits的整数值(hashnr),这个值决定了元素属于哪一个bucket.

hashnr = hash(key);// size是bucket的数量bucket_id = hashnr % LF_HASH::size; bucket_id从0开始。

Hash的反转值是指将Hash的所有Bits的顺序颠倒过来。例如

// 为了表示方便,这里假设hashnr是8位的,按8位反转// 实际使用是32位的,按32位反转0 -> 00000000 -> 00000001 -> 00000001 -> 10000002 -> 00000010 -> 0100000

排序特点

LF_HASH的全局排序链表看起来是这样的:

为了书写方便,假设hash值的长度是8bit.

这个链表是按hash值的反向bit位排序的,因此最低位为0的排在一起,为1的排在一起。

最低位相同的元素,又按第二低位排序。第二低位相同的,按第三低位排序。

hash值相同的按hash key排序(这个不是重点,这里可以忽略)。

Bucket的数量必须是2的倍数

当bucket的数量是2的倍数时我们会发现当bucket size是1时,所有元素会分到同一个bucket中。

当bucket size是2时,最低1位相同的元素会分到同一个bucket中。

当bucket size是4时,最低2位相同的元素会分到同一个bucket中。

bucket每扩展1倍,多1bit用来分bucket.

这个规律使得每个bucket的元素在全局链表中排列在一起。

如果将bucket id反转,我们会发现全局链表是按照元素的 bucket id的反转值分bucket的。bucket id的反转值就是当前bucket的里的最小值。当bucket size是1时,所有的元素在bucket 0中。

当bucket size是2时,按照hash值的最低位(反转值的最高位)分bucket,0的分在bucket 0中,1分在bucket 中。排序规律符合要求,bucket 0和1的元素分别排列在一起。

当bucket size是4时,按照最低2位的值分bucket,00的分在bucket 0, 01分在bucket 2中。10排在bucket 1中,11排在bucket 3中。排序规律要求,每个bucket的元素仍然是排列在一起的。

因此以2的倍数来扩展lf_hash的bucket时全局链表不需要任何变动

原有的buckets不需要变动

只需要将新的buckets指向自己的第一个元素。

Bucket Parent

你可能已经注意到了,按2的倍数扩展。实际上就是将原bucket能容纳的排序值的范围分成两半。前一半保留在原来的bucket中,后一半放到一个新bucket中。lf_hash中称这个被分裂的bucket为parent。Parent bucket是固定的,根据bucket id可以算出parent. 对于bucket id的反转值来说,是将低位的1清零。

对bucket id来说,就是将高位的1清零。

uint parent = my_clear_highest_bit(bucket);

Dummy 元素

每个Bucket中都是一个指针,指向全局链表中这个bucket的最小元素,即head。为了避免这个指针随着head的变化而变化:初始化一个bucket时会生成一个dummy元素,把dummy元素插入到全局链表中。

dummy元素的hash指定为bucket id。

bucket id的反转值是bucket中所有元素的最小值。所以dummy元素始终是这个bucket的链表的head。bucket的指针将始终指向这个dummy元素。

区分用户元素和Dummy元素

用户元素的hash值可能会等于bucket id,为了避免将这个元素插到dummy元素的前面(lf_hash中用的是前插)。lf_hash会将用户元素的的hash反转值的最低位变为1。这样就保证了dummy元素的hash反转值最小且唯一。

元素管理

为了Lock Free, lf_hash自己管理元素的内存分配。

元素结构

lf_hash的元素使用一块连续的内存,包含两部分信息:LF_SLIST 链表和hash相关的信息

用户数据。放在LF_SLIST之后,

LF_SLISTlink:        指向链表中的下一个元素

hashnr    hash的反转值

key         指针指向key值

LF_ALLOCATOR

LF_ALLOCATOR负责元素的管理。

LF_ALLOCATOR::top

Hash链表中的元素被删除后,并不会被释放(free)掉。它们会被放到一个链表中(lf_hash中称作栈),top指向链表中的第一个元素(栈顶)。当向Hash链表中插入一个元素时,会从这个链表中取一个元素使用。如果没有可供使用的元素,才会通过my_malloc分配一个新的。

用LF_SLIST::key指向下一个元素

这里要注意的一点是,这个链表是使用LF_SLIST::key连在一起的。为什么不使用LF_SLIST::link呢?那是因为,是因为lf_hash lock free的设计。

问题

除非Destroy整个Hash,LF_ALLOCATOR中未使用的元素是不会释放的。如果这个HASH链表在某个时刻特别大,占用内存特别多。这些内存就会一直被占用,直到整个Hash被释放掉。

PIN的机制

Lock Free意味着多个线程可能同时在使用一个元素。一个元素从全局链表中移除后,不能被立刻放入到LF_ALLOCATOR::top 指向的Free元素链表中。别的线程可能正在使用这个元素。如果此时放到free链表中,又被别的线程重用了,就可能会造成错误。lf_hash用LF_PINS来保护一个正在使用的元素不被删除或者重用。我们可以将PIN想象成一个锁。

LF_PINS::pin

std::atomic pin[LF_PINBOX_PINS];

pin包含4个指针,可以同时引用4个元素,看代码中最多用了3个。这是因为lf_hash链表在操作的过程中最多可以使用到连续的三个元素previous, current, next。这3个元素要同时pin住。

线程在将一个元素放入Free元素链表之前,要检查所有的pin。如果有任何pin引用了这个元素,则要等待这个元素的引用被取消后才能继续操作。

LF_PINS::purgatory

如果并发的线程很多,遍历所有的pin就会消耗较长的时间。因此lf_hash并不是每删除一个元素做一次遍历操作。而是对多个要删除的元素一起做遍历操作。这些要删除的元素会临时的放入LF_PINS::purgatory链表。只有当purgatory的元素数量到达LF_PURGATORY_SIZE(10个)时或这个pin被释放时,才做一次遍历。没有被引用的元素会被放到LF_ALLOCATOR::top指向的Free 元素链表中去。

当将一个元素放入purgatory时,其他的线程可能正在读取这个元素,也可能正在读取这个元素的LF_SLIST::link。因此puragory链表使用LF_SLIST::key将要purge的元素链接到一起的。难道并发的线程不访问这个元素的LF_SLIST::key吗?会访问,为了能够访问到正确的值,lf_hash有下面这个设计。

删除标记

每个元素都有一个DELETED的标记位,在将元素从全局链表中移除之前,首先要将元素标记为DELETED。看代码时,你可能会迷惑。因为LF_SLIST中,并没有一个DELETED标记位。那是因为DELETED标记位共享了link的最低位。

之所以能够和link共享最低位,是因为link是一个指针指向一个内存地址。内存地址总是4/8字节对齐的,最低位一定是0。

删除的过程找到元素

标记为DELETED

从全局链表中移除

加入purgatory链表,会修改元素的LF_SLIST::key

执行purge过程,如果purgatory链表有10个元素。

查找元素的过程pin当前元素

拷贝元素的hash key指针到临时变量,会读取LF_SLIST::key

检查元素是否是DELETED,如果是则移动到下一个元素。

比较元素的hashnr和key,如果hashnr和key都小于要查找的hashnr和key则,移动到下一个元素。

可以看到,删除的过程中是先标记DELETED,然后修改LF_SLIST::key。而在查找元素时,是先拷贝LF_SLIST::key,然后检查DELETED标记。这就保证了查找中使用的key是正确的key。

LF_PINBOX

Pinbox是pin的管理器,所有的pin放在一个动态数据里。pinarray pin的动态数组

LF_PINBOX::pinstack_top_ver

和LF_ALLOCATOR::top类似,pinstack_top_ver指向free pin的链表(栈)。但它存储的不是指针,而是第一个元素在pinarray中的index. LF_PINS::link用来指向下一个pin在pinarray中的index。

当用户调用lf_hash_put_pins()时,会将pin放入这个链表。当调用lf_hash_get_pins()时,会从pinstack_top_ver取出一个free pin。如果free pin的链表是空的(top是0),则会给pinarray中增加一个元素。

top version

LF_ALLOCATOR::top上的lock free操作是通过Pin来保护。那么LF_PINBOX::pinstack_top_ver上的lock free操作又是做到的呢? 为了做到lock free, LF_PINBOX::pinstack_top_ver上使用了version的方法。

每次操作free pin链表时,都会将version加1。在做atomic_compare_exchange操作时,pinstack_top_ver作为一个整数,整体进行操作。

由于top只有16位,这就限制了pinarray最多只能有LF_PINBOX_MAX_PINS(65535)个元素。

PIN使用上的问题

从pin的设计可以看出,pin的使用原则是保护lf_hash操作本身的。一个操作完成后,pin就可以释放了。MySQL中有些lf_hash的pin是长期持有的。如MDL_context::m_pins,这个pin是在session第一次调用时获取,session退出时才释放。它会导致:session的数量最多只能有65535个

session的数量很大时,导致pinarray很大。因此元素的purge操作效率很低。

前面说过purgatory中的元素到达LF_PURGATORY_SIZE(10个)时或者释放pin时,才会释放。由于这些pin到session结束时才释放,就会导致元素的释放不及时。分配的元素更多,占用内存更多。

动态数组

lf_hash中的bucket和pin都使用了动态数组。为了实现lock free,在动态扩展时不拷贝内存,它做了特殊的设计。

多级数组

这个数组LF_DYNARRAY_LEVELS(4).

LevelIndex范围

00 到 255

1256 到 256*256-1

2256*256 到 256*256*256-1

3256*256*256 到 256*256*256*256-1

0级

0级包含256个指针,指向index 0到255的元素。这些元素初始化时不分配,用到时才分配。

1级

1级包含256个指针,每个指针指向一个0级数组。

2级

2级包含256个指针,每个指针指向一个1级数组。

3级

3级包含256个指针,每个指针指向一个2级数组。

相关资源:mysql错误以及处理方式_mysql语法错误怎么办-MySQL文档类资源...

MySQL源码解读之数据结构-LF_DYNARRAY相关推荐

  1. mysql源码分析——VIO数据结构

    一.VIO数据类型 VIO是一个数据结构,在include/violite.h中定义的说明中有一句话"This structure is for every connection on bo ...

  2. 源码解读_Go Map源码解读之Map迭代

    点击上方蓝色"后端开发杂谈"关注我们, 专注于后端日常开发技术分享 map 迭代 本文主要是针对map迭代部分的源码分析, 可能篇幅有些过长,且全部是代码, 请耐心阅读. 源码位置 ...

  3. ArrayList源码解读

    ArrayList源码解读 底层数据结构 ArrayList是在底层维护了一个elementData数组,添加了自动扩容等功能,最终形成了一个动态数组. 基本属性 // 初始化大小 private s ...

  4. MySQL内核源码解读-SQL解析之解析器浅析

    MYSQL服务器接收SQL格式的查询,首先要对sql进行解析,内部将文本格式转换为二进制结构,这个转换就是解析器,解析的目的是为了让优化器更好的处理指令,以便以最优的路径,最少的耗时返回我们想要的结果 ...

  5. mysql8.0源代码解析_源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统

    原标题:源码解读:MySQL 8.0 InnoDB无锁化设计的日志系统 作者介绍 张永翔,现任网易云RDS开发,持续关注MySQL及数据库运维领域,擅长MySQL运维,知乎ID:雁南归. MySQL ...

  6. 读 MySQL 源码再看 INSERT 加锁流程

    欢迎关注方志朋的博客,回复"666"获面试宝典 在之前的博客中,我写了一系列的文章,比较系统的学习了 MySQL 的事务.隔离级别.加锁流程以及死锁,我自认为对常见 SQL 语句的 ...

  7. 怒肝两个月MySQL源码,我总结出这篇2W字的MySQL协议详解(超硬核干货)!!

    写在前面 最近,在开发一个分库分表中间件,由于功能需求,需要分析MySQL协议,发现网上对于MySQL协议分析的文章大部分都过时了,原因是分析的MySQL版本太低了.怎么办呢?于是乎,我便硬着头皮开始 ...

  8. PyTorch 源码解读之即时编译篇

    点击上方"AI遇见机器学习",选择"星标"公众号 重磅干货,第一时间送达 作者丨OpenMMLab 来源丨https://zhuanlan.zhihu.com/ ...

  9. php service locator,Yii源码解读-服务定位器(ServiceLocator)

    SL的目的也是解耦,并且非常适合基于服务和组件的应用. Service Locator充当了一个运行时的链接器的角色,可以在运行时动态地修改一个类所要选用的服务, 而不必对类作任何的修改. 一个类可以 ...

最新文章

  1. CVPR2020论文解析:视频语义检索
  2. 每日一皮:当我在处理别人的代码时...
  3. 鸿蒙系统的功能如何,华为鸿蒙系统发布会,这个功能怎么那么像小米MIUI的
  4. buildroot的使用简介【转】
  5. Objective-C中的@property
  6. 软件项目管理课后题下载【共5个章(1、3、4、5、6)】
  7. python的整数类型_Python int 数字整型类型 定义int()范围大小转换
  8. 和Hibernate3.6相比,Hibernate 5.x中的增删改性能降低了
  9. oracle日期相减工作日_oracle 日期相减 转载
  10. 2021MySql-8.0.26安装详细教程(保姆级)
  11. FFmpeg 以及帧率的解释
  12. CUDA编程.cu文件
  13. 2019 NLP大全:论文、博客、教程、工程进展全梳理(长文预警)
  14. MySQL 导入、备份
  15. pandas中isin()函数及其逆函数使用
  16. (SWAT-4)SWAT中水文响应单元划分(HRU)分析
  17. 键盘录入 写入文件 quit时 结束
  18. 《Kubernetes知识篇:基于Pod进行资源配额管理》
  19. 遨博机械臂——末端工具ROS驱动
  20. SWF是什么文件,SWF文件用什么软件可以打开 1

热门文章

  1. 技术分享连载(六十一)
  2. Oracle中exp的使用2
  3. Linux—程序包安装与管理
  4. python中__init__函数以及参数self
  5. android怎样封装,如何封装属于自己的博客网站安卓APP 源码家园
  6. kafka tool 查看指定group下topic的堆积数量_ELK架构下利用Kafka Group实现Logstash的高可用...
  7. Python 模块之科学计算 Pandas
  8. 光源时间_缩短背光源的使用寿命的原因
  9. 【python】数据结构和算法 + 浅谈单链表与双链表的区别
  10. 挂载nfs文件系统_综合架构-day38-NFS服务补充