今天是个好天气 logo 今天是个好天气
  • Home
  • Go
  • MySQL
  • Redis
  • LeetCode
  • Hello World
↩️README Hello MySQL SQL 写点SQL InnoDB存储引擎 索引 事务 日志 有哪些锁 MySQL如何加锁 MySQL性能如何优化

InnoDB存储引擎

一行记录的存储

长...

char和varchar区别

MySQL层区别

  • 最大长度:char是255,varchar是65535,单位是字符(而不是字节)。

  • 尾随空格:char会将尾随空格去掉,而varchar不会。

    如果char字段有唯一索引,a和a_会提示唯一索引冲突。

  • 存储空间占用:varchar会占用额外的1~2字节来存储字符串长度。如果最大长度超过255,就需要2字节,否则1字节。(变长字段长度列表)

    • 对于CHAR(N)字段,如果实际存储数据小于N字节,会填充空格到N个字节。

存储引擎层区别

在InnoDB存储引擎下、DYNAMIC行格式下。

  • varchar类型对于短字符串、长字符串、多字节编码,都是存储了实际的字符+字符长度
  • char类型存储字符 + 字符长度,对于CHAR(N)字段,如果实际存储数据小于N字节,会填充空格到N个字节。

性能对比

  • char填充空格可能导致浪费存储空间,进而导致性能下降。
  • 因为char多存储一些空格,意味着需要从磁盘读写更多的数据、耗费更多内存、查找数据时删除空格可能也会耗费一些CPU性能。
  • 使用char类型的好处,插入记录分配100个字节,后序修改不会造成页分裂、空分析等问题。
  • 而varchar(100)没有提前分配存储空间,后序Update可能造成页分裂,导致性能下降。
    • MyISAM会拆成不同的片段存储

其他字符串

  • BINARY / VARBINARY : 存放二进制字符串

  • BLOB / TEXT : 存放较大的数据,分别采用二进制和字符方式存储

  • ENUM:枚举

Buffer Pool

为了提升查询性能,数据从磁盘读取出来后,缓存到内存当中,后续查询缓存命中,提高了数据库的读写性能。

为此,Innodb存储引擎设计了缓冲池。默认缓冲池大小为128MB。可以通过调整 innodb_buffer_pool_size 参数来设置 Buffer Pool 的大小,一般建议设置成可用物理内存的 60%~80%。

有了缓冲池之后:

  • 当读取数据存在Buffer Pool当中,客户端直接读取缓冲池中的数据,否则去磁盘中读取
  • 当修改数据时,首先是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘。

缓存什么

Buffer Pool同样按照页来划分,在 MySQL 启动的时候,InnoDB 会为 Buffer Pool 申请一片连续的内存空间,然后按照默认的16KB的大小划分出一个个的页, Buffer Pool 中的页就叫做缓存页。

  • MySQL刚启动时,虚拟内存很大,使用到的物理内存很少,只有这些虚拟内存被访问后,操作系统才会触发缺页中断,接着将虚拟地址和物理地址建立映射关系。
  • Buffer Pool 除了缓存「索引页」和「数据页」,还包括了 undo 页,插入缓存、自适应哈希索引、锁信息等等。
  • 为了更好管理缓存页,为每一个页创建了一个控制块
    • 控制块信息包括「缓存页的表空间、页号、缓存页地址、链表节点」等等。
    • 控制块也是占据内存空间的,放在Buffer Pool的最前面。
  • 当控制块和缓存页设置不刚刚好时,可能产生碎片空间
  • 查询到一条记录后,实际当中是将整个页加载到Buffer Pool当中,然后通过页目录定位到具体的记录。

如何管理空闲页面

缓存池是连续的内存空间,当MySQL运行一段时间后,这篇空间既有使用的也有空闲的,为了更快遍历到空闲的缓存页,构建了一个Free链表。

Free链表中的包括了一个头节点,来包含链表信息,空闲缓存页的控制块作为链表的节点,每个控制块又包含对应缓存页的地址。所以相当于Free链表节点对应一个个空闲的缓存页。

如何管理脏页

每次修改数据,修改Buffer Pool 中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘。

为了快速知道哪些是脏页,而有了Flush链表,记录哪些是脏页,同上面的Free链表结构相似。

如何提高缓存命中率

由于缓冲池的大小有限,希望尽可能留下频繁访问的数据,去除很少访问的数据,从而提高命中率。

最简单的实现:LRU(Least recently used)算法。

该算法的思路是,链表头部的节点是最近使用的,而链表末尾的节点是最久没被使用的。那么,当空间不够了,就淘汰最久没被使用的节点,从而腾出空间。

简单的 LRU 算法的实现思路是这样的:

  • 当访问的页在 Buffer Pool 里,就直接把该页对应的 LRU 链表节点移动到链表的头部。
  • 当访问的页不在 Buffer Pool 里,除了要把页放入到 LRU 链表的头部,还要淘汰 LRU 链表末尾的节点。

最简单的LRU没有被采用,考虑到以下两个问题:

  • 预读失效;

靠近被访问的数据的数据,在未来大概率会被访问,所以MySQL在加载数据时会提前把它相邻的数据一起加入过来,减少磁盘IO。

当时这些预加载的数据可能未被访问,从而造成预读失效。

当使用LRU算法时,就可能会造成加预读页放在前排位置,缓冲池空间不够时淘汰了末尾的页,而这些预读页如果一直不被访问,会占用前排位置,并且淘汰了可能访问的页。

MySQL将LRU算法划分前半部分的young区域,后半部分的old区域,一般按照63:37划分。

划分这两个区域后,预读的页就只需要加入到 old 区域的头部,当页被真正访问的时候,才将页插入 young 区域的头部。如果预读的页一直没有被访问,就会从 old 区域移除,这样就不会影响 young 区域中的热点数据。

优先去除old区域的页。

  • Buffer Pool 污染;

某一个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量热数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染。

发生全表扫描时,很容易出现这样的问题,热点数据被替换掉。因此想到提高数据进入young区域的门槛,通过判断是不是频繁访问,而不是短暂访问一次。

MySQL 是这样做的,进入到 young 区域条件增加了一个停留在 old 区域的时间判断。

  • 如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该缓存页就不会被从 old 区域移动到 young 区域的头部;
  • 如果后续的访问时间与第一次访问的时间不在某个时间间隔内,那么该缓存页移动到 young 区域的头部;

同时满足被访问,并且在old区域停留在超过1s两个条件,才会被插入到young区域。

另外,MySQL 针对 young 区域其实做了一个优化,为了防止 young 区域节点频繁移动到头部。young 区域前面 1/4 被访问不会移动到链表头部,只有后面的 3/4被访问了才会。

脏页什么时候刷新到磁盘

为了提高性能,会在一定时机统一批量刷入磁盘。并且InnoDB具有Write Ahead Log策略,先写日志再写入磁盘,通过日志可以进行崩溃恢复,防止MySQL宕机未来得及写入磁盘。

当满足以下情况会触发脏页的刷新:

  • 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
  • Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
  • MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘;
  • MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;

普通索引和唯一索引如何选择

查询过程

  • 对于普通索引来说,查找到满足条件的第一个记录 (5,500) 后,需要查找下一个记录,直到碰到第一个不满足 k=5 条件的记录。
  • 对于唯一索引来说,由于索引定义了唯一性,查找到第一个满足条件的记录后,就会停止继续检索。

对比下,普通索引重复查找确认,性能差距不大。毕竟是按页读写的,查到k=5,所在的数据也就在内存当中了,重复查找就在这个内存页中指针寻找和计算。

查询时,不在缓冲池,磁盘IO读入。

更新过程

当需要更新一个数据页时,如果数据页在内存中就直接更新(标记为脏页),而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。

在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。

  • 将change buffer中操作应用到原数据页,得到最新的结果过程称为merge。
  • 后台也会定期merge,正常关闭数据库也会发生merge。

change buffer在内存中和磁盘中都有拷贝,是持久化数据。

通过先修改change buffer,减少磁盘IO提高了岁都,同时避免数据写入内存,提高内存利用率。

change buffer使用的是buffer pool内存,因此不能无限量增加。

对于唯一索引来说,需要先判断现在表中是否存在k=4(保证唯一性),必须将数据页读入内存才能判断 ,读入后不如直接读内存中更新了。因此change buffer应用在普通索引上,因为普通索引不用考虑唯一性,直接记录。

当更新记录的目标页不在内存中,唯一索引需要先加载到内存当中,这个磁盘IO消耗是巨大的。而普通索引直接使用change buffer,大大降低了成本。

change buffer的使用场景

在merge更新之前,change buffer记录越多,说明节省的成本越大,收益大。

因此change buffer适合写多读少的场景,比如账单类、日志类的系统。

如果一个业务更新模式写入之后马上查询,那么就去看内存中有没有缓存,没有的话需要加载到内存中,立即触发merge操作,这样操作反而增加了merge的成本。


综上考虑,查询过程性能差距不大,在更新时由于change buffer存在,应该尽可能选择普通索引。

如果所有的更新后面,都马上伴随着对这个记录的查询,那么你应该关闭 change buffer。而在其他情况下,change buffer 都能提升更新性能。

  1. 一行记录的存储
  2. char和varchar区别
    1. MySQL层区别
    2. 存储引擎层区别
    3. 性能对比
    4. 其他字符串
  3. Buffer Pool
    1. 缓存什么
    2. 如何管理空闲页面
    3. 如何管理脏页
    4. 如何提高缓存命中率
    5. 脏页什么时候刷新到磁盘
  4. 普通索引和唯一索引如何选择
    1. 查询过程
    2. 更新过程
    3. change buffer的使用场景
Created by shixiaocaia | Powered by idoc
Think less and do more.