微信订阅号:魔术师耿
我是一个互联网公司的螺丝钉;
魔术师耿
MySQL InnoDB理论基础
以下内容是观看陈东老师的公开课,自己总结的笔记
1. MySQL InnoDB存储原理深度剖析
1.1 MySQL 记录存储
- 页头
- 记录页面的控制信息,共占用56字节,包括页的左右兄弟页面指针、页面空间使用情况等。
- 虚记录
- 最大虚记录:比页内最大主键还大
- 最小虚记录: 比页内最小主键还小
- 记录堆
- 行记录存储区,分为有效记录和已删除记录两种
- 自由空间链表
- 已删除记录组成的链表
- 未分配空间
- 页面未使用的存储空间
- Slot区
- 页尾
- 页面最后部分,占用8个字节,主要存储页面的校验信息;
1.2 页内记录维护
-
顺序保证
- 物理连续(X)
- 逻辑连续(MySQL用的这种)
-
插入的策略(先堵住空洞,再用新的)
- 自由空间链表
- 未使用空间
-
页内查询
- 遍历
- 二分查找
类似跳表的实现机制
2. MySQL InnoDB索引实现原理及使用优化分析
2.1 索引原理分析
-
聚簇索引
- 数据存储在主键索引中
- 数据按主键顺序存储
自增主键 VS 随机主键
-
二级索引
-
除主键索引以外的所有索引
-
叶子节点中存储主键值
-
一次查询需要走两遍索引
-
主键大小会影响所有索引的大小
-
-
联合索引
- Key由多个字段组成
- 最左匹配原则
- 一个索引只创建一颗树
- 按照第一列排序,第一列相同按第二列排序
- 如果不是按照最左开始查找,无法使用索引
- 不能跳过中间列
- 某列使用范围查询,后面的列不能使用索引
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存储引擎内存管理
- 预分配内存空间
- 数据以页为单位加载
- 数据内外存交换
3.1 InnoDB内存管理—技术点
-
内存池
-
内存页面管理
- 页面映射
- 页面数据管理
- 空闲页
- 数据页
- 脏页
-
数据淘汰
- 内存页面都被使用
- 需要加载新数据
页面淘汰
- LRU
思考:全表扫描对内存的影响?
热数据被挤出了内存
解决问题:
- 避免热数据被淘汰
思路:
-
访问时间 + 频率?(redis这么干的)
-
两个LRU表?
一个热LRU,一个冷LRU; 分段LUR
InnoDB怎么做的:
-
Buffer Pool
- 预分配的内存池,
-
Page
- 按页去加载,Buffer Pool的最小单位
-
Free list
- 空闲Page组成的链表
-
Flush list
- 脏页组成的链表
-
Page hash表
- 维护内存页和文件页的映射关系
-
LRU
-
内存淘汰算法
-
页面装载
-
磁盘数据到内存
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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_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的设计思路:减少移动次数
两个重要参考:(通过这两个参数来判断是否需要移动数据)
- freed_page_clock: Buffer Pool淘汰页数
- 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: 存的回滚指针,偏移量
- 当前读(select for update)
- 快照读 (select Current TRX ID 98)
-
-
可见性分析 ( 判断我能看见的是哪个版本)
- 创建快照这一刻,还未提交的事务;(我之前创建的还没提交的事务,我看不到)
- 创建快照之后创建的事务;(新开始的事务,我看不到)
-
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 :待落盘数据 (刷过盘的区域,就可以重复使用覆盖去写入数据了,)
-
-
写入流程
- 记录页的修改,状态为prepare
- 事务提交,将事务记录为commit状态
-
-
刷盘时机
-
innodb_flush_log_at_trx_commit
-
- 每秒刷一次、
- 每次commit刷一次、
- 每次只要commit就写文件后异步刷盘
-
redolog的意义是什么
- 体积小,记录页的修改,比写入页代价低;(,比直接写入多个页,代价小的多)
- 末尾追加,随机写变顺序写,发生改变的页不固定(用一小块儿地方,把要做的事儿先记录下来,追加写,)
-
5. MySQL InnoDB锁机制详解
5.1 InnoDB锁的种类
-
锁粒度
-
行级锁
- 作用在索引上
- 聚簇索引 & 二级索引
where条件根据主键查,锁聚簇索引,
where条件根据二级索引查,锁二级索引,
RC隔离级别(读取已提交) & RR隔离级别 (可重复读) 不是太一样;
-
间隙锁(gap)
- 解决可重复读模式下的幻读问题;
- GAP锁不是加在记录上;(GAP是加在非唯一索引上的,主键索引、唯一索引就不需要GAP锁了)
- GAP锁住的位置,是两条记录之间的GAP;
- 保证两次当前读返回一致的记录;
两次当前读之间,其他的事务不会插入新的满足条件的记录;
X 就是uid主键
-
表级锁
-
lock tables (偏运维,dba用,锁表)
-
元数据锁(meta data lock,MDL) ((偏运维更新数据,修改字段)
-
全表扫描"表锁" (where条件没有索引,)
没有索引,只能走主键全表扫描,所有记录加锁返回,然后又MySQL Server层进行过滤;
- RC(读取已提交):通过行锁 锁住所有的记录
- RR(可重复读): 通过行锁 锁住所有的记录 和 间隙
执行的时候要谨慎
-
-
-
锁的类型
- 共享锁(S)
- 读锁,可以同时被多个事务获取,阻止其他事务对记录的修改
- 排他锁(W)
- 写锁,只能被一个事务获取,允许获得锁的事务修改数据;
- 共享锁(S)
5.2 MySQL 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 分库分表案例分享
-
用户库分表
tuser表按照主键uid 取模进行分表,如果需要根据手机号进行查询用户怎么查?
- 跟业务相关的: 根据uid的查询需求和根据phone的查询需求, uid/phone = 1/10000 很极端,就按照手机号分表;
- uid/phone = 1000/1 也很极端,就按照uid分表;
- uid/phone = 1/10 不极端,假如按照手机号phone分表了,就需要建立一个根据uid查手机号phone的映射表,查询的时候先通过映射表找到具体要查的表,;
-
商品库分表
按照商品pid分表,商家如何查询自己发布的商品?
-
这个时候再通过建立映射表(pid–>uid)就不太合适了,()
-
用户UID和商品PID都是业务主键;
-
生成商品Id的时候(ID生成算法),找出一个段,公用一个段;
用公用那一段pub去模128
-
系统消息库分表
- 时效性强
- 冷热数据拆分
IM给用户发消息,都是有失效的,先想到按冷热数据的形式拆分表,按照日期分表,
系统消息保存30天(超过30天的数据清理掉);如果要查询自己过去30天的数据,30个表都要查;大部分场景都是跨月的。
写数据时,数据双写;
假如现在是19年01月01号: 写数据时,除了往tsysmsg_1901表写数据外,还往tsysmsg_1902表写数据;
1月份过完了,tsysmsg_1901表和tsysmsg_1902表中关于1月份的数据是一样的。然后删除tsysmsg_1901表后,可以做到 在19年02月03号查询过去30天的消息时,一直都是单表查询;
写了两份数据,对存储不友好,但是对业务友好;
-
分库分表-分表分少了怎么办?
分表只分了128个表(部署方式一主一从),数据量大了以后;不行了;怎么办?
- 再拉一个新的从库出来;(他里面也有128个表)
- 在某一刻把这个主从断开(晚上低峰期);
- 这个从库成为一个新的主,(也是一主一从);(拆成了两组实例,每组各有128个表,共256个表)
- 业务此时修改路由算法;(写入数据可以路由到两个Master库,原本该写入到table_0的数据,由两个库的table_0共同分摊; 查询数据,两个Master库此时都是全量的数据,查询都能查到;)
- 后台把历史数据里的 垃圾清理掉
- 如记录Id=001新路由逻辑该在Master_node1中, 就把Master_node2不该留的数据如Id%2=0 的数据删掉; ,
- 如记录Id=002新路由逻辑该在Master_node2中, 就把Master_node1不该留的数据如Id%2=1 的数据删掉; ,
-
MySQL学习笔记总结