Category: 读书记录

  • 为什么要索引

    为什么要索引

    没有索引查找数据要全表扫描

    索引如何起作用

    • 一种使用索引提高效率的方法:我们可以得知匹配行在什么地方结束,从而跳过其它部分
    • 另一种使用索引提高效率的方法:利用定位算法,不用从索引开始位置进行线性扫描,即可直接找到第一个匹配项(例如二分查找比扫描要快很多)。

    为什么不直接对数据排序

    如果一个表只有一个索引这样做事可以的,但是可能我们还想添加第二索引。例如用户表的id和名称字段。

    此外索引里的行通常比表里的数据行更短。当发生插入或者删除时,为保存排序顺序,来回移动较短的索引值,要比来回移动较长的数据行更加容易。

    索引如何影响mysql查询

    假设,你有3个表t1,t2,t3,其中每个表都包含一个列,它们分别为i1,i2,i3,并且每个表都有1000行记录。

    select t1.i1, t2.i2, t3.i3
    from t1 inner join t2 inner join t3
    where t1.i1 = t2.i2 and t2.i2 = t3.i3
    
    • 如果没有索引,需要尝试所有组合1000 * 1000 *1000

    • 如果i1,i2,i3都建有索引

      • 从表t1中选择第一个行,看该行包含的是什么值
      • 利用表2的索引,直接转到表t1的值相匹配的那个行。类似利用表t3的索引直接转到与表t1的值相匹配的那个行
      • 继续选择表t1的下一行,然后重复前面的过程。

      整个过程,只对表t1执行了全扫描操作。对表t2,t3执行的是索引查找。

    因此我们要

    1. 为用于搜索、排序或者分组的列(候选列)创建索引,而对于用作输出显示的列则不用创建索引。
      1. where
      2. order by and group by
    2. 认真考虑数据列基数(列能容纳的所有非重复值得个数)。

      基数越高,重复率越低,索引的使用效果越好。并且查询优化程序确定出某个值在表的行里出现频率很大时,它会跳过索引,转而执行全表扫描操作(因此性别列建索引没啥用)

    3. 索引短小值

      1. 数据类型越小,内存占用越小
      2. 索引的比较操作更快
      3. 键缓存的索引块可能容纳更多的键值
      4. 对于innodb 使用聚簇索引,数据行和主键值存储在一起。在二级索引里进行查找,会先得到主键值,再通过主键定位到相应的行。因此主键值会在一个二级索引里都会重复出现
    4. 索引字符串的前缀。

    5. 利用最左前缀。

      当创建包含n个列的复合索引时,实际上会创建n个专供mysql使用的索引。如果对一个表添加一个(country/state/city)的复合索引,那么索引会根据country/state/city的顺序来进行索引排序。因此country也是顺序的,可以直接使用country进行检索。

    6. 不要建立过多的索引

      1. 索引占用空间
      2. 表的修改需要对索引进行维护
    7. 让参与比较的索引保存匹配。
      1. 哈希散列,适合精确查找
      2. B树索引适合范围查询
    8. 利用慢查询日志找出那些性能低劣的查询

  • Innodb 逻辑存储结构

    Innodb 逻辑存储结构

    [TOC]

    来源

    《mysql技术内幕》

    从一条数据说起——InnoDB行存储数据结构

    MYSQL存储引擎INNODB详解,从底层看清INNODB数据结构

    22.2.1.1 Fil Header

    重要的

    • innodb B+树索引本身并不能找到具体的一条记录,能找到只是该记录所在的页。数据库把页载入到内存,然后通过page directory在进行二叉查找具体的记录。
    • 数据记录在页中是以堆的形式存放的
    • VARCHAR 或者 BLOB 对象大的时候,使用溢出页面存放实际的数据。

    (more…)

  • inode

    来源:理解inode-阮一峰的网络日志

    文件inode

    每一个文件都有对应的inode,里面包含了与该文件有关的一些信息。

    ➜  ~ stat /tmp/hui/x
      File: ‘/tmp/hui/x’
      Size: 0           Blocks: 0          IO Block: 4096   regular empty file
    Device: fd01h/64769d    Inode: 395197      Links: 1
    Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
    Access: 2021-04-12 13:56:18.677116921 +0800
    Modify: 2021-04-12 13:56:18.677116921 +0800
    Change: 2021-04-12 13:56:18.677116921 +0800
     Birth: -
    
    ➜  ~ ls -i /tmp/hui/x
    395197 /tmp/hui/x
    

    由于inode号码与文件名分离,这种机制导致了一些Unix/Linux系统特有的现象。
      1. 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。
      2. 移动文件或重命名文件,只是改变文件名,不影响inode号码。
      3. 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。
    第3点使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过inode号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode则被回收。

    ➜  ~ lsblk
    NAME   MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
    sr0     11:0    1 41.2M  0 rom
    vda    253:0    0   50G  0 disk
    └─vda1 253:1    0   50G  0 part /
    ➜  ~ df -i /dev/vda1
    Filesystem      Inodes  IUsed   IFree IUse% Mounted on
    /dev/vda1      3276800 197965 3078835    7% /
    
    ➜  ~ dumpe2fs -h /dev/vda1|grep "Inode size"
    dumpe2fs 1.42.9 (28-Dec-2013)
    Inode size:           256
    

    硬连接

    • inode 相同
    • 源文件与目标文件的inode号码相同,都指向同一个inode
    • inode信息中有一项叫做”链接数”,记录指向该inode的文件名总数,这时就会增加1。
    • 反过来,删除一个文件名,就会使得inode节点中的”链接数”减1。
    • 当这个值减到0,表明没有文件名指向这个inode,系统就会回收这个inode号码,以及其所对应block区域
    ➜  ~ ln /tmp/hui/x /tmp/hui/x1
    ➜  ~ ls -li /tmp/hui
    total 0
    395197 -rw-r--r-- 2 root root 0 Apr 12 13:56 x
    395197 -rw-r--r-- 2 root root 0 Apr 12 13:56 x1
    
    ➜  ~ stat /tmp/hui/x1
      File: ‘/tmp/hui/x1’
      Size: 0           Blocks: 0          IO Block: 4096   regular empty file
    Device: fd01h/64769d    Inode: 395197      Links: 2
    Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
    Access: 2021-04-12 13:56:18.677116921 +0800
    Modify: 2021-04-12 13:56:18.677116921 +0800
    Change: 2021-04-12 13:57:57.015423754 +0800
     Birth: -
    

    软连接

    软链接与硬链接最大的不同:指向文件名,而不是文件的inode.因此inode不同

    ➜  ~ ln -s /tmp/hui/x /tmp/hui/x2
    ➜  ~ ls -li /tmp/hui
    total 0
    395197 -rw-r--r-- 2 root root  0 Apr 12 13:56 x
    395197 -rw-r--r-- 2 root root  0 Apr 12 13:56 x1
    395212 lrwxrwxrwx 1 root root 10 Apr 12 13:58 x2 -> /tmp/hui/x
    
    

    目录

    任何一个目录的”硬链接”总数=2+子目录总数(含隐藏目录)

    创建目录时,默认会生成两个目录项:”.”和”..”。
    * 前者的inode号码就是当前目录的inode号码,等同于当前目录的”硬链接”;
    * 后者的inode号码就是当前目录的父目录的inode号码,等同于父目录的”硬链接”。

    ➜  ~ stat /tmp/hui
      File: ‘/tmp/hui’
      Size: 4096        Blocks: 8          IO Block: 4096   directory
    Device: fd01h/64769d    Inode: 394833      Links: 2
    Access: (0755/drwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
    Access: 2021-04-12 13:58:36.718546254 +0800
    Modify: 2021-04-12 13:58:31.312529621 +0800
    Change: 2021-04-12 13:58:31.312529621 +0800
     Birth: -
    
    ➜  ~ mkdir /tmp/hui/huichild
    
    ➜  ~ stat /tmp/hui
      File: ‘/tmp/hui’
      Size: 4096        Blocks: 8          IO Block: 4096   directory
    Device: fd01h/64769d    Inode: 394833      Links: 3
    Access: (0755/drwxr-xr-x)  Uid: (    0/    root)   Gid: (    0/    root)
    Access: 2021-04-12 13:58:36.718546254 +0800
    Modify: 2021-04-12 14:02:40.264280838 +0800
    Change: 2021-04-12 14:02:40.264280838 +0800
     Birth: -
    
  • innodb存储引擎

    innodb存储引擎关键点

    [TOC]

    缓存池

    • 索引页

    • 数据页

    • undo页

    • 插入缓存

    • 自适应哈希索引

    • innodb引擎的锁信息

    • 数据字典信息

    innodb 内存数据对象

    mysql> use information_schema;                                                   Database changed
    mysql> select pool_id,pool_size, free_buffers,database_pages from innodb_buffer_pool_stats \G;
    *************************** 1. row ***************************
           pool_id: 0
         pool_size: 8192
      free_buffers: 1024
    database_pages: 7160
    1 row in set (0.00 sec)
    

    缓存池管理

    innodb_buffer_pool_list

    • LRU(默认)
      • 频繁使用的在LRU列表前端
      • 优化:新访问的页不是放在列表前端,而是放在Midpoint。避免扫描全表的sql,将热点数据挤压刷出
      • midpoint之前称为新表(new sublist),表示热点数据
      • midpoint之前称为旧表(old sublist)
    • unzip_LRU
      • 管理非16KB的页
      • LRU列表中包含unzip_LRU列表中的页

      在LRU列表中的页被修改后,成改页为脏页(dirty page)。

      数据块通过checkpoint机制将脏页刷新回磁盘

      Flush列表中的页为脏页列表。脏页同时存在于LRU列表和Flush列表

    重做日志缓存(redo log buffer)

    • 先将redo信息放入redo log buffer
    • 然后按照一定频率刷新到redo log file(一般1秒一刷)
    • flush redo log
      • master thread 一秒刷redo log buffer to redo log
      • 每个事务提交时
      • 当redo log buffer 剩余空间少于1/2时

    额外的内存池

    • 一些数据结构本身的内存优先从(额外的内存池)中分配
    • 当(额外的内存池)内存不足,会从缓存池中分配

    checkpoint技术

    面对的难题

    • 缓存池不可能缓存数据库所有数据
    • redo log不可能无限大

    用于解决

    • 缩短数据库恢复时间
    • 缓冲池不够用时,将脏页刷新到磁盘
    • 重做日志不可用时,刷新脏页

    通过LSN(log sequence Number)来标记版本的。

    • LSN是8字节的数字
    • 每个页有LSN
    • redo log有LSN
    • checkpoint 有LSN

    有两种checkpoint

    • Sharp checkpoint,数据库关闭时,将所有脏页刷新回磁盘
    • Fuzzy checkpoint,数据库运行中使用,进行页的刷新,只刷新一部分脏页回到磁盘
      • master Fuzzy checkpoint。异步定时从缓冲池刷脏页到磁盘
      • FLUSH_LRU_LIST Checkpoint 为了保证LRU LIST有差不多100个空闲页可供使用,Page Cleaner线程中执行
      • Async/Sync flush Checkpoint.
      • dirty page too much Checkpoint

    MASTER THREAD

    • 具有最高的线程优先级
    • 由多个loop循环组成
      • 主循环(loop):每秒操作和每十秒操作
      • 后台循环(backgroup loop)
      • flush loop
      • 暂停循环(suspend loop)

    loop

    每秒操作

    • 刷redo log buffer到磁盘,即使这个事务还没有提交(总是)

      解析了为什么再大的事务commit都很快

    • 合并插入缓冲(可能)

      只有io压力很低,前一秒的io总数<5,才会执行。

    • 至多刷100个Innodb 缓冲池中的脏页到磁盘(可能)

      当前缓存池中的脏页比例buffer_get_modified_ratio_pct大于配置值

    • 当前没有用户,切换到backupgroup loop状态(可能)

    每十秒操作

    • 刷100个Innodb 缓冲池中的脏页到磁盘(可能)
    • 合并至多5个插入缓冲(总是)
    • 刷redo log buffer到磁盘(总是)
    • 删除无用的undo页(总是)
    • 刷100或者10个Innodb 缓冲池中的脏页到磁盘(总是)

    backupgroup loop

    • 删除无用的undo页(总是)
    • 合并20个插入缓冲(总是)
    • 跳回主循环(总是)
    • 不断刷100个Innodb 缓冲池中的脏页到磁盘(总是)(可能跳转到flush loop中完成)

    InnoDB1.0.x之后

    1.0.x版本之前,Master Thread 有了一定的hard coding。随着硬件性能日新月异,这对编码方式不能很好的匹配不同性能的硬件。

    • 使用配置代替hard coding
    • 增加了一些参数自适应

    InnoDB1.2.x之后

    刷新脏页的操作从Master Thread分离到一个独立的Page Cleaner Thread。减轻Master Thread 的工作,同时进一步提高了系统的并发性。

    Innode关键特征

    • insert buffer
    • double write
    • adaptive hash index
    • async IO
    • Flush Neighbor Page

    insert buffer

    insert buffer 和数据页一样,也是物理页的一个组成部分,用于非唯一辅助索引的插入操作。

    每一张表上除了有一个聚集索引(primary key),有可能还有多个非聚集的辅助索引。

    在进行插入操作的时候,数据页还是按照primary key进行顺序存放的。但是对于非聚集索引叶子节点的插入不再是顺序的。这时需要离散的访问非聚集索引页,由于随机读取的存在,进而影响了插入性能。

    mysql引入insert buffer,对非聚集索引的插入或更新操作,不是每一次直接插入到索引页中,而是先判断插入的非聚集索引页是否在缓冲池中,若是在,则直接插入;若不在,则先放入到一个insert buffer对象中。

    然后再以一定的频率和情况进行insert buffer和辅助叶子节点的merge操作。

    辅助索引不能是唯一的,因为在插入缓冲时,数据库为了避免离散读取的情况发生,并不去查找索引页来判断插入的记录的唯一性。

    Change Buffer

    Innodb 1.0.x版本开始引入Change Buffer,可将其视为Insert BUFFER的升级。Innodb可以对DML操作–INSERT、DELETE、UPDATE都进行缓冲,它们分别是:

    • Insert Buffer
    • Delete Buffer
    • Purge Buffer

    Innodb 提供参数Innodb_change_bufferiing,用于开启各种Buffer的选项

    对一条记录进行UPDATE操作可能分为连个过程:

    • Delete buffer复制将记录标记为删除

    • Purge buffer负责将记录真正的删除

    Insert Buffer的内部实现

    • 使用b+树

      mysql 4.1之前每张表有一颗insert buffer B+树。现在全局只有一颗Insert Buffer B+树

    • 它保存在共享空间中(ibdata1),因此,试图通过独立表空间ibd文件恢复表中数据时,往往会导致CREATE TABLE失败。这是因为表的辅助索引中的数据可能还在Insert Buffer中。所以通过idb文件进行恢复后,还需要进行REPAIR TABLE操作来重建表上所有的辅助索引。

    • 由叶子节点和非叶子节点组成

    • 非叶子节点存放的是查询的search key(键值),结构如下

    space marker offset
    表示待插入记录所以在表的表空间id 用于兼容向后兼容 页所在的偏移量
    • 当一个辅助索引要插入到页(space,offset)时,如果这个页不再缓冲池中,那么Innodb首先根据上述规则构造一个search key,接下来查询insert buffer,然后将记录插入到insert buffer B+树的叶子节点中。
    space :marker offset metadata
    • metadata中IBUF_REC_OFFSET_COUNT(2字节的整数)用来排序每个记录进入insert buffer的顺序

    • insert buffer bitmap:一个特殊的页,用来标记每个辅助索引页的可用空间,保证每次merge insert buffer页必须成功。

    • 每个insert buffer bitmap用来追踪16384个辅助索引页,也就是256个区(Extent)

    • 每个辅助索引页在insert buffer bitmap中存储结构

    merge insert buffer

    • 辅助索引页被读取到缓冲池中
      • 检查insert buffer bitmap页,确认该辅助索引页是否存放在insert buffer B+树中
      • 存在,则将B+树中该页的记录插入到该辅助索引页中。对该页多次的记录操作通过一次合并到原有的辅助索引页
    • insert buffer bitmap页追踪到该辅助索引页已无可用空间时。(小于1/32页,会强制进行一次合并操作)
    • Master Thread中每秒或者每十秒的合并操作。

    两次写

    避免数据库宕机时,出现部分写失效(partial page write)。例如某个页16KB,只写了4KB就发生了宕机。

    通过redo log进行数据恢复,由于redo日志记录的是对页的物理操作(如偏移量800,写‘aaa’记录),需要基于页是好的。如果这个页本身已经发生了损坏,无法通过redo 日志进行重放。

    doublewrrite:当写入失效发生时,先通过页的副本还原该页,再进行重做。

    doublewrrite由两部分组成:

    • 内存中的doublewrrite buffer,2MB
    • 物理磁盘上共享表空间中连续的128个页,即2个区(extent),2MB

    在对缓冲池进行flush时,并不是通过写磁盘,而是通过memcpy函数将脏页先复制到内存中的doublewrite buffer,之后通过doublewrite buffer再分两次,每次写1MB顺序写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘避免缓冲写带来的问题(顺序写)。doublewrite buffer中的页写入到各个表空间文件中。(离散写)

    自适应哈希索引(AHI)

    自适应哈希索引:Innodb存储引擎会监控对表上各索引的查询,如果观察到建立哈希索引可以带来速度提升,则建立哈希索引。

    AHI是通过缓冲池的B+树页构造,不需要对整张表构建哈希索引。

    条件:

    • 访问模式一样,即查询的条件一样。
    • 以该模式访问了100次
    • 页通过该模式访问了N次,其中(N=页中记录*1/16)。

    异步IO

    Innodb支持AIO。

    • read ahead 方式的读取(借助aio,将所有io发送完毕后,然后等待所有IO操作的完成)
    • 脏页的刷新(可以将多个连续io进行IO Merge)

    刷新邻近页

    当刷新一个脏页时,Innodb会检测改页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。然后通过AIO将多个io写入操作合并成一个IO操作。

    缺点:

    • 可能会将不怎么脏的页进行了写入,而该页之后很快又会变成脏页。
    • 固态硬盘有着较高的IOPS,不需要这个特性。
  • 如何进行k/v存储选型

    下面是bitcask的论文里,作者考虑到的目标

    • low latency per item read or written
    • high throughput, especially when writing an incoming stream of random items
    • ability to handle datasets much larger than RAM w/o degradation
    • crash friendliness, both in terms of fast recovery and not losing data
    • ease of backup and restore
    • a relatively simple, understandable (and thus supportable) code structure and data format
    • predictable behavior under heavy access load or large volume
    • a license that allowed for easy default use in Riak

    论文地址:https://riak.com/assets/bitcask-intro.pdf