Redis源码---如何实现一个性能优异的Hash表

news2024/11/26 1:29:31

目录

前言

Redis 如何实现链式哈希?

什么是哈希冲突?

链式哈希如何设计与实现?

Redis 如何实现 rehash?

什么时候触发 rehash?

rehash 扩容扩多大?

渐进式 rehash 如何实现?


  • 前言

  • Hash 表是一种非常关键的数据结构,在计算机系统中发挥着重要作用
  • 比如在Memcached 中,Hash 表被用来索引数据
  • 在数据库系统中,Hash 表被用来辅助 SQL 查询
  • 而对于 Redis 键值数据库来说,Hash 表既是键值对中的一种值类型,同时,Redis 也使用一个全局 Hash 表来保存所有的键值对,从而既满足应用存取 Hash 结构数据需求,又能提供快速查询功能
  • 那么,Hash 表应用如此广泛的一个重要原因,就是从理论上来说,它能以 O(1) 的复杂度快速查询数据
  • Hash 表通过 Hash 函数的计算,就能定位数据在表中的位置,紧接着可以对数据进行操作,这就使得数据操作非常快速
  • Hash 表这个结构也并不难理解,但是在实际应用 Hash 表时,当数据量不断增加,它的性能就经常会受到哈希冲突和 rehash 开销的影响
  • 而这两个问题的核心,其实都来自于 Hash 表
  • 要保存的数据量,超过了当前 Hash 表能容纳的数据量
  • 那么要如何应对这两个问题呢?
  • Redis 为我们提供了一个经典的 Hash 表实现方案
  • 针对哈希冲突,Redis 采用了链式哈希,在不扩容哈希表的前提下,将具有相同哈希值的数据链接起来,以便这些数据在表中仍然可以被查询到
  • 对于 rehash 开销,Redis 实现了渐进式 rehash 设计,进而缓解了 rehash 操作带来的额外开销对系统的性能影响
  • 通过学习 Redis 中针对 Hash 表的设计思路和实现方法
  • 帮助掌握应对哈希冲突和优化 rehash 操作性能的能力,并以此支撑你在实际使用 Hash 表保存大量数据的场景中,可以实现高性能的 Hash 表
  • 接下来先来聊聊链式哈希的设计与实现
  • Redis 如何实现链式哈希?

  • 在开始学习链式哈希的设计实现之前,还需要明白 Redis 中 Hash 表的结构设计是啥样的,以及为何会在数据量增加时产生哈希冲突,这样也更容易理解链式哈希应对哈希冲突的解决思路
  • 什么是哈希冲突?

  • 实际上,一个最简单的 Hash 表就是一个数组,数组里的每个元素是一个哈希桶(也叫做Bucket)
  • 第一个数组元素被编为哈希桶 0,以此类推
  • 当一个键值对的键经过 Hash 函数计算后,再对数组元素个数取模,就能得到该键值对对应的数组元素位置,也就是第几个哈希桶
  • 如下图所示,key1 经过哈希计算和哈希值取模后,就对应哈希桶 1,类似的,key3 和 key16分别对应哈希桶 7 和桶 4

  • 从图上还可以看到,需要写入 Hash 表的键空间一共有 16 个键,而 Hash 表的空间大小只有 8 个元素,这样就会导致有些键会对应到相同的哈希桶中
  • 在实际应用 Hash 表时,其实一般很难预估要保存的数据量
  • 如果一开始就创建一个非常大的哈希表,当数据量较小时,就会造成空间浪费
  • 所以,通常会给哈希表设定一个初始大小,而当数据量增加时,键空间的大小就会大于 Hash 表空间大小了
  • 也正是由于键空间会大于 Hash 表空间,这就导致在用 Hash 函数把键映射到 Hash 表空间时,不可避免地会出现不同的键被映射到数组的同一个位置上
  • 而如果同一个位置只能保存一个键值对,就会导致 Hash 表保存的数据非常有限,这就是常说的哈希冲突
  • 比如下图中,key3 和 key100 都被映射到了 Hash 表的桶 5 中,这样,当桶 5 只能保存一个key 时,key3 和 key100 就会有一个 key 无法保存到哈希表中了

  • 那么该如何解决哈希冲突呢?
  • 可以考虑使用以下两种解决方案:
    • 第一种方案,就是接下来要介绍的链式哈希
    • 这里需要先知道,链式哈希的链不能太长,否则会降低 Hash 表性能
    • 第二种方案,就是当链式哈希的链长达到一定长度时,可以使用 rehash
    • 不过,执行rehash本身开销比较大,所以就需要采用稍后会给你介绍的渐进式 rehash 设计
  • 链式哈希如何设计与实现?

  • 所谓的链式哈希,就是用一个链表把映射到 Hash 表同一桶中的键给连接起来
  • 下面就来看看 Redis 是如何实现链式哈希的,以及为何链式哈希能够帮助解决哈希冲突
  • 首先,需要了解 Redis 源码中对 Hash 表的实现
  • Redis 中和 Hash 表实现相关的文件主要是 dict.h 和 dict.c
  • 其中,dict.h 文件定义了 Hash 表的结构、哈希项,以及 Hash 表的各种操作函数
  • 而 dict.c 文件包含了 Hash 表各种操作的具体实现代码
  • 在 dict.h 文件中,Hash 表被定义为一个二维数组(dictEntry **table),这个数组的每个元素是一个指向哈希项(dictEntry)的指针
  • 下面的代码展示的就是在 dict.h 文件中对 Hash表的定义:

  • 那么为了实现链式哈希, Redis 在每个 dictEntry 的结构设计中,除了包含指向键和值的指针,还包含了指向下一个哈希项的指针
  • 如下面的代码所示,dictEntry 结构体中包含了指向另一个 dictEntry 结构的指针 *next,这就是用来实现链式哈希的

  • 除了用于实现链式哈希的指针外,这里还有一个值得注意的地方,就是在 dictEntry 结构体中,键值对的值是由一个联合体 v 定义的
  • 这个联合体 v 中包含了指向实际值的指针 *val
  • 还包含了无符号的 64 位整数、有符号的 64 位整数,以及 double 类的值
  • 之所以要提醒注意这里,其实是为了说明,这种实现方法是一种节省内存的开发小技巧,非常值得学习
  • 因为当值为整数或双精度浮点数时,由于其本身就是 64 位,就可以不用指针指向了
  • 而是可以直接存在键值对的结构体中,这样就避免了再用一个指针,从而节省了内存空间
  • 那么到这里,应该就了解了 Redis 中链式哈希的实现,不过现在你可能还是不太明白,为什么这种链式哈希可以帮助解决哈希冲突呢?
  • 就拿刚才的例子来说明一下,key3 和 key100 都被映射到了 Hash 表的桶 5 中
  • 而当使用了链式哈希,桶 5 就不会只保存 key3 或 key100
  • 而是会用一个链表把 key3 和key100 连接起来,如下图所示
  • 当有更多的 key 被映射到桶 5 时,这些 key 都可以用链表串接起来,以应对哈希冲突

  • 这样,当要查询 key100 时,可以先通过哈希函数计算,得到 key100 的哈希值被映射到了桶 5 中
  • 然后,再逐一比较桶 5 中串接的 key,直到查找到 key100
  • 如此一来,就能在链式哈希中找到所查的哈希项了
  • 不过,链式哈希也存在局限性,那就是随着链表长度的增加,Hash 表在一个位置上查询哈希项的耗时就会增加,从而增加了 Hash 表的整体查询时间,这样也会导致 Hash 表的性能下降
  • 那么,有没有什么其他的方法可以减少对 Hash 表性能的影响呢?
  • 当然是有的,这就是接下来要介绍的 rehash 的设计与实现了
  • Redis 如何实现 rehash?

  • rehash 操作,其实就是指扩大 Hash 表空间
  • 而 Redis 实现 rehash 的基本思路是这样的:
  • 首先,Redis 准备了两个哈希表,用于 rehash 时交替保存数据
  • 在前面介绍过,Redis 在 dict.h 文件中使用 dictht 结构体定义了 Hash 表
  • 不过,在实际使用 Hash 表时,Redis 又在 dict.h 文件中,定义了一个 dict 结构体
  • 这个结构体中有一个数组(ht[2]),包含了两个 Hash 表 ht[0]和 ht[1]
  • dict 结构体的代码定义如下所示:

  • 其次,在正常服务请求阶段,所有的键值对写入哈希表 ht[0]
  • 接着,当进行 rehash 时,键值对被迁移到哈希表 ht[1]中
  • 最后,当迁移完成后,ht[0]的空间会被释放,并把 ht[1]的地址赋值给 ht[0],ht[1]的表大小设置为 0
  • 这样一来,又回到了正常服务请求的阶段,ht[0]接收和服务请求,ht[1]作为下一次 rehash 时的迁移表

  • 在了解了 Redis 交替使用两个 Hash 表实现 rehash 的基本思路后
  • 还需要明确的是:在实现 rehash 时,都需要解决哪些问题?
  • 我认为主要有以下三点:
    • 什么时候触发 rehash?
    • rehash 扩容扩多大?
    • rehash 如何执行?
  • 什么时候触发 rehash?

  • 首先要知道,Redis 用来判断是否触发 rehash 的函数是 _dictExpandIfNeeded
  • 所以接下来就先看看,_dictExpandIfNeeded 函数中进行扩容的触发条件
  • 然后,再来了解下 _dictExpandIfNeeded 又是在哪些函数中被调用的
  • 实际上,_dictExpandIfNeeded 函数中定义了三个扩容条件:
    • 条件一:ht[0]的大小为 0
    • 条件二:ht[0]承载的元素个数已经超过了 ht[0]的大小,同时 Hash 表可以进行扩容
    • 条件三:ht[0]承载的元素个数,是 ht[0]的大小的 dict_force_resize_ratio 倍,其中,dict_force_resize_ratio 的默认值是 5

  • 那么,对于条件一来说,此时 Hash 表是空的,所以 Redis 就需要将 Hash 表空间设置为初始大小,而这是初始化的工作,并不属于 rehash 操作
  • 而条件二和三就对应了 rehash 的场景
  • 因为在这两个条件中,都比较了 Hash 表当前承载的元素个数(d->ht[0].used)和 Hash 表当前设定的大小(d->ht[0].size),这两个值的比值
  • 一般称为负载因子(load factor)
  • 也就是说,Redis 判断是否进行 rehash 的条件,就是看load factor 是否大于等于 1 和是否大于 5
  • 实际上,当 load factor 大于 5 时,就表明 Hash 表已经过载比较严重了,需要立刻进行库扩容
  • 而当 load factor 大于等于 1 时,Redis 还会再判断 dict_can_resize 这个变量值,查看当前是否可以进行扩容
  • 你可能要问了,这里的 dict_can_resize 变量值是啥呀?
  • 其实,这个变量值是在dictEnableResize 和 dictDisableResize 两个函数中设置的,它们的作用分别是启用和禁止哈希表执行 rehash 功能,如下所示:

  • 然后,这两个函数又被封装在了 updateDictResizePolicy 函数中
  • updateDictResizePolicy 函数是用来启用或禁用 rehash 扩容功能的
  • 这个函数调用dictEnableResize 函数启用扩容功能的条件是:当前没有 RDB 子进程,并且也没有 AOF 子进程
  • 这就对应了 Redis 没有执行 RDB 快照和没有进行 AOF 重写的场景
  • 可以参考下面的代码

  • 到这里就了解了 _dictExpandIfNeeded 对 rehash 的判断触发条件
  • 那么现在,再来看下 Redis 会在哪些函数中,调用 _dictExpandIfNeeded 进行判断
  • 首先,通过在dict.c文件中查看 _dictExpandIfNeeded 的被调用关系,可以发现,_dictExpandIfNeeded 是被 _dictKeyIndex 函数调用的,而 _dictKeyIndex 函数又会被dictAddRaw 函数调用,然后 dictAddRaw 会被以下三个函数调用:
    • dictAdd:用来往 Hash 表中添加一个键值对
    • dictRelace:用来往 Hash 表中添加一个键值对,或者键值对存在时,修改键值对
    • dictAddorFind:直接调用 dictAddRaw
  • 因此,当往 Redis 中写入新的键值对或是修改键值对时,Redis 都会判断下是否需要进行rehash
  • 这里可以参考下面给出的示意图:

  • 简而言之,Redis 中触发 rehash 操作的关键,就是 _dictExpandIfNeeded 函数和updateDictResizePolicy 函数
  • _dictExpandIfNeeded 函数会根据 Hash 表的负载因子以及能否进行 rehash 的标识,判断是否进行 rehash
  • 而 updateDictResizePolicy 函数会根据 RDB 和 AOF 的执行情况,启用或禁用 rehash
  • 接下来,继续探讨 Redis 在实现 rehash 时,要解决的第二个问题:rehash 扩容扩多大?
  • rehash 扩容扩多大?

  • 在 Redis 中,rehash 对 Hash 表空间的扩容是通过调用 dictExpand 函数来完成的
  • dictExpand 函数的参数有两个,一个是要扩容的 Hash 表,另一个是要扩到的容量
  • 下面的代码就展示了 dictExpand 函数的原型定义:

  • 那么,对于一个 Hash 表来说,就可以根据前面提到的 _dictExpandIfNeeded 函数,来判断是否要对其进行扩容
  • 而一旦判断要扩容,Redis 在执行 rehash 操作时,对 Hash 表扩容的思路也很简单,就是如果当前表的已用空间大小为 size,那么就将表扩容到 size*2 的大小
  • 如下所示,当 _dictExpandIfNeeded 函数在判断了需要进行 rehash 后,就调用 dictExpand进行扩容
  • 这里可以看到,rehash 的扩容大小是当前 ht[0]已使用大小的 2 倍

  • 而在 dictExpand 函数中,具体执行是由 _dictNextPower 函数完成的
  • 以下代码显示的Hash表扩容的操作,就是从 Hash 表的初始大小(DICT_HT_INITIAL_SIZE),不停地乘以2,直到达到目标大小

  • 下面再来看看 Redis 要解决的第三个问题,即 rehash 要如何执行?而这个问题,本质上就是 Redis 要如何实现渐进式 rehash 设计
  • 渐进式 rehash 如何实现?

  • 那么这里要先搞清楚一个问题,就是为什么要实现渐进式 rehash?
  • 其实这是因为,Hash 表在执行 rehash 时,由于 Hash 表空间扩大,原本映射到某一位置的键可能会被映射到一个新的位置上,因此,很多键就需要从原来的位置拷贝到新的位置
  • 而在键拷贝时,由于 Redis 主线程无法执行其他请求,所以键拷贝会阻塞主线程,这样就会产生rehash开销
  • 而为了降低 rehash 开销,Redis 就提出了渐进式 rehash 的方法
  • 简单来说,渐进式 rehash 的意思就是 Redis 并不会一次性把当前 Hash 表中的所有键,都拷贝到新位置,而是会分批拷贝,每次的键拷贝只拷贝 Hash 表中一个 bucket 中的哈希项
  • 这样一来,每次键拷贝的时长有限,对主线程的影响也就有限了
  • 那么,渐进式 rehash 在代码层面是如何实现的呢?
  • 这里有两个关键函数:dictRehash 和_dictRehashStep
  • 先来看 dictRehash 函数,这个函数实际执行键拷贝,它的输入参数有两个,分别是全局哈希表(即前面提到的 dict 结构体,包含了 ht[0]和 ht[1])和需要进行键拷贝的桶数量(bucket 数量)
  • dictRehash 函数的整体逻辑包括两部分:
  • 首先,该函数会执行一个循环,根据要进行键拷贝的 bucket 数量 n,依次完成这些bucket 内部所有键的迁移
  • 当然,如果ht[0]哈希表中的数据已经都迁移完成了,键拷贝的循环也会停止执行
  • 其次,在完成了 n 个 bucket 拷贝后,dictRehash 函数的第二部分逻辑,就是判断 ht[0]表中数据是否都已迁移完
  • 如果都迁移完了,那么 ht[0]的空间会被释放
  • 因为 Redis 在处理请求时,代码逻辑中都是使用 ht[0],所以当 rehash 执行完成后,虽然数据都在 ht[1]中了,但 Redis 仍然会把 ht[1]赋值给 ht[0],以便其他部分的代码逻辑正常使用
  • 而在 ht[1]赋值给 ht[0]后,它的大小就会被重置为 0,等待下一次 rehash
  • 与此同时,全局哈希表中的 rehashidx 变量会被标为 -1,表示 rehash 结束了(这里的 rehashidx 变量用来表示 rehash 的进度,稍后会给具体解释)
  • 下面这张图,展示了 dictRehash 的主要执行流程:

  • 同时也可以通过下面代码,来了解 dictRehash 函数的主要执行逻辑:

  • 在了解了 dictRehash 函数的主体逻辑后,再看下渐进式 rehash 是如何按照 bucket粒度拷贝数据的,这其实就和全局哈希表 dict 结构中的 rehashidx 变量相关了
  • rehashidx 变量表示的是当前 rehash 在对哪个 bucket 做数据迁移
  • 比如,当 rehashidx 等于 0 时,表示对 ht[0]中的第一个 bucket 进行数据迁移
  • 当 rehashidx 等于 1 时,表示对ht[0]中的第二个 bucket 进行数据迁移,以此类推
  • 而 dictRehash 函数的主循环,首先会判断 rehashidx 指向的 bucket 是否为空,如果为空,那就将 rehashidx 的值加 1,检查下一个 bucket
  • 那么,有没有可能连续几个 bucket 都为空呢?
  • 其实是有可能的,在这种情况下,渐进式rehash 不会一直递增 rehashidx 进行检查
  • 这是因为一旦执行了 rehash,Redis 主线程就无法处理其他请求了
  • 所以,渐进式 rehash 在执行时设置了一个变量 empty_visits,用来表示已经检查过的空bucket,当检查了一定数量的空 bucket 后,这一轮的 rehash 就停止执行,转而继续处理外来请求,避免了对 Redis 性能的影响
  • 下面的代码显示了这部分逻辑,可以看看:

  • 而如果 rehashidx 指向的 bucket 有数据可以迁移,那么 Redis 就会把这个 bucket 中的哈希项依次取出来,并根据 ht[1]的表空间大小,重新计算哈希项在 ht[1]中的 bucket 位置,然后把这个哈希项赋值到 ht[1]对应 bucket 中
  • 这样每做完一个哈希项的迁移,ht[0]和 ht[1]用来表示承载哈希项多少的变量 used,就会分别减一和加一
  • 当然,如果当前 rehashidx 指向的 bucket 中数据都迁移完了,rehashidx就会递增加 1,指向下一个 bucket
  • 下面的代码显示了这一迁移过程:

  • 到这里就已经基本了解了 dictRehash 函数的全部逻辑
  • 现在知道,dictRehash 函数本身是按照 bucket 粒度执行哈希项迁移的,它内部执行的bucket 迁移个数,主要由传入的循环次数变量 n 来决定
  • 但凡 Redis 要进行 rehash 操作,最终都会调用 dictRehash 函数
  • 接下来,来学习和渐进式 rehash 相关的第二个关键函数 _dictRehashStep,这个函数实现了每次只对一个 bucket 执行 rehash
  • 从 Redis 的源码中可以看到,一共会有 5 个函数通过调用 _dictRehashStep 函数,进而调用 dictRehash 函数,来执行 rehash
  • 它们分别是:dictAddRaw,dictGenericDelete,dictFind,dictGetRandomKey,dictGetSomeKeys
  • 其中,dictAddRaw 和 dictGenericDelete 函数,分别对应了往 Redis 中增加和删除键值对,而后三个函数则对应了在 Redis 中进行查询操作
  • 下图展示了这些函数间的调用关系:

  • 但要注意,不管是增删查哪种操作,这 5 个函数调用的 _dictRehashStep 函数,给dictRehash 传入的循环次数变量 n 的值都为 1,下面的代码就显示了这一传参的情况

  • 这样一来,每次迁移完一个 bucket,Hash 表就会执行正常的增删查请求操作,这就是在代码层面实现渐进式 rehash 的方法

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

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

相关文章

数据处理 |遍历所有文件夹及子目录文件夹方法总结与实例代码详解

深度学习中不可避免的数据预处理~1. glob.glob()方法 2. pathlib中的Path方法3. os.walk()方法1. glob.glob()方法 语法glob.glob(pathname)(多指定文件类型,查找jpg,png,txt,json等)缺点:查找文件较慢2. 路径操作库pathlib中的Pa…

【计算机三级网络技术】 第四篇 路由设计技术基础

文章目录一、分组转发二、路由选择1.理想的路由算法的基本特征2.路由算法的度量标准3.路由算法分类:4.IP路由选择与路由汇聚(重点)三、自治系统与Internet的路由选择协议1.自治系统2.路由选择协议的分类四、内部网关协议1.RIP的基本概念2.RIP的原理3.RIP的运行过程五…

Android Lmkd 低内存终止守护程序

一、低内存终止守护程序 Android 低内存终止守护程序 (lmkd) 进程可监控运行中的 Android 系统的内存状态,并通过终止最不必要的进程来应对内存压力大的问题,使系统以可接受的性能水平运行。 所有应用进程都是从zygote孵化出来的,记录在AMS…

Android问题笔记 - 打开Android Studio先弹出项目选择框

专栏分享点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册点击跳转>Scratch编程案例 👉关于作者 众所周知,人生是一个漫长的流程,不断克服困难,不断…

leetcode 427. Construct Quad Tree(构建四叉树)

刚看到题的时候是懵的,这也太长了。到底是要表达什么呢。 不妨把这个矩阵看成一个正方形的图片,想象你在处理图片,从整体逐步到局部。 刚开始看一整张图片,如果是全0或全1,这个就是叶子节点,怎么表达叶子节…

网络货运平台“降本提质引流增值”秘籍是什么?

2月24日,2022(第五届)中国网络货运平台年会在厦门举行,数据宝作为中物联副会长单位受邀参加峰会,数据宝轮值CEO肖斌发表题为“网络货运平台数字化创新应用实践分享”的主题分享。 据交通运输部统计,截止到2…

某建筑设计研究院“综合布线管理软件”应用实践

某建筑设计研究院有限公司(简称“某院”)隶属于国务院国资委直属的大型骨干科技型中央企业。“某院”前身为中央直属设计公司,创建于1952年。成立近70年来,始终秉承优良传统,致力于推进国内勘察设计产业的创新发展&…

CASENet中edge GT是如何产生的

1:首先下载cityscape数据集,包含两个大文件夹,具体的数据集介绍参考此链接。cityscape数据集解析 看一下gtFine子文件夹,另一个也是同理: 2:将下载好的数据集放到data_orig中,还有一个文件…

报错记录:element-admin框架打包后静态文件加载异常与登录异常解决方案

报错记录:element-admin框架打包后静态文件加载异常与登录异常解决方案 一、静态文件加载异常解决方案 二、登录异常解决方案 现象 element-admin如果用 electron 打包 是会有个问题: electron serve 运行没问题 electron build 打release 包 就会出错…

AST之path常用属性和方法总结笔记

文章目录1. path常用属性总结1.1 path.node1.2 path.scope1.3 path.parentPath1.4 path.parent1.5 path.container1.6 path.type1.7 path.key2. path常用方法总结2.1 path.toString2.2 path.replaceWith2.3 path.replaceWithMultiple2.4 path.remove2.5 path.insertBefore2.6 p…

TypeScript 常用知识

「 推荐一个学习 ts 基础的专栏,满满的干货:typeScript 」 1、为什么推荐使用 TypeScript 【】ts 是 js 的超集,包含 js 的所有元素 【】ts 通过对代码进行类型检查,可以帮助我们避免在编写 js 时经常遇到令人痛苦的错误 【】强…

第六节 方法

方法 方法是一种语法结构。 方法的作用: 1.提高代码的复用性 2.让程序逻辑更加清晰 方法定义的完整格式: 修饰符 返回值类型 方法名(形参列表){ 方法体的代码(需要执行的功能代码) return 返回值&#xff…

Unity性能优化: 性能优化之内存篇

前言 本文和传统的内存优化不一样,不是讲如何降低内存占用,而是讲编程开发中要注意的内存问题以及一些内存技术的演变与原理。 对惹,这里有一个游戏开发交流小组,希望大家可以点击进来一起交流一下开发经验呀 1: Application进程…

maven项目无法解析插件

发现问题使用IDEA创建Maven项目时,报错无法解析插件 org.apache.maven.plugins:maven-clean-plugin这里使用的是IDEA捆绑的Maven插件解决方案查看Maven配置打开用户设置文件settings.xml,在其中加入如果该路径下没有此文件,可以自己创建一个。…

软件分析笔记02---Intermediate Representation

整体contents compiler (source code ——> machine code) non-trivial非平凡的 经过 语义分析->语法分析->类型检查等各种trivial的分析(前端),生成中间代码IR->进行non-trivial的分析(及静…

Linux 基础介绍-基础命令

文章目录01 学习目标02 Linux/Unix 操作系统简介2.1 Linux 操作系统的目标2.2 Linux 操作系统的作用2.3 Unix 家族历史2.4 Linux 家族历史2.5 Linux 和Unix 的联系2.6 Linux 内核介绍2.7 Linux 发行版本2.8 Unix/Linux 开发应用领域介绍03 Linux 目录结构3.1 Win 和Linux 文件系…

C++之入门之引用,内联函数

一、引用 1、引用的概念 在C中,引用的本质其实就是给一个已经存在的变量”起别名“。也就是说,引用与它所引用的对象共用一块空间。(同一块空间的多个名字) 就比如说,李逵又叫黑旋风,而黑旋风就是指李逵…

线程安全实例分析

一、变量的线程安全分析 成员变量和静态变量是否线程安全? ● 如果它们没有共享,则线程安全 ● 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况 —— 如果只有读操作,则线程安全 —— 如果有读写操作&am…

STM32学习笔记-USART串口通信+与野火STM32F407板载ESP8266进行通信

文章目录STM32USART介绍STM32USART框图第一部分第二部分第三部分发送器时序图接收器第四部分软件部分:STM32通过USART与板载ESP8266通讯实验板载WIFI模块电路图实现方式:第一步:配置USART1和USART3的GPIO及其中断第二步:通过中断服…

(2023版)零基础入门网络安全/Web安全,收藏这一篇就够了

由于我之前写了不少网络安全技术相关的文章和回答,不少读者朋友知道我是从事网络安全相关的工作,于是经常有人私信问我: 我刚入门网络安全,该怎么学? 要学哪些东西? 有哪些方向? 怎么选&#x…