微信订阅号:魔术师耿
我是一个互联网公司的螺丝钉;
魔术师耿

MySQL InnoDB理论基础

以下内容是观看陈东老师的公开课,自己总结的笔记

1. MySQL InnoDB存储原理深度剖析

1.1 MySQL 记录存储

  • 页头
    • 记录页面的控制信息,共占用56字节,包括页的左右兄弟页面指针、页面空间使用情况等。
  • 虚记录
    • 最大虚记录:比页内最大主键还大
    • 最小虚记录: 比页内最小主键还小
  • 记录堆
    • 行记录存储区,分为有效记录和已删除记录两种
  • 自由空间链表
    • 已删除记录组成的链表
  • 未分配空间
    • 页面未使用的存储空间
  • Slot区
  • 页尾
    • 页面最后部分,占用8个字节,主要存储页面的校验信息;

InnoDB理论基础和工作总结

1.2 页内记录维护

  • 顺序保证

    • 物理连续(X
    • 逻辑连续(MySQL用的这种)

InnoDB理论基础和工作总结

  • 插入的策略(先堵住空洞,再用新的)

    • 自由空间链表
    • 未使用空间
  • 页内查询

    • 遍历
    • 二分查找

    类似跳表的实现机制

    InnoDB理论基础和工作总结

2. MySQL InnoDB索引实现原理及使用优化分析

2.1 索引原理分析

  • 聚簇索引

    • 数据存储在主键索引中
    • 数据按主键顺序存储

    自增主键 VS 随机主键

    InnoDB理论基础和工作总结

  • 二级索引

    • 除主键索引以外的所有索引

    • 叶子节点中存储主键值

    • 一次查询需要走两遍索引

    • 主键大小会影响所有索引的大小

      InnoDB理论基础和工作总结

  • 联合索引

    • Key由多个字段组成
    • 最左匹配原则
    • 一个索引只创建一颗树
    • 按照第一列排序,第一列相同按第二列排序
    1. 如果不是按照最左开始查找,无法使用索引
    2. 不能跳过中间列
    3. 某列使用范围查询,后面的列不能使用索引

InnoDB理论基础和工作总结

2.2 索引使用优化分析

  • 存储空间

    • 索引文件大小
    • 字段大小–》 页内节点个数–》树的层数;

    BIGINT类型主键3层可以存储约10亿条数据

    16KB/(8B(key)+8B(指针)) = 1K 一页可以存1000个数据

    10^3 * 10^3 * 10^3 = 10亿

    32字节主键3层可以存储6400W数据

  • 主键选择

    • 自增主键,顺序写入,效率高;
      • 写入磁盘利用率高,每次查询走二级索引;
    • 随机主键,节点分裂、数据移动;(写入非常不友好)
      • 写入磁盘利用率低,每次查询走二级索引;
    • 业务主键(雪花算法:时间递增,机器编号,count++)
      • 写入、查询磁盘利用率都高,可以使用一级索引;
    • 联合主键
      • 影响索引大小,不易维护,不建议使用(DBA强烈反对);
  • 联合索引使用

    • 按索引区分度排序

      • 第一列,让它尽可能区分出更多数据来,(区分度高的放前面)

        区分度太低,mysql内部去评估,可能直接就去全表扫描了

    • 覆盖索引

      • 尽量在索引上就能找到所需要的数据,无需会表查询
  • 字符串索引

    • 设置合理长度

      • 如果非要给字符串字段建立索引(如bak varchar(256)), 为避免索引占用空间太大,给索引设置长度如32个字符

      • 不支持%开头的模糊查询

        mysql不适合做全文检索,出现ES,分词,倒排索引的原因

3. MySQL InnoDB存储引擎内存管理

  • 预分配内存空间
  • 数据以页为单位加载
  • 数据内外存交换

InnoDB理论基础和工作总结

3.1 InnoDB内存管理—技术点

  • 内存池

  • 内存页面管理

    • 页面映射
    • 页面数据管理
    1. 空闲页
    2. 数据页
    3. 脏页
  • 数据淘汰

    • 内存页面都被使用
    • 需要加载新数据

页面淘汰

  • LRU

InnoDB理论基础和工作总结

思考:全表扫描对内存的影响?

热数据被挤出了内存

解决问题:

  1. 避免热数据被淘汰

思路:

  1. 访问时间 + 频率?(redis这么干的)

  2. 两个LRU表?

    一个热LRU,一个冷LRU; 分段LUR

InnoDB怎么做的:

InnoDB理论基础和工作总结

  • Buffer Pool

    • 预分配的内存池,
  • Page

    • 按页去加载,Buffer Pool的最小单位
  • Free list

    • 空闲Page组成的链表
  • Flush list

    • 脏页组成的链表
  • Page hash表

    • 维护内存页和文件页的映射关系
  • LRU

    • 内存淘汰算法

      InnoDB理论基础和工作总结

    • 页面装载

      • 磁盘数据到内存

        [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YE44eozL-1596020556956)(./img/页面装载.png)]

        没有空闲页的时候怎么办?

        Free list 中取 > LRU中淘汰 > LRU Flush

        页面加载时优先从Free list中去取,没有空闲空间了;去LRU_old里去淘汰; 极端情况下LRU里面淘汰不掉,(数据正在使用,被Lock住了),要去刷脏页;

      • 页面淘汰

        LRU链表中将第一个脏页刷盘并“释放”(这一块儿内存就无效了),

        放到LRU尾部?直接放FreeList? (mysql直接放到Free list )

        • LRU尾部淘汰
        • Flush LRU淘汰
      • 位置移动

        • old 到 new

          InnoDB理论基础和工作总结

          移动时机:怎么去定义热数据,不能一个全表扫描,就把热数据全给踢出内存啦;

          innodb_old_blocks_time(配置的值)

          old区存活时间,大于这个值,有机会进入new区

          页面进入LRU_old时记录一个时间,now(), 被访问时的时间now()2 ; now()2 - now() 是在LRU中的存活时间

        • new 到 old

          • 没有节点的移动,移动下Midpoint指针的值就行

            [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P8a8OPn7-1596020556958)(./img/LRU-位置移动2.png)]

      • LRU_new的操作

        链表操作效率非常高,有访问移动到表头吗? ()

        链表的移动,要加锁才能操作(Lock !!!)

        MySQL的设计思路:减少移动次数

        两个重要参考:(通过这两个参数来判断是否需要移动数据)

        1. freed_page_clock: Buffer Pool淘汰页数
        2. LRU_new长度1/4

        当前freed_page_clock - 上次移动到Header 时freed_page_clock > LRU_new长度1/4

        通过这个算法,来减少操作链表的次数

4. MySQL事务管理机制原理分析

  • 事务特性

    • A (Atomicity原子性):全部成功或全部失败
    • I(Isolation隔离性): 并行事务之间互不干扰
    • D(Durability持久性): 事务提交后,永久生效
    • C(Consistency一致性): 通过AID来保证
  • 并发问题

    • 脏读(Dirty Read):读取到未提交的数据

    • 不可重复读(Non-repeatable read):两次读取结果不同(同一个事务里)

    • 幻读(Phantom Read): select 操作得到的结果所表现的数据状态无法支撑后续的业务操作 (读到的东西不知道是对的是错的,)

  • 隔离级别

    • Read Uncommitted (读取未提交):最低隔离级别,会读取到其他事务未提交的数据(脏读);
    • Read Committed (读取已提交): 事务过程中可以读取到其他事务已提交的数据,(不可重复度);
    • Repeatable Read (可重复读):每次读取相同结果集,不管其他事务是否提交,(幻读);
    • Serializable(串行化) : 事务排队,隔离级别最高,性能最差;

4.1 MySQL事务实现原理

  • MVCC

    • 多版本并发控制

    • 解决读-写的冲突问题,读的时候不加锁,

    • 通过隐藏列实现 (DB_TRX_ID、DB_ROLL_PTR)

      DB_TRX_ID: 写入时的事务ID(递增);

      DB_ROLL_PTR: 存的回滚指针,偏移量

    1. 当前读(select for update)
    2. 快照读 (select Current TRX ID 98)

InnoDB理论基础和工作总结

InnoDB理论基础和工作总结

  • 可见性分析 ( 判断我能看见的是哪个版本)

    • 创建快照这一刻,还未提交的事务;(我之前创建的还没提交的事务,我看不到)
    • 创建快照之后创建的事务;(新开始的事务,我看不到)
  • Read View

    • 快照读 活跃事务列表 (每个事务都有一个事务TRX_ID,放到一个列表里;事务ID是递增的)
    • 列表中最小事务ID
    • 列表中最大事务ID
  • undo log (保证事务回滚用的)

    • 回滚日志
    • 保证事务原子性
    • 实现数据多版本
    • delete undo log:用于回滚,提交即清理;
    • update undo log :用于回滚,同时实现快照读,不能随便删除

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3BxCKWYi-1596020556961)(./img/undolog.png)]

    思考: undolog如何清理?(insert delete 提交以后就可以清理了,)

    依据系统活跃的最小事务ID Read View (比他小的都可以清理掉)

    为什么Inno DB count(*) 这么慢?

    因为他去数记录数了,因为有MVCC,每个人看到的数据和记录数不太一样,没有一个准确的计数,所以他要去数数

  • redo log (保证事务有效性,一致性用的)

    • 实现事务的持久性,(保证事务有效,数据真正写进磁盘,不会丢)

    • 记录修改

    • 用于异常恢复

    • 循环写文件

      • Write Pos:写入位置

      • Chick Point:刷盘位置

      • Chick Point --> Write Pos :待落盘数据 (刷过盘的区域,就可以重复使用覆盖去写入数据了,)

        InnoDB理论基础和工作总结

    • 写入流程

      • 记录页的修改,状态为prepare
      • 事务提交,将事务记录为commit状态

InnoDB理论基础和工作总结

  • 刷盘时机

    • innodb_flush_log_at_trx_commit

    • InnoDB理论基础和工作总结

      • 每秒刷一次、
      • 每次commit刷一次、
      • 每次只要commit就写文件后异步刷盘
    • redolog的意义是什么

      • 体积小,记录页的修改,比写入页代价低;(,比直接写入多个页,代价小的多)
      • 末尾追加,随机写变顺序写,发生改变的页不固定(用一小块儿地方,把要做的事儿先记录下来,追加写,)

5. MySQL InnoDB锁机制详解

5.1 InnoDB锁的种类

  • 锁粒度

    • 行级锁

      • 作用在索引上
      • 聚簇索引 & 二级索引

      where条件根据主键查,锁聚簇索引,InnoDB理论基础和工作总结

      where条件根据二级索引查,锁二级索引,InnoDB理论基础和工作总结

      RC隔离级别(读取已提交) & RR隔离级别 (可重复读) 不是太一样;

    • 间隙锁(gap)

      • 解决可重复读模式下的幻读问题;
      • GAP锁不是加在记录上;(GAP是加在非唯一索引上的,主键索引、唯一索引就不需要GAP锁了)
      • GAP锁住的位置,是两条记录之间的GAP;
      • 保证两次当前读返回一致的记录;

      两次当前读之间,其他的事务不会插入新的满足条件的记录;

      InnoDB理论基础和工作总结

      X 就是uid主键

    • 表级锁

      • lock tables (偏运维,dba用,锁表)

      • 元数据锁(meta data lock,MDL) ((偏运维更新数据,修改字段)

      • 全表扫描"表锁" (where条件没有索引,)

        InnoDB理论基础和工作总结

        没有索引,只能走主键全表扫描,所有记录加锁返回,然后又MySQL Server层进行过滤;

        1. RC(读取已提交):通过行锁 锁住所有的记录
        2. RR(可重复读): 通过行锁 锁住所有的记录 和 间隙

        执行的时候要谨慎

  • 锁的类型

    • 共享锁(S)
      • 读锁,可以同时被多个事务获取,阻止其他事务对记录的修改
    • 排他锁(W)
      • 写锁,只能被一个事务获取,允许获得锁的事务修改数据;

5.2 MySQL InnoDB加锁的过程剖析

  • InnoDB加锁的过程

    • InnoDB理论基础和工作总结

    • 锁是执行过程中一条一条加上去的。

    • 死锁的情况:

      InnoDB理论基础和工作总结

      (基本上所有的死锁产生的原因都是因为加锁的顺序导致死锁的)

  • 2

  • 3

6. MySQL使用实践经验分享

6.1 索引和数据类型

  • 联合索引:优于多列独立索引
    • (A,B,C) 好于 A,B,C
  • 索引顺序: 选择性高的在前面
  • 覆盖索引: Key里面包含要查询的数据
  • 索引排序:索引同时满足查询和排序 (索引本身是有序的)

  • 数据库字符集使用utf8mb4 (表情特殊字符uft8支持不了)

  • VARCHAR按实际需要分配长度

    (varchar不定长,按照实际长度去分配,比如设置1024个字符长度,实际只能存下700+个字节,剩下的数据怎么办,是存在溢出页里,) 每一页16K,至少要存两条记录,

  • 文本字段建议使用VARCHAR

  • 时间字段建议使用long (用时间戳去存,date,datetime万一迁移数据库麻烦,long基本类型都支持)

  • bool字段建议使用tinyint (减少占用空间)

  • 枚举字段建议使用tinyint

  • 交易金额建议使用long (1块9毛9,电商存成分,除非需要精确计算的;股票银行,存成其他Decimal。)

  • 禁止使用“%”前导的查询

  • 禁止在索引列进行数学运算

    • 会导致索引失效
    • select * from t1 where id+1 > 1121 (不会使用索引)
    • select * from t1 where id > 1121 - 1 (会使用索引

  • 表必须有主键,建议使用业务主键
  • 单张表中索引数量不超过5个;
  • 单个索引字段数不超过5个;
  • 字符串索引使用前缀索引,前缀长度不超过10个字符;(前缀取一定长度就行,不能把所有字段当成索引)

6.2分库分表

  • 是否分表(什么时候分表,)
    • 建议单标不超过1KW (超过1KW,数据库可能会有性能问题了,如果只是根据主键查询,可以大一点,根据业务场景来判断,)
    • 评估下表的数据量,大数据量的上来先分好
  • 分表方式
    • 取模:存储均匀 & 访问均匀
      • 每个分表数据量差不多(存储均匀)
      • 流量查询,大家尽量均摊流量 (访问均匀)
    • 按时间: 冷热库
      • 订单从生成到结束,30天后,基本上就很少访问了,适合冷热库的场景
  • 分库
    • 按业务垂直分
      • userDB,商品DB,订单DB
    • 水平拆分多个库
      • u

6.3 分库分表案例分享

  • 用户库分表

    • InnoDB理论基础和工作总结

    tuser表按照主键uid 取模进行分表,如果需要根据手机号进行查询用户怎么查?

    1. 跟业务相关的: 根据uid的查询需求和根据phone的查询需求, uid/phone = 1/10000 很极端,就按照手机号分表;
    2. uid/phone = 1000/1 也很极端,就按照uid分表;
    3. uid/phone = 1/10 不极端,假如按照手机号phone分表了,就需要建立一个根据uid查手机号phone的映射表,查询的时候先通过映射表找到具体要查的表,;
  • 商品库分表

    • InnoDB理论基础和工作总结

    按照商品pid分表,商家如何查询自己发布的商品?

    1. 这个时候再通过建立映射表(pid–>uid)就不太合适了,()

    2. 用户UID和商品PID都是业务主键;

    3. 生成商品Id的时候(ID生成算法),找出一个段,公用一个段;

      InnoDB理论基础和工作总结

      用公用那一段pub去模128

  • 系统消息库分表

    • 时效性强
    • 冷热数据拆分

    IM给用户发消息,都是有失效的,先想到按冷热数据的形式拆分表,按照日期分表,

    InnoDB理论基础和工作总结

    系统消息保存30天(超过30天的数据清理掉);如果要查询自己过去30天的数据,30个表都要查;大部分场景都是跨月的。

    InnoDB理论基础和工作总结

    写数据时,数据双写;

    假如现在是19年01月01号: 写数据时,除了往tsysmsg_1901表写数据外,还往tsysmsg_1902表写数据;

    1月份过完了,tsysmsg_1901表和tsysmsg_1902表中关于1月份的数据是一样的。然后删除tsysmsg_1901表后,可以做到 在19年02月03号查询过去30天的消息时,一直都是单表查询;

    写了两份数据,对存储不友好,但是对业务友好;

  • 分库分表-分表分少了怎么办?

    分表只分了128个表(部署方式一主一从),数据量大了以后;不行了;怎么办?

    1. 再拉一个新的从库出来;(他里面也有128个表)
    2. 在某一刻把这个主从断开(晚上低峰期);
    3. 这个从库成为一个新的主,(也是一主一从);(拆成了两组实例,每组各有128个表,共256个表)
    4. 业务此时修改路由算法;(写入数据可以路由到两个Master库,原本该写入到table_0的数据,由两个库的table_0共同分摊; 查询数据,两个Master库此时都是全量的数据,查询都能查到;)
    5. 后台把历史数据里的 垃圾清理掉
      • 如记录Id=001新路由逻辑该在Master_node1中, 就把Master_node2不该留的数据如Id%2=0 的数据删掉; ,
      • 如记录Id=002新路由逻辑该在Master_node2中, 就把Master_node1不该留的数据如Id%2=1 的数据删掉; ,

    -InnoDB理论基础和工作总结

MySQL学习笔记总结

相关文章: