【2.23】MySQL索引、动态规划、七牛云、操作系统

内存管理

为什么要有虚拟内存?

  • 单片机的 CPU 是直接操作内存的「物理地址」。在这种情况下,要想在内存中同时运行两个程序是不可能的。想要同时在内存中运行多个程序,就需要把进程所使用的地址隔离,所以使用了虚拟内存。
    • 虚拟内存地址是程序使用的内存地址。

    • 物理内存地址是实际存在硬件里面的地址。

  • 操作系统为每个进程都分配了一套虚拟内存地址,进程持有的虚拟地址会通过 CPU 芯片中的内存管理单元(MMU)的映射关系,来转换变成物理地址,然后再通过物理地址访问内存

内存分段

  • 操作系统使用内存分段内存分页的方式管理虚拟地址与物理地址之间的关系。

  • 内存分段 :程序是由若干个逻辑分段组成的,如可由代码分段、数据分段、栈段、堆段组成。不同的段是有不同的属性的,所以就用分段(*Segmentation*)的形式把这些段分离出来。

  • 分段机制下的虚拟地址由段选择因子段内偏移量两部分组成

    • 段选择因子保存在段寄存器内。段选择子里面最重要的是段号,用作段表的索引。段表里面保存的是这个段的基地址、段的界限和特权等级等。

    • 虚拟地址中的段内偏移量应该位于 0 和段界限之间,如果段内偏移量是合法的,就将段基地址加上段内偏移量得到物理内存地址。

  • 虚拟地址是通过段表与物理地址进行映射的,分段机制会把程序的虚拟地址分成 4 个段,每个段在段表中有一个项,在这一项找到段的基地址,再加上偏移量,于是就能找到物理内存中的地址。

  • 分段也存在不足之处,存在内存碎片问题内存交换的效率低的问题。

    • 为什么会产生内存碎片?

      ​ 假设有 1G 的物理内存,用户执行了多个程序,其中:

      • 游戏占用了 512MB 内存
      • 浏览器占用了 128MB 内存
      • 音乐占用了 256 MB 内存。

      这个时候,如果我们关闭了浏览器,则空闲内存还有 1024 - 512 - 256 = 256MB。

      如果这个 256MB 不是连续的,被分成了两段 128 MB 内存,这就会导致没有空间再打开一个 200MB 的程序。

    • 内存碎片主要分为内部内存碎片和外部内存碎片。内存分段管理可以做到1段根据实际需求分配内存,有多少需求就分配多大的段,所以不会出现内部内存碎片。但是由于每个段的长度不固定,所以多个段未必能恰好使用所有的内存空间,会产生了多个不连续的小物理内存,导致新的程序无法被装载,所以会出现外部内存碎片的问题。

    • 解决「外部内存碎片」的问题就是内存交换。可以将音乐程序占用的那 256MB 内存写到硬盘上,然后再从硬盘上读回来到内存里。不过再读回的时候,我们不能装载回原来的位置,而是紧紧跟着那已经被占用了的 512MB 内存后面。这样就能空缺出连续的 256MB 空间,于是新的 200MB 程序就可以装载进来。

    • 为什么分段会导致内存交换效率低?

    • 产生了外部内存碎片,那就不得不重新 Swap 内存区域,这个过程会产生性能瓶颈。因为硬盘的访问速度要比内存慢太多了,每一次内存交换,我们都需要把一大段连续的内存数据写到硬盘上。

    • 所以,**如果内存交换的时候,交换的是一个占内存空间很大的程序,这样整个机器都会显得卡顿。**为了解决内存分段的「外部内存碎片和内存交换效率低」的问题,就出现了内存分页。

内存分页

  • 内存分页:当需要进行内存交换的时候,让需要交换写入或者从磁盘装载的数据更少一点,这样就可以解决问题了。这个办法,也就是内存分页Paging)。分页是把整个虚拟和物理内存空间切成一段段固定尺寸的大小。这样一个连续并且尺寸固定的内存空间,我们叫Page)。在 Linux 下,每一页的大小为 4KB

  • 内存分页中,虚拟地址与物理地址之间通过页表来映射

  • 页表是存储在内存里的,内存管理单元MMU)就做将虚拟内存地址转换成物理地址的工作。

  • 当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入系统内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行。

  • 分页是怎么解决分段【外部内存碎片和内存交换效率低】的问题?

    • 内存分页由于内存空间都是预先划分好的,也就不会像内存分段一样,在段与段之间会产生间隙非常小的内存,这正是分段会产生外部内存碎片的原因。而采用了分页,页与页之间是紧密排列的,所以不会有外部碎片。
    • 但是,因为内存分页机制分配内存的最小单位是一页,即使程序不足一页大小,我们最少只能分配一个页,所以页内会出现内存浪费,所以针对内存分页机制会有内部内存碎片的现象。
    • 如果内存空间不够,操作系统会把其他正在运行的进程中的「最近没被使用」的内存页面给释放掉,也就是暂时写在硬盘上,称为换出Swap Out)。一旦需要的时候,再加载进来,称为换入Swap In)。所以,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多时间,内存交换的效率就相对比较高。
    • 在加载程序时,不再需要一次性都把程序加载到物理内存中。我们完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存里,而是只有在程序运行中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。
  • 分页机制下,虚拟地址和物理地址是如何映射的?

    • 在分页机制下,虚拟地址分为两部分,页号页内偏移。页号作为页表的索引,页表包含物理页每页所在物理内存的基地址,这个基地址与页内偏移的组合就形成了物理内存地址。

    • 对于虚拟地址和物理地址相互转换,就三个步骤:

      • 把虚拟内存地址,切分成页号和偏移量;
      • 根据页号,从页表里面,查询对应的物理页号;
      • 直接拿物理页号,加上前面的偏移量,就得到了物理内存地址。

多级页表

  • 单页表的实现方式,在 32 位和页大小 4KB 的环境下,一个进程的页表需要装下 100 多万个「页表项」,并且每个页表项是占用 4 字节大小的,于是相当于每个页表需占用 4MB 大小的空间。存在空间上的缺陷。

  • 我们把这个 100 多万个「页表项」的单级页表再分页,将页表(一级页表)分为 1024 个页表(二级页表),每个表(二级页表)中包含 1024 个「页表项」,形成二级分页


  • 为什么多级页表比单页表省空间?

  • 局部性原理讲的是:在一段时间内,整个程序的执行仅限于程序的某一部分,相应地,程序访问的存储空间也局限于某个内存区域。主要分为两类:

    1. 时间局部性:如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。
    2. 空间局部性:是指一旦程序访问了某个存储单元,则不久之后,其附近的存储单元也将被访问。
  • 对于大多数程序来说,其使用到的空间远未达到 4GB,因为会存在部分对应的页表项都是空的,根本没有分配,对于已分配的页表项,如果存在最近一定时间未访问的页表,在物理内存紧张的情况下,操作系统会将页面换出到硬盘,也就是说不会占用物理内存。

  • 使用二级分页,如果某个一级页表的页表项没有被用到,也就不需要创建这个页表项对应的二级页表了,即可以在需要时才创建二级页表。从页表的性质来看,保存在内存中的页表承担的职责是将虚拟地址翻译成物理地址。假如虚拟地址在页表中找不到对应的页表项,计算机系统就不能工作了。所以页表一定要覆盖全部虚拟地址空间,不分级的页表就需要有 100 多万个页表项来映射,而二级分页则只需要 1024 个页表项。

  • 我们把二级分页再推广到多级页表,就会发现页表占用的内存空间更少了,这一切都要归功于对局部性原理的充分应用。

  • 对于 64 位的系统,两级分页肯定不够了,就变成了四级目录,分别是:

    • 全局页目录项 PGD(Page Global Directory);

    • 上层页目录项 PUD(Page Upper Directory);

    • 中间页目录项 PMD(Page Middle Directory);

    • 页表项 PTE(Page Table Entry);

TLB

  • 多级页表虽然解决了空间上的问题,但是虚拟地址到物理地址的转换就多了几道转换的工序,这显然就降低了这俩地址转换的速度,也就是带来了时间上的开销。

  • 程序是有局部性的,即在一段时间内,整个程序的执行仅限于程序中的某一部分。相应地,执行所访问的存储空间也局限于某个内存区域。我们就可以利用这一特性,把最常访问的几个页表项存储到访问速度更快的硬件,于是计算机科学家们,就在 CPU 芯片中,加入了一个专门存放程序最常访问的页表项的 Cache,这个 Cache 就是 TLB(Translation Lookaside Buffer) ,通常称为页表缓存、转址旁路缓存、快表等。

  • 在 CPU 芯片里面,封装了内存管理单元(Memory Management Unit)芯片,它用来完成地址转换和 TLB 的访问与交互。有了 TLB 后,那么 CPU 在寻址时,会先查 TLB,如果没找到,才会继续查常规的页表。TLB 的命中率其实是很高的,因为程序最常访问的页就那么几个。

段页式内存管理

  • 内存分段和内存分页并不是对立的,它们是可以组合起来在同一个系统中使用的,那么组合起来后,通常称为段页式内存管理

  • 段页式内存管理实现的方式:

    • 先将程序划分为多个有逻辑意义的段,也就是前面提到的分段机制;

    • 接着再把每个段划分为多个页,也就是对分段划分出来的连续空间,再划分固定大小的页;

  • 地址结构由段号、段内页号和页内位移三部分组成。

  • 用于段页式地址变换的数据结构是每一个程序一张段表,每个段又建立一张页表,段表中的地址是页表的起始地址,而页表中的地址则为某页的物理页号。

  • 段页式地址变换中要得到物理地址须经过三次内存访问:

    • 第一次访问段表,得到页表起始地址;
    • 第二次访问页表,得到物理页号;
    • 第三次将物理页号与页内位移组合,得到物理地址。
  • 虚拟内存的作用?

    • 第一,虚拟内存可以使得进程对运行内存超过物理内存大小,因为程序运行符合局部性原理,CPU 访问内存会有很明显的重复访问的倾向性,对于那些没有被经常使用到的内存,我们可以把它换出到物理内存之外,比如硬盘上的 swap 区域。
    • 第二,解决了多进程之间地址冲突的问题,由于每个进程都有自己的页表,所以每个进程的虚拟内存空间就是相互独立的。这些页表是私有的,进程也没有办法访问其他进程的页表。
    • 第三,在内存访问方面,安全性更高。页表里的页表项中除了物理地址之外,还有一些标记属性的比特,比如控制一个页的读写权限,标记该页是否存在等。

MySQL单表不要超过2000W行,靠谱吗?

  • InnoDB存储引擎的表数据时存放在一个.idb(innodb data)的文件中,也叫做表空间。
  • MySQL 的表数据是以页的形式存放的,页在磁盘中不一定是连续的。
  • 页的空间是 16K, 但并不是所有的空间都是用来存放数据的,会有一些固定的信息,如,页头,页尾,页码,校验码等等。
  • 在 B+ 树中,叶子节点和非叶子节点的数据结构是一样的,区别在于,叶子节点存放的是实际的行数据,而非叶子节点存放的是主键和页号。
  • 索引结构不会影响单表最大行数,2000W 也只是推荐值,超过了这个值可能会导致 B + 树层级更高,影响查询性能。

索引失效有哪些?

  • 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效。
    • 因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。
  • 对索引列进行表达式计算、使用函数,这些情况下都会造成索引失效。
    • 因为索引保存的是索引字段的原始值,而不是经过计算后的值。
  • 对索引隐式类型转换。MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较如果字符串是索引列,而输入的参数是数字的话,那么索引列会发生隐式类型转换,由于隐式类型转换是通过 CAST 函数实现的,等同于对索引列使用了函数,所以就会导致索引失效。
  • 联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左边的索引优先的方式进行索引的匹配,否则就会导致索引失效。
  • 在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
    • 因为 OR 的含义就是两个只要满足一个即可,只要有条件列不是索引列,就会进行全表扫描。

MySQL使用like “%x”,索引一定会失效吗?

  • 使用左模糊匹配(like “%xx”)并不一定会走全表扫描(索引不一定失效),如果数据库表中的字段只有主键+二级索引,那么即使使用了左模糊匹配,也不会走全表扫描(type=all),而是走全扫描二级索引树(type=index)。

  • 联合索引要遵循最左匹配才能走索引,但是如果数据库表中的字段都是索引的话,即使查询过程中,没有遵循最左匹配原则,也是走全扫描二级索引树(type=index)。

count(*)和count(1)有什么区别?哪个性能好?

  • count(*) = count(1) > count(主键字段) > count(字段)

  • count()是什么?

    • count()是一个聚合函数,函数的参数不仅可以是字段名,也可以是其他任意的表达式,作用是统计符合查询条件的记录中,函数指定的参数不为 NULL 的记录有多少个
    • 比如count(name):统计name不为NULL的字段有多少。count(1):统计1不为NULL的字段有多少。1永远不可能是NULL,所以其实是在统计一共有多少条记录。
  • count(主键字段)执行过程是怎样的?

    • 在通过 count 函数统计有多少个记录时,MySQL 的 server 层会维护一个名叫 count 的变量。server 层会循环向 InnoDB 读取一条记录,如果 count 函数指定的参数不为 NULL,那么就会将变量 count 加 1,直到符合查询的全部记录被读完,就退出循环。最后将 count 变量的值发送给客户端。
    • 如果表中只有主键索引,没有二级索引,InnoDB在遍历时就会遍历聚簇索引,将读取到的记录返回给server层,然后读取记录中的主键值,如果为NULL,就将count变量 + 1。如果表中有二级索引,InnoDB就会遍历二级索引。
  • count(1)执行过程是怎样的?

    • 如果表中只有主键索引,没有二级索引,InnoDB遍历时会遍历聚簇索引,将读取到的记录返回给server层,但是不会读取记录中的任何字段的值。因为 count 函数的参数是 1,不是字段,所以不需要读取记录中的字段值。如果表中有二级索引,InnoDB就会遍历二级索引。
      • 因为 count 函数的参数是 1,不是字段,所以不需要读取记录中的字段值。
  • count(*)执行过程是怎样的?

    • count(\*) 其实等于 count(0),也就是说,当你使用 count(*) 时,MySQL 会将 * 参数转化为参数 0 来处理。
  • count(字段)执行过程是怎样的?

    • 会采用全表扫描的方式来计数,所以它的执行效率是比较差的。
  • count(1)、 count(*)、 count(主键字段)在执行的时候,如果表里存在二级索引,优化器就会选择二级索引进行扫描。

    所以,如果要执行 count(1)、 count(*)、 count(主键字段) 时,尽量在数据表上建立二级索引,这样优化器会自动采用 key_len 最小的二级索引进行扫描,相比于扫描主键索引效率会高一些。

  • 为什么要通过遍历的方式计数?

    • InnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的,所以无法像 MyISAM一样,只维护一个 row_count 变量。
  • 如何优化count(*) ?

    • 如果业务对于统计个数不需要很精确,可以使用explain 命令来进行估算。
    • 使用额外表保存计数值,将这个计数值保存到单独的一张计数表中。

LeetCode

  • leetcode279

    完全平方数的大小不可能超过n,所以i* i <= n。同理,背包的容量也不会超过n。

    class Solution {
        public int numSquares(int n) {
            //完全平方数就是i * i。
            int dp [] = new int [n + 1];
            int max = Integer.MAX_VALUE;
            for(int i = 0 ; i < dp.length ; i++){
                dp[i] = max;
            }
            dp[0] = 0;
            for(int i = 1; i * i <= n ; i ++){ //先遍历物品
                for(int j = i * i ; j <= n ; j ++){     //再遍历背包 
    
                    dp[j] = Math.min(dp[j] , dp[j - i * i] + 1);
                }
            }
            return dp[n];
        }
    }
    
  • leetcode139

    单词就是物品,字符串s就是背包。该题的递推公式是一个难点,需要好好考虑。

    dp[i]可以拼接出s的前提,是dp[i - word.length]可以拼接出s,并且s的[i - word.length ~ i)之间有wordDict中的单词。

    class Solution {
        public boolean wordBreak(String s, List<String> wordDict) {
            //dp[j]:长度为j的字符串,是否可以成为组成s的一部分。
            //递推公式:dp[i]可以拼接出s的前提,dp[i - word.length]可以拼接出s,并且s的[i - word.length ~ i)之间有wordDict中的单词。
            int n = s.length();
            boolean dp [] = new boolean [n + 1];
            //空字符串可以成为组成s的一部分。
            dp[0] = true;
            for(int i = 1; i <= n ;i ++){ //先遍历背包
                for(String str : wordDict){
                    int wl = str.length();
                    if(i >= wl && dp[i - wl] && wordDict.contains(s.substring(i - wl , i))){
                        dp[i] = true;
                    }
                }
            }
            return dp[n];
        }
    }
    

多重背包

  • 有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
  • **每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。**所以直接在遍历的时候加一层循环即可。用来拆开每件物品的Mi件。
public void testMultiPack(){
	int[] weight = new int[] {1, 3, 4};
    int[] value = new int[] {15, 20, 30};
    int[] nums = new int[] {2, 3, 2};
    int bagWeight = 10;
    int dp [] = new int [bagWeight + 1];
    //01背包
    for(int i = 0 ; i < nums.length; i++){ //遍历物品
        for(int j =bagWeight ; j >= weight[i] ; j--){ //遍历容量
            for(int k = nums[i] ; k >0 && (j - k * weight[i]) >= 0; k --){ //拆开每件物品
                dp[j] = Math.max(dp[j] , dp[j - k * weight[i]] + k * values[i]);
            }
        }
    } 
    return dp[bagWeight];
}
	

打家劫舍

  • 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

  • 判断该房间的偷与不偷主要与前两个房间有关,所以是动态规划问题。

  • 动规五部曲

    • 确定dp数组及其下标含义:下标i及i以内的房屋,最多可以偷窃的金额是dp[i]。

    • 确定递推公式:dp[i] = Math.max(dp[i - 1] , dp [i - 2] + nums[ i ])

      • 如果偷第 i 间房,那么dp[i - 2] + nums[ i ],即:第i - 1间一定不考虑,所以是i - 2间房以内的房屋金额 + 偷第i间房的金额nums[i]。
      • 如果不偷第i间房,那么dp[ i ] = dp[i - 1],即:考虑偷第i - 1间房。
    • 确定初始化:从递推公式看出,基础是dp[0]和dp[1]房。dp[0]一定是nums[0],dp[1]就是max(nums[0] , nums[1])。

    • 确定遍历顺序:从前往后遍历。

    • 举例推导dp:

LeetCode

  • leetcode108

    class Solution {
        public int rob(int[] nums) {
            int n = nums.length;
            if(n == 1) return nums[0];
            int dp [] = new int [n];
            dp[0] = nums[0];
            dp[1] = Math.max(nums[0],nums[1]);
            for(int i = 2 ; i < n ; i ++){
                dp[i] = Math.max(dp[i - 1] , dp[i - 2] + nums[i]);
            }
            return dp[n - 1];
        }
    }
    

七牛云存储

七牛云(隶属于上海七牛信息技术有限公司)是国内领先的以视觉智能和数据智能为核心的企业级云计算服务商,同时也是国内知名智能视频云服务商,累计为 70 多万家企业提供服务,覆盖了国内80%网民。围绕富媒体场景推出了对象存储、融合 CDN 加速、容器云、大数据平台、深度学习平台等产品、并提供一站式智能视频云解决方案。为各行业及应用提供可持续发展的智能视频云生态,帮助企业快速上云,创造更广阔的商业价值。

官网:https://www.qiniu.com/

通过七牛云官网介绍我们可以知道其提供了多种服务,我们主要使用的是七牛云提供的对象存储服务来存储图片。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sivan_Xin

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值