MySQL InnoDB的数据由B+树来组织,数据记录存储在B+树数据页(page)中,每个数据页16kb,数据页
包括页头、虚记录、记录堆、自由空间链表、未分配空间、slot区、页尾七部分组成。
所有的非终端结点可以看成是索引部分,结点中仅含有其子树根结点中最大(或最小)关键码。
56个
字节,记录本页的信息,包括页的左右兄弟页指针(双向链表,可使用此双向链表遍历数据,会比遍历树块
),页面使用情况等。比页内最小主键还小
)、最大虚记录(比页内最大主键还大
),表示本页数据key
的范围。采用什么方式存储,主要考量读写性能。物理连续,每次插入可能涉及数据移动,写入性能低,查询
效率也无法像数组一样使用二分查找,数组的数据类型一致,数据长度一致,但记录数据的长度因存在变长字段而不定。
逻辑连续,写入不涉及数据移动性能高,查询需要利用slot区优化,不优化需要遍历整个链表,优化后只需要遍历subList。
字段:长度+内容,MySQL varchar类型的字段默认给的长度为什么是255而非256,因为255时,长度用1个字节就可以了。
每个PageSize 16kB,每页至少存2条数据,页内单行数据最大8kb,超出8kb后会存在溢出表,超出的部分放到溢出页中(称之为【行溢出】,当某一行数据过大,导致数据页存放不下时,我们把这种情况叫做行溢出,简单的解决方式就是把记录存储在溢出页(磁盘的其它空闲地方)中)
页分裂机制: 页分裂机制
聚簇索引
聚集索引是一种对磁盘上实际数据重新组织以按指定的一个或多个列的值排序,即使没有创建索引,也会存在。InnoDB通过主键聚集数据,即使没有设置主键,InnoDB也会使用第一个非空唯一列作为主键,如果非空唯一列也不存在,则InnoDB会将RowId作为主键;聚集索引的叶子节点存放是实际数据,数据按主键顺序存储,因此为了插入数据的效率,应采取递增主键
。
二级索引
除了聚簇索引,其他的索引都可以称为二级索引,包括唯一索引、普通索引、联合索引。二级索引的叶子结点的data存放不是实际的数据,而是数据的主键,通过主键再去聚簇索引上查找数据
。因此尽量使用聚簇索引。
一次查询需要走两遍索引(称为回表),主键大小会影响所有索引的大小。
联合索引本质:
当创建(a,b,c)联合索引时,相当于创建了(a)单列索引,(a,b)联合索引以及(a,b,c)联合索引
想要索引生效的话,只能使用 a和a,b和a,b,c三种组合;当然,我们上面测试过,a,c组合也可以,
但实际上只用到了a的索引,c并没有用到!
引擎自动优化:
(A,B,C) 如果查找A=XX,B=XX,C=XX,可以命中,但是A=XX,C=XX,B=XX,也能命中,因为数据引擎
会将sql调整优化为A=XX,B=XX,C=XX。
主键选择
自增主键:顺序写入,写效率高,步长为1,但是容易暴露商业信息,或者每次查询走二级索引;
随机主键:如UUID,易造成页分裂,写入效率低;
业务主键:如雪花算法,写入、查询的效率都很高,可直接使用一级索引;
联合主键:影响索引大小,不易维护,不建议使用;
雪花算法详解:传送门
联合索引使用
按区分度排序:(a,b,c),多的放前面,如果上亿数据
,用a就能筛选后得到的数据就很少,那么,后面效率就高了;
覆盖索引:
explain
时,如果Extra为Using index则使用了覆盖索引。覆盖索引的解释为,索引中包含要查询的结果(小于等于),不需要回表查询,大数据常用。
create index UIX_BINFO_CUSERID
ON busi_busi_note(BUSI_INFO_ID,CREATER_USER_ID)
explain select BUSI_BUSI_NOTE_ID,CREATER_USER_DATE from busi_busi_note
where BUSI_INFO_ID='1579296835263004672' and CREATER_USER_ID='1111111207'
explain select * from busi_busi_note where BUSI_INFO_ID =
'1579296835263004672' and CREATER_USER_ID = '1111111207'
explain select BUSI_BUSI_NOTE_ID from busi_busi_note where
BUSI_INFO_ID = '1579296835263004672' and CREATER_USER_ID = '1111111207'
倒排索引
,通过value查key,如小孩(file1id,file2id,file3id)1.InnoDB会在内存中申请一片内存(内存池),数据是以数据页为最小单位(16k)通过映射关系加载到内存,加载到内存的数据页分为空数据页、冷数据页、热数据页、脏数据页。如果数据页被使用完毕,然后InnoDB会通过LRU、LFU等方式通过映射关系进行数据内外存置换,淘汰冷数据、加载新数据,脏数据固化等
LRU是Least Recently Used
的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。但是LRU全表扫描时,一次性加载大量数据,可能会将内存池完全更新一遍,无论原来内存池中有多少热数据都会被淘汰,所以InnoDB在LRU基础上进行了优化,避免热数据被淘汰。
优化:避免热数据被淘汰,(访问时间+频率)、(两个LRU表:一个热数据的LRU表,一个冷数据的LRU表,只淘汰冷数据的LRU表,热数据转冷,冷数据转热);
数据装载:
mysql最早经历123步,后来13,再优秀的产品也是一步一步迭代过来的。
select * from table
全表扫描时,会将某page加载到内存,然后逐条遍历数据,那么短时间内此page会高频次被使用,那么此页就会称为热数据,当大量的页成为热数据时,原来的热数据就会被误淘汰。如果每次使用到的数据页都移动至LRU_new的head,但由于在多线程,存在LOCK,可能会串行移动,效率低。MySql的设计思路是减少移动次数, freed_page_clock: Buffer Pool淘汰的页数,如果总淘汰数a减去当前page上次处于head时的Buffer Pool淘汰的页数b,大于LRU_new的1/4时,开启淘汰。
A:原子性
,多条操作要么全部成功、要么全部失败; undo log
C:一致性
,通过AID保证I:隔离性
,并行事务之间相互隔离,互不干扰; MVCC
D:持久性
,事务提交后,数据一定会被持久化,永久生效; redo log
脏读:
隔离性最差,可读到未提交
的数据(即未生效的数据,系统外的数据),称之为脏读;不可重复读:
隔离性好点,读已提交
的数据(即自己没改,被其他事务改的数据,但事务也未完全隔离开;幻读:
隔离性再严格一点,可重复读
数据(无论其他事务提交没提交读的都一样),会造成读取的数据,与实际情况不符,仿佛产生了幻觉一样。select操作后得到的结果无法支撑后续的业务操作; 比如数据库,事务A,T1时刻查询有id为1、2、3的三条数据,T2时刻事务B插入了一条id为4的数据,
此时事务A查询数据还是3条,这时事务A执行插入一条数据,id为4,将出错,但这个时候事务A不知
怎么回事,仿佛出现幻觉一样。
RR级别(可重复读级别)解决了幻读,只是解决了特定场景下(两次当前读)的幻读,通过gap锁。
Next-Key Locks
Next-Key Locks是在存储引擎
innodb、事务级别在可重复读的情况下使用的数据库锁,官网上有介绍,Next-Key Locks是行锁和gap锁
的组合。行锁是什么我们都很清楚,这篇文章主要简单分析一下mysql中的gap锁是什么。innodb默认的锁就是Next-Key locks。
GAP锁
gap锁,又称为间隙锁
。存在的主要目的就是为了防止在可重复读的事务级别下,出现幻读
问题。
在可重复读的事务级别下面,普通的select读的是快照,不存在幻读情况,但是如果加上for update的话,读取是已提交事务数据,gap锁
保证for update情况下,不出现幻读。
那么gap锁到底是如何加锁的呢?
假如是for update级别操作,先看看几条总结的何时加锁的规则。
唯一索引
精确等值检索,Next-Key Locks就退化为记录锁,不会加gap锁
范围检索,会锁住where条件中相应的范围,范围中的记录以及间隙,换言之就是加上记录锁和gap 锁(至于区间是多大稍后讨论)。
不走索引检索
,全表间隙加gap锁、全表记录加记录锁
非唯一索引
精确等值检索,Next-Key Locks会对间隙加gap锁(至于区间是多大稍后讨论),以及对应检索到的记录加记录锁。
范围检索,会锁住where条件中相应的范围,范围中的记录以及间隙,换言之就是加上记录锁和gap 锁(至于区间是多大稍后讨论)。
非索引检索
,全表间隙gap lock,全表记录record lock
1、MySql事务实现原理:
存储记录除了普通字段以外还会存事务ID来标注这条记录是那个事务写的;回滚指针指向上一个版本记录快照(可能没有上一版本)
。当前读指读当前存储引擎的最新版本,会将最新版本数据加锁(唯一索引定值查询行锁,范围查询Next-key锁;非唯一索引定值查询与范围查询均使用Next-key锁;无索引表锁,但是索引失效时会转化为全表扫描,行锁得会转化为表锁),其他的当前读就不能读了,只能等着当前读
当前读:读取最新提交的数据,当前读加锁,而快照读不加锁。
例如下面语句都会按最新提交的数据进行操作:
select … for update; (for update加的是互斥锁)
select ... lock in share mode; (lock in share mode加的是共享锁【读锁】)
insert、update、delete;
当前读本质上是基于锁的并发读操作
快照读:大多是快照读,读历史指定版本,可重复读某一版本,这就是MVCC实现可重复读,且此版本为历史版本,历史版本不会被锁,解决了读写并发的问题。gap锁,next锁
可见性判断:即使是读快照,但是不能随便读一个版本,所以应该读那个版本应该有个判断。mysql在快照读的时候创建快照(创建快照我的理解是当时的数据状态,实现是创建一个数组),如select时,原则是不能读创建快照这一刻还未提交的事务、以及创建快照后创建的事务
,即第一次读是选取已提交的事务id最大的数据,再次读取的按快照时刻存储的数组查询数据,改的话只能改当前数据。
创建快照举例: session A只需要做一件事:用一个数组,来记录当前活跃的事务ID。
假设session A的事务ID是97,当前还有另外两个事务,事务ID是94、96,所以session A会生成
一个[94,96,97]的数组。
接着,session B执行了update语句,来更新id=1这一行数据,给这一行数据生成一个新的版本,
假设session B的事务ID是98,因此这行数据就有了两个版本:这时候,session A又来select了,
当前版本是session B生成的,那session A是如何找到之前的版本的呢?这时候,session A
一开始生成的事务数组就派上用场了,session A的事务数组是[94,96,97],最小事务ID是94,最大
事务ID是97,所以,当它遇到一行数据时,会先判断这行数据的版本号X:如果X大于97,那么意味着这
行数据,是在session A开始之后,才提交的,应该对session A不可见如果X小于97,那么分两种情
况:如果X在数组里面,比如X是96,那么意味着,当session A开始时,生成这个版本的数据的事务,
还没提交,因此这行数据对Session A不可见,如果X不在数组里面,比如X是95,那么意味着,
当session A开始时,生成这个版本的数据的事务,已经提交,因此这行数据对Session A可见。
现在session A开始遍历id=1这行数据的所有版本:当前版本是98,大于97,所以不可见,继续看上
一个版本;再往上,版本是90,小于94,可见,就它了,所以session A select出来的id=1的数据,c
的值是1。
可见性的原则:
版本未提交,不可见;
版本已提交,但是在快照创建后提交的,不可见;
版本已提交,而且是在快照创建前提交的,可见。
这其实就是可重复读的想要实现的效果。
问题:为什么mysql没有计数器?
更新数据前先记undo log,记录历史版本,undo log记录完毕后更新数据,
提交事务前,redo log就已经写好了!
redo log也得刷盘,事务也得刷盘,为什么 redo log就不丢数据呢?
因为事务时整个page的刷盘,16k, 且page位置不固定(随机写),代价大;
redo log只是一条日志,体积小,比page写代价小;且是写文件,追加写,是顺序写;