数据库三范式
高级别范式的依赖于低级别的范式
一:确保每列的原子性
。属性不可分。
二:非主键列不存在对主键
的部分依赖 (要求每个表只描述一件事情。比如学生选课表Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。)
三:满足第二范式,并且表中的列不存在对非主键
列的传递依赖 (Sno -> Sdept -> Mname,改成Sno -> Sdept ,Sdept -> Mname)
分库分表
1、单表记录条数达到百万或千万级别时
2、解决表锁的问题
水平分表:表很大,分割后可以降低在查询时需要读的数据和索引的页数,同时也降低了索引的层数,提高查询次数
垂直分表:把主键和一些列放在一个表,然后把主键和另外的列放在另一个表中。
Sharding 策略:哈希取模:hash(key)%N。范围:可以是 ID 范围也可以是时间范围。映射表:使用单独的一个数据库来存储映射关系
设定网站用户数量在千万级,但是活跃用户数量只有1%,如何通过优化数据库提高活跃用户访问速度?
可以使用MySQL的分区,把活跃用户分在一个区,不活跃用户分在另外一个区,本身活跃用户区数据量比较少,因此可以提高活跃用户访问速度。
还可以水平分表,把活跃用户分在一张表,不活跃用户分在另一张表,可以提高活跃用户访问速度。
分区 (垂直分区/水平分区)
分区是将数据分段划分在多个位置存放,可以是同一块磁盘也可以在不同的机器。分区后,表面上还是一张表,但数据散列到多个位置了。
在下面的场景中,分区可以起到非常大的作用:
- 表非常大以至于无法全部都放在内存中,或者只在表的最后部分有热点数据,其他都是历史数据
- 分区表的数据更容易维护,如:想批量删除大量数据可以使用清除整个分区的方式。另外,还可以对一个独立分区进行优化、检查、修复等操作
- 分区表的数据可以分布在不同的物理设备上,从而高效地利用多个硬件设备
- 可以使用分区表来避免某些特殊的瓶颈,如:innodb的单个索引的互斥访问,ext3文件系统的inode锁竞争等
- 如果需要,还可以备份和恢复独立的分区,这在非常大的数据集的场景下效果非常好
- 优化查询,在where字句中包含分区列时,可以只使用必要的分区来提高查询效率,同时在涉及sum()和count()这类聚合函数的查询时,可以在每个分区上面并行处理,最终只需要汇总所有分区得到的结果。
事务隔离级别
Read uncommitted 、Read committed 、Repeatable read 、Serializable
1.脏读:
脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
2.不可重复读:
是指在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。(即不能读到相同的数据内容)
例如,一个编辑人员两次读取同一文档,但在两次读取之间,作者重写了该文档。当编辑人员第二次读取文档时,文档已更改。原始读取不可重复。如果只有在作者全部完成编写后编辑人员才可以读取文档,则可以避免该问题。
3.幻读:
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象
发生了幻觉一样。
例如,一个编辑人员更改作者提交的文档,但当生产部门将其更改内容合并到该文档的主复本时,发现作者已将未编辑的新材料添加到该文档中。如果在编辑人员和生产部门完成对原始文档的处理之前,任何人都不能将新材料添加到文档中,则可以避免该问题。
不可重复读和幻读区别:
不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录 增多或减少了
与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别下使用的是Next-Key Lock 锁算法,结合MVCC机制可以避免幻读的产生,这与其他数据库系统(如SQL Server) 是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 已经可以完全保证事务的隔离性要求。
Mysql Repeatable Read级别,只使用当前读通过next-key lock或者快照读通过mvcc能避免幻读。混用当前读+快照读就无法避免啦。
Select语句也可以是当前读,只需要加锁。(LOCK IN SHARE MODE[共享锁] / FOR UPDATE[排他锁])
PostgreSQL Repeatable Read级别能避免幻读,它没有当前读,都是快照读。
索引
索引与全表扫描主要区别就是扫描数据量大小以及IO的操作,全表扫描是顺序IO,索引扫描是随机IO,MySQL对此做了优化,增加了change buffer特性来提高IO性能。
索引设计->降低接口响应时间->降低服务器配置->降低成本
程序局部性:一个程序在访问了一条数据之后,在之后会有极大的可能再次访问这条数据和访问这条数据的相邻数据。在操作系统的概念中,当我们往磁盘中取数据,假设要取出的数据的大小是1KB,但是操作系统并会取出4KB(一个页表项)的数据。在MySQL的InnoDb引擎中,页的大小是16KB,是操作系统的4倍。
二叉树插入有序数列会变成链表
,而平衡二叉查找树,左右子树高度差不得超过1,时间复杂度O(logn),会产生非常多IO次数。(红黑树也是为了保证树的平衡性,降低树的高度)
B树是一种多路搜索树,每个节点可以拥有多于2个孩子的节点。(不限制路数会退化成有序数组)它常用于文件系统
的索引,因为一棵树无法一次性加载进入内存,涉及磁盘操作
。可以每次加载B树的一个节点
,一步步往下找。
索引的优点
- 大大减少了服务器需要扫描的数据行数。
- 帮助服务器避免进行排序和分组,以及避免创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,不需要排序和分组,也就不需要创建临时表)。
- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一起)。
B+ Tree
B+树的数据都在叶子节点,同时叶子节点之间还加了指针形成链表。查找起始于根节点,自顶向下遍历树,选择其分离值在要查找值的任意一边的子指针。在节点内部使用二分查找确定位置。
B+树主要是在所有的叶子结点中增加了指向下一个叶子节点的指针,因此 InnoDB 建议为大部分表使用默认自增的主键作为主索引。(如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。)
树的常见特性
- AVL 树
平衡二叉树,左右子树树高差不超过1,那么和红黑树比较它是严格的平衡二叉树,平衡条件非常严格(树高差只有1),只要插入或删除不满足上面的条件就要通过旋转来保持平衡。由于旋转是非常耗费时间的。所以 AVL 树适用于插入/删除次数比较少,但查找多的场景。 - 红黑树
通过对从根节点到叶子节点路径上各个节点的颜色进行约束,确保没有一条路径会比其他路径长2倍,因而是近似平衡的。所以相对于严格要求平衡的AVL树来说,它的旋转保持平衡次数较少。适合,查找少,插入/删除次数多的场景。(现在部分场景使用跳表来替换红黑树,redis 使用跳表(skiplist)而不是使用 red-black,因为简单且排序集通常是许多Zrange或Zrevrange操作的目标,即作为链表遍历跳过列表) - B/B+ 树
多路查找树,出度高,磁盘IO低,一般用于数据库系统中。
索引选择为什么是B +
B+Tree由三部分组成:根root、枝branch以及Leaf叶子,其中root和branch不存储数据,只存储指针地址,数据全部存储在Leaf Node,同时Leaf Node之间用双向链表链接
- 为啥不用hash索引呢
- 取多条数据时,B+树由于所有数据都在叶子结点,不用跨层,同时由于有链表结构,只需要找到首尾,通过链表就能把所有数据取出来了。
- 数据库中的索引一般是在磁盘上,数据量大的情况可能无法一次装入内存,B+树的设计可以允许数据分批加载,同时树的高度较低,提高查找效率。
InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
- B + 树与红黑树
- 磁盘 IO 次数:B+ 树一个节点可以存储多个元素,相对于红黑树的树高更低。
- 磁盘预读特性:为了减少磁盘 I/O 操作,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道。每次会读取页的整数倍。
操作系统一般将内存和磁盘分割成固定大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。
- B + 树与 B 树
- B+ 树的磁盘 IO 更低 B+ 树的内部节点并没有指向关键字具体信息的指针。因为B树不管叶子节点还是非叶子节点,都会保存数据,这样导致在非叶子节点中能保存的指针数量变少(有些资料也称为扇出),指针少的情况下要保存大量数据,只能增加树的高度,导致IO操作变多,查询性能变低。
- B+ 树的查询效率更加稳定 由于非叶子结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。
- B+ 树元素遍历效率高 B+树只要遍历叶子节点就可以实现整棵树的遍历。
InnoDB一棵B+树可以存放多少行数据
- InnoDB存储引擎的最小存储单元是页,页可以用于存放数据也可以用于存放键值+指针,在B+树中叶子节点存放数据,非叶子节点存放键值+指针。
- 索引组织表通过非叶子节点的二分查找法以及指针确定数据在哪个页中,进而在去数据页中查找到需要的数据;
先假设B+树高为2,即存在一个根节点和若干个叶子节点,那么这棵B+树的存放总记录数为:根节点指针数*单个叶子节点记录行数
。
上文我们已经说明单个叶子节点(页)中的记录数=16K/1K=16。(这里假设一行记录的数据大小为1k)。
假设主键ID为bigint类型,长度为8字节,而指针大小在InnoDB源码中设置为6字节,这样一共14字节,我们一个页中能存放多少这样的单元,其实就代表有多少指针,即(2的14次方)16384/14(类型大小+指针大小)=1170。那么可以算出一棵高度为2的B+树,能存放1170*16=18720条这样的数据记录。
一个高度为3的B+树可以存放:1170_1170_16=21902400条这样的记录。所以在InnoDB中B+树高度一般为1-3层,它就能满足千万级的数据存储。在查找数据时一次页的查找代表一次IO,所以通过主键索引查询通常只需要1-3次IO操作即可查找到数据。
聚集索引与非聚集索引
InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录
,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
辅助索引也称为二级索引,索引中除了存储索引列外,还存储了主键id。因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找,这个过程也被称作回表。
- InnoDB(聚集索引, 主键索引与行记录是存储在一起,必须要有且只有一个),其中叶子中存放的就是数据行
普通索引,其中叶子节点存储主键值,通过辅助索引查找到主键之后,再通过查找的主键去查找主键索引。(回表查询,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低。),考虑使用
覆盖索引
代替
- myisam(非聚集索引,索引与行记录是分开存储的,索引的叶子节点储存数据记录的地址/指针)
回表
Innodb把表中所有数据都存放在主索引的叶子节点里,在往表里插入数据时,可能会导致主索引结构发生变化(分裂或合并的操作),也就导致了数据地址的变化,所以为什么要再回表一次确保拿到正确的数据。
而myisam的做法使得B+树结构发生变化时,还需要同步更新其他的索引。
当覆盖索引生效的时候能够避免“回表查询”操作,减少了io查询的次数。如果数据库引擎使用了myisam则不需要回表,直接根据对应的叶子地址去查询数据即可。
索引的重建过程
现某张表的数据量和其存储的实际数据数目不匹配,这种情况通常是因为删除了过多的数据。alter table t engine=InnoDB
可以用于重建整张表的索引数据,将表里以前的一些索引空洞给一并压缩进行重新整合。
在mysql进行索引树重建的时候,会将原先表的所有数据都拷贝存入另外一张表,在拷贝的期间如果有新数据写入表的话,会建立一份redo log文件将新写入的数据存放进去,保证整个过程都是online的,因此这也被称为Online DDL,redo log在这整个过程中就起到了一个类似缓冲池的角色。
最左前缀匹配原则,MySQL会一直向右匹配直到遇到范围查询(>、<、between、like)
。
=和in可以乱序,比如 a=1 and b=2 and c=3 建立(a,b,c)索引可以任意顺序,MySQL的查询优化器会帮你优化成索引可以识别的形式。
MyISAM和InnoDB都使用B+树来实现索引:
MyISAM的索引与数据分开存储
MyISAM的索引叶子存储指针,主键索引与普通索引无太大区别
InnoDB的聚集索引和数据行统一存储
InnoDB的聚集索引存储数据行本身,普通索引存储主键
InnoDB一定有且只有一个聚集索引
InnoDB建议使用趋势递增整数作为PK,而不宜使用较长的列作为PK
HASH索引
(1):BTree索引可能需要多次运用折半查找来找到对应的数据块
(2):HASH索引是通过HASH函数,计算出HASH值,在表中找出对应的数据
(3):大量不同数据等值精确查询,HASH索引效率通常比B+TREE高
(4):HASH索引不支持模糊查询、范围查询和联合索引中的最左匹配规则,而这些Btree索引都支持
MySQL索引设计
前缀索引
业务发展初期,为了快速实现功能,对一些数据表字段的长度定义都比较宽松,系统运行了一段时间之后,查询users表最大的nickname长度为30,这个时候就可以创建前缀索引来减小索引的长度提升性能。
前缀索引虽然可以减小索引的大小,但是不能消除排序。(排序的时候需要回表)
联合索引
create index idx_obj on user(age asc,height asc,weight asc)
- idx_obj这个索引会根据age,height,weight进行
排序
(联合索引的排序有这么一个原则,从左往右依次比较大小,因此直接用height和weight不会走索引。) - idx_obj这个索引是一个
非聚簇索引
,查询时需要回表。
复合索引设计原则
- 将范围查询的列放在复合索引的最后面,例如idx_status_create_time。
- 列过滤的频繁越高,选择性越好,应该作为复合索引的前导列,适用于等值查找,例如idx_user_id_status。
- 联合索引范围查询 where a>1 and b=1 大于失效问题。因为b是无序的,无法通过二分查找去找到b
跳跃索引
MySQL 8.0也实现Oracle类似的索引跳跃扫描,在优化器选项也可以看到skip_scan=on。
适合复合索引前导列唯一值少,后导列唯一值多的情况,如果前导列唯一值变多了,则MySQL CBO不会选择索引跳跃扫描,取决于索引列的数据分表情况。
MySQL死锁分析
show engine innodb status;
(锁会加在聚集索引)explain
- 间隙锁互斥 (并发事务,间隙锁可能互斥): 事务A删除某个区间内的一条不存在记录,获取到
共享间隙锁
,会阻止其他事务B在相应的区间插入数据,因为插入需要获取排他间隙锁
。 - 共享排他锁死锁(并发插入相同记录,可能死锁) : 三个事务都试图往表中插入一条为7的记录,A先执行,插入成功,rollback,id=7排他锁释放。B和C要想插入成功,必须获得id=7的排他锁,但由于双方都已经获取到id=7的共享锁,它们都无法获取到彼此的排他锁,死锁就出现了。当然,InnoDB有死锁检测机制,B和C中的一个事务会插入成功,另一个事务会自动放弃
- 并发间隙锁的死锁(并发插入,可能出现间隙锁死锁(难排查)) : A执行delete后,会获得(3, 10)的共享间隙锁。
B执行delete后,也会获得(3, 10)的共享间隙锁。
A执行insert后,希望获得(3, 10)的排他间隙锁,于是会阻塞。
B执行insert后,也希望获得(3, 10)的排他间隙锁,于是死锁出现。
唯一索引中定义的类型与传入不一致,导致update时无法命中索引而对每一行都加锁(type:index:走索引的全表扫描;),导致并发时互相等待对方insert时的锁而出现死锁。
索引优化案例
要充分利用辅助索引包含主键id的特性,先通过索引获取主键id走覆盖索引扫描,不需要回表,然后再通过id去关联操作是高效的。
同时根据MySQL的特性使用分而治之的思想既能高效完成操作,又能避免主从复制延迟产生的业务数据混乱。
深度分页查询优化
- 以结果作为条件,已查询条件的变化换取分页的不变。
- 采用子查询模式,其原理依赖于覆盖索引。
SELECT * FROM product WHERE ID > =(select id from product limit 866613, 1) limit 20
- 复合索引:其原理同样是索引覆盖的思想,只不过是其以查询条件的一份作为索引,最终的索引字段是主键id。
表trade_info上有索引idx_status_create_time(status,create_time),等价于索引(status,create_time,id),对于典型的分页limit m, n来说,越往后翻页越慢,也就是m越大会越慢,因为要定位m位置需要扫描的数据越来越多,导致IO开销比较大。
这里可以利用辅助索引的覆盖扫描来进行优化,先获取id,这一步就是索引覆盖扫描,不需要回表,然后通过id跟原表trade_info进行关联
select * from trade_info a ,
(select id from trade_info where status = 0 and create_time >= '2020-10-01 00:00:00' and create_time <= '2020-10-07 23:59:59' order by id desc limit 102120, 20) as b //这一步走的是索引覆盖扫描,不需要回表
where a.id = b.id;
分而治之
MySQL就需要注意了,一个SQL只能使用一个cpu core去处理,如果SQL很复杂或执行很慢,就会阻塞后面的SQL请求,造成活动连接数暴增,MySQL CPU 100%,相应的接口Timeout,同时对于主从复制架构,而且做了业务读写分离,更新500w数据需要5分钟,Master上执行了5分钟,binlog传到了slave也需要执行5分钟,那就是Slave延迟5分钟,在这期间会造成业务脏数据,比如重复下单等。
优化思路:先获取where条件中的最小id和最大id,然后分批次去更新,每个批次1000条,这样既能快速完成更新,又能保证主从复制不会出现延迟。
current_id = min_id;
for current_id < max_id do
update coupons set status = 1 where id >=current_id and id <= current_id + 1000; //通过主键id更新1000条很快
commit;
current_id += 1000;
done
sql语句调优
- 列类型尽量定义成数值类型,且长度尽可能短
- 根据需要建立多列联合索引 / 覆盖索引 (索引主要区别就是扫描数据量大小以及IO的操作,全表扫描是顺序IO,索引扫描是随机IO,MySQL对此做了优化,增加了change buffer特性来提高IO性能。)
- 多表连接 / 排序 / 分组 的字段上建立索引
- 避免在索引列上使用mysql的内置函数 / 表达式操作
- 查询结果只有一条用limit 1
or
可能会导致全表扫描,此时可以优化为union
查询;
如果
允许空值,不等于(!=)的查询,不会将空值行(row)包含进来
,此时的结果集往往是不符合预期的,此时往往要加上一个or条件,把空值(isnull)结果包含进来;
越是精确的查找 越会用到索引, 传两个参数时查出的数据过多,mysql觉得用不用索引都一样,而一个参数时,数据量少了,应该少于某个临界值,mysql觉得可以让索引起作用了。所以in/or 都有可能不走索引。
- 优化limit分页,返回上次最大查询记录(偏移量)。
- like语句前面尽量不要加%
- 避免在where子句中使用!=或<>操作符
- 联合索引一般遵循最左匹配原则。
- exist&in的合理利用。
B的数据量小于A,适合使用in,如果B的数据量大于A,即适合选择exist。
- 如果检索结果中不会有重复的记录,推荐union all 替换 union。
- 前缀索引:对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。
- 减少服务器端扫描的行数:最有效的方式是使用索引来覆盖查询。
explain关键字
type
字段(扫描方式):
- system:系统表,少量数据,往往不需要进行磁盘IO;(外层嵌套从
临时表
查询/系统表
) - const:常量连接;(命中
主键
,且被连接的部分是一个常量
) - eq_ref:主键索引(primary key)或者非空唯一索引(unique not null)等值扫描;(
对于前表的每一行(row),后表只有一行被扫描
) - ref:非主键非唯一索引等值扫描(对于前表的每一行(row),后表可能有多于一行的数据被扫描);
- range:范围扫描(
索引上
的范围查询); - index:索引树扫描(扫描索引上的全部数据);
- ALL:全表扫描(full table scan);
SQL语句执行得很慢
- 偶尔很慢
- 数据库在刷新脏页。(
redo log
被写满了,数据同步到磁盘中) - 拿不到锁。
show processlist
- 一直很慢
- 没用到索引
- 数据库自己选错索引。(系统采样来预测索引的基数,导致系统没有走索引)
innodb内部的线程
Change Buffer
对表执行 INSERT,UPDATE和 DELETE操作时, 索引列的值(尤其是secondary keys的值)通常按未排序顺序排列,需要大量I / O才能使二级索引更新。Change Buffer会缓存这个更新当相关页面不在Buffer Pool中,从而磁盘上的相关页面不会立即被读避免了昂贵的I / O操作。当页面加载到缓冲池中时,将合并缓冲的更改,稍后将更新的页面刷新到磁盘。该InnoDB主线程在服务器几乎空闲时以及在慢速关闭期间合并缓冲的更改 。
这里有两个数据修改,一个是表数据,一个是索引数据,表数据的修改还是在buff pool中,对索引数据的修改会被记录到change buff中,当进行刷盘时,会把这两个操作合并。
MySQL中InnoDB脏页刷新机制Checkpoint
事务提交时,先写重做日志,再修改内存数据页,这样就产生了脏页。
mysql脏页刷新的几种情况
- redo日志切换
- buffer pool不足
- mysql 自己觉得空闲
- mysql正常关机
当数据库宕机时,数据库不需要重做所有的日志,只需要执行上次刷入点之后的日志。这个点就叫做Checkpoint
MVCC (多版本并发控制)
MVCC (Multiversion Concurrency Control)是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问。
其它事务可以修改这条记录,反正每次修改都会在版本链中记录。SELECT可以去版本链中拿记录,这就实现了读-写,写-读的并发执行,提升了系统的性能。
MVCC只在 Read Committed 和 Repeatable Read
两个隔离级别下工作。其他两个隔离级别和MVCC不兼容,Read Uncommitted总是读取最新的记录行,而不是符合当前事务版本的记录行;Serializable 则会对所有读取的记录行都加锁。
默认事务隔离级别是RR(可重复读),防止幻读是通过 “行级锁+MVCC
“一起实现的(for update)。
而 MVCC 的实现依赖:隐藏字段、Read View、Undo log
- 隐藏字段
- DB_TRX_ID(6字节):表示最近一次对本记录行作
修改(insert | update)的事务ID
。 - DB_ROLL_PTR(7字节):回滚指针,
指向当前记录行的undo log信息
- DB_ROW_ID(6字节):随着新行插入而单调递增的行ID。
- Read View 结构(重点)
其实Read View(读视图),跟快照、snapshot是一个概念。
Read View主要是用来做可见性判断的, 里面保存了“对本事务不可见的其他活跃事务
”。
- low_limit_id:目前出现过的最大的事务ID+1,即下一个将被分配的事务ID
- up_limit_id:活跃事务列表trx_ids中最小的事务ID,如果trx_ids为空,则up_limit_id 为 low_limit_id。
- trx_ids:Read View创建时其他未提交的活跃事务ID列表。
- creator_trx_id:当前创建事务的ID,是一个递增的编号。
- Undo log
Undo log中存储的是老版本数据,当一个事务需要读取记录行时,如果当前记录行不可见,可以顺着undo log链找到满足其可见性条件的记录行版本。
- insert undo log : 事务对insert新记录时产生的undo log, 只在
事务回滚时需要
, 并且在事务提交后就可以立即丢弃。 - update undo log : 事务对记录进行delete和update操作时产生的undo log,不仅在
事务回滚时需要,快照读也需要
,只有当数据库所使用的快照中不涉及该日志记录,对应的回滚日志才会被purge线程删除。
Purge线程:为了实现InnoDB的MVCC机制,更新或者删除操作都只是设置一下旧记录的deleted_bit,并不真正将旧记录删除。
为了节省磁盘空间,InnoDB有专门的purge线程来清理deleted_bit为true的记录。purge线程自己也维护了一个read view,如果某个记录的deleted_bit为true,并且DB_TRX_ID相对于purge线程的read view可见,那么这条记录一定是可以被安全清除的。
不同事务或者相同事务的对同一记录行的修改,会使该记录行的undo log成为一条链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录。
当前读和快照读
快照读
(snapshot read):普通的 select 语句(不包括 select … lock in share mode, select … for update)当前读
(current read) :select … lock in share mode,select … for update,insert,update,delete 语句(这些语句获取的是数据库中的最新数据
)
Read Committed / Repeatable Read的Read View(快照)产生
- Repeatable Read :
每次读取前生成一个
- Read Committed :
第一次生成一个
在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。
单独靠MVCC并不能完全防止幻读,对“当前读语句”读取的记录行加记录锁(record lock)和间隙锁(gap lock)
,禁止其他事务在间隙间插入记录行,来防止幻读。也就是前文说的”行级锁+MVCC
“。
可见性比较算法
在innodb中,创建一个新事务后,执行第一个select语句的时候,innodb会创建一个快照(read view),快照中会保存系统当前不应该被本事务看到的其他活跃事务id列表(即trx_ids)。当用户在这个事务中要读取某个记录行的时候,innodb会将该记录行的DB_TRX_ID与该Read View中的一些变量进行比较,判断是否满足可见性条件。
LOG
对于一条写语句,写入的时候是先写redo日志后写binlog日志。
以innodb为例,实际顺序如下:
- 会话发起COMMIT动作
- 存储引擎层开启[Prepare]状态:在对应的Redo日志记录上打上Prepare标记
- 写入binlog并执行fsync(刷盘)
- 在redo日志记录上打上COMMIT标记表示记录提交完成
重做日志(redo log
) -重做日志,提供前滚操作
重做日志用来实现事务的原子性和持久性
。
- 日志由两部分组成,一部分是
内存中的重做日志缓冲(redo log buffer)
,是容易丢失的,另一个部分是重做日志文件(redo log file)
,是持久到磁盘的。 - 为了确保每次日志都写入重做日志文件,在每次将重做日志缓冲写入重做日志文件后,InnoDB引擎都会调用一个fsync刷盘完成持久化,刷盘时机受
innodb_flush_log_at_trx_commit
参数影响。
在redolog中有个重要的概念,LSN(Log Sequence Number),代表的是日志序列号,占用8字节,单调递增,表示事务写入重做日志的字节总量,用于意外宕机后重启时的redolog恢复。比如:某个页的LSN为800,而数据库启动时检测到重做日志中的LSN为1000,那么数据库需要进行恢复操作,将重做日志应用到该页中。
// Log sequence number 表示当前的LSN,Log flushed up to表示刷新到重做日志文件的LSN,Last checkpoint at表示刷新到磁盘的LSN。
show engine innodb status;
防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。
在事务开始之后逐步写入重做日志文件,存储引擎先将重做日志写入innodb_log_buffer
。
redo log通常是物理日志,记录的是数据页的物理修改,而不是某一行或某几行修改成怎样怎样,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
回滚日志(undo log
)-回滚日志,提供回滚操作。
在对数据库进行修改时,InnoDB存储引擎会产生一定量的undolog,用于保证数据的原子性和一致性。undolog存放在数据库内部的一个特殊段(undo segment)中,位于共享表空间内。主要有下面两个作用:
- undolog是逻辑日志,对每个操作记录了相应的
反操作
。 - undolog通过指针记录了
每行数据的不同版本
,用于实现MVCC。
保存了事务发生之前的数据的一个版本
,可以用于回滚,同时可以提供多版本并发控制下的读(MVCC),也即非锁定读。
事务开始之前,将当前是的版本生成undo log,undo 也会产生 redo 来保证undo log的可靠性
。
可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。
undolog删除时机
事务提交后并不能马上删除undolog,因为还可能有其他的事务通过undolog来记录之前的版本,用于实现MVCC,在事务提交时将undolog放入history list中,等待purge线程进行最后删除。
增删改操作:
- 插入操作产生 TRX_UNDO_INSERT_REC 类型的undolog。
- 删除操作产生 TRX_UNDO_DEL_MARK_REC 类型的undolog,标记记录的delete flag为1,并不会真实删除,等待purge线程真正执行删除操作。
- 更新操作对undolog来说,分为删除和插入两步,先记录 TRX_UNDO_DEL_MARK_REC 类型的undolog并标记删除,再插入一条新的undolog TRX_UNDO_INSERT_REC。
purge
所有操作产生的undolog按照事务提交的顺序,保存在history list中,在执行purge时,会从该链表头部开始找到第一个需要被清理的记录
,并根据事务id向后检测该undolog是否被其他事务使用,如果未使用则进行purge操作并删除,否则放弃,继续判断链表的下一个undolog。
为了提高purge效率,可以通过 innodb_purge_batch_size
设置每次purge操作需要清理的undolog数量,越大则每次回收undo页越多,可供重用的undo页就越多,减少了磁盘存储空间和分配的开销。不过该参数设置得过大,会导致每次需要purge的undo配置越多,从而导致CPU和磁盘IO过于集中使用,使性能下降。
全局动态参数 innodb_max_purge_lag 用来控制history list的长度,若长度大于该参数时,会延缓DML操作(dalay单位为毫秒),默认值为0,表示不对list大小做任限制。
group commit
为什么需要group commit
若每次事务提交时需要进行一次fsync操作,保证重做日志成功刷盘,以便当数据库发生宕机时,可以通过重做日志进行数据恢复。为了提高刷盘效率,数据库提供了group commit功能,在一次fsync时可以刷新多个事务日志写入磁盘
。
事务提交后做了哪些操作
在开启binlog后,为了保证redolog和binlog中的一致性,事务提交时采取二阶段提交,步骤如下:
- InnoDB存储引擎进行prepare操作,将日志写入redolog缓冲
- 【加锁,阻止其它事务写binlog】数据库层写入binlog日志,由sync_binlog控制刷盘策略
- 调用fsync将redolog刷入磁盘
在上述步骤中,第二步完成后,就确保了事务提交,为了保证提交顺序一致,在第二步执行之前,会加 prepare_commit_mutex
锁,从而阻止其他事务写入binlog,用于保证事务的一致性,与此同时就导致了group commit失效。
用Binary Log Group Commit解决group commit失效问题
MySQL数据库上层进行提交时,首先按顺序将redolog放入一个队列中,队列中第一个事务称为leader,其他事务称为follower,leader控制follower进行group commit,提交步骤如下:
- Flush阶段将队列中所有事务的二进制日志写入内存中
- Sync阶段将内存中的二进制日志刷新到磁盘(binlog的group commit)
- Commit阶段,leader调用InnoDB层执行redolog的group commit操作。
当有一组事务在进行commit阶段时,其他新事务可与进行flush阶段,从而提高效率,同时通过 binlog_max_flush_queue_time
控制flush阶段中等待时间,即使前一组事务完成commit,当前一组事务也不会马上进入sync阶段,而是等待该参数设置的时间,这样可以使下一次group commit的事务数量更多。该参数默认为0,且推荐设置为0,除非当前MySQL有大量的活跃连接进行写入事务,可以调高该参数,用来提高commit效率。
二进制日志(binlog
)
用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步;
用于数据库的基于时间点的还原;
事务提交的时候,一次性将事务中的sql语句(一个事物可能对应多个sql语句)按照一定的格式记录到binlog中。
MySQL通过两阶段提交过程来完成事务的一致性的,也即redo log和binlog的一致性的,两个日志都提交成功(刷入磁盘),事务才算真正的完成。
阶段1:InnoDB redo log 写盘,InnoDB 事务进入 prepare 状态。
阶段2:如果前面prepare成功,binlog 写盘,那么再继续将事务日志持久化到binlog,如果持久化成功,那么InnoDB 事务 则进入 commit 状态(实际是在redo log里面写上一个commit记录)。
MYSQL怎么保证ACID
A原子性
由undo log日志保证,它记录了需要回滚的日志信息,事务回滚时撤销已经执行成功的sql。
undolog记录反向操作,用于回滚:
对于每一个insert,undolog记录一个delete
对于每一个delete,undolog记录一个insert
对于每一个update,undolog记录一个相反的update
redolog group commit 用于提交。
C一致性
- redolog+undolog ,保证所有操作同时提交和回滚。
- 通过加锁保证同一行数据更新串行,避免提交丢失。
- 通过MVCC保证事务内读一致性。
加锁参考:https://success.blog.csdn.net/article/details/103154299
I隔离性
- 通过隔离模式实现不同级别下的隔离性。
- 通过锁和MVCC实现隔离模式下的读写并行。
四种隔离模式:https://success.blog.csdn.net/article/details/105027900
D持久性 【双1策略】
通过redolog实现持久性。sync_binlog
【MySQL 在每写 1次 二进制日志binary log时,会使用fdatasync()函数将它的写二进制日志binary log同步到磁盘中去。】和innodb_flush_log_at_trx_commit
【每次事务提交时MySQL都会把log buffer的数据写入log file,并且flush(刷到磁盘)中去】设置为1,也就是大家常说的双1策略,保证宕机数据不丢失。
宕机重启时通过对比redolog页和数据页的LSN(Log Sequence Number)进行恢复。
如果innodb_flush_log_at_trx_commit设置为0:log buffer将每秒一次地写入log file中,并且log file的flush(刷到磁盘)操作同时进行.该模式下,在事务提交的时候,不会主动触发写入磁盘的操作;
如果innodb_flush_log_at_trx_commit设置为1:每次事务提交时MySQL都会把log buffer的数据写入log file,并且flush(刷到磁盘)中去;
如果innodb_flush_log_at_trx_commit设置为2:每次事务提交时MySQL都会把log buffer的数据写入log file,但是flush(刷到磁盘)操作并不会同时进行。该模式下,MySQL会每秒执行一次 flush(刷到磁盘)操作。sync_binlog 的默认值是0,像操作系统刷其他文件的机制一样,MySQL不会同步到磁盘中去而是依赖操作系统来刷新binary log。
当sync_binlog =N (N>0) ,MySQL 在每写 N次 二进制日志binary log时,会使用fdatasync()函数将它的写二进制日志binary log同步到磁盘中去。
安全
- 当innodb_flush_log_at_trx_commit和sync_binlog 都为 1 时是最安全的,在mysqld 服务崩溃或者服务器主机crash的情况下,binary log 只有可能丢失最多一个语句或者一个事务。但是鱼与熊掌不可兼得,双11 会导致频繁的io操作,因此该模式也是最慢的一种方式。
- 当innodb_flush_log_at_trx_commit设置为0,mysqld进程的崩溃会导致上一秒钟所有事务数据的丢失。
- 当innodb_flush_log_at_trx_commit设置为2,只有在操作系统崩溃或者系统掉电的情况下,上一秒钟所有事务数据才可能丢失。
“双1设置”适合数据安全性要求非常高,而且磁盘IO写能力足够支持业务,比如订单,交易,充值,支付消费系统。双1模式下,当磁盘IO无法满足业务需求时 比如11.11 活动的压力。推荐的做法是 innodb_flush_log_at_trx_commit=2 ,sync_binlog=N (N为500 或1000) 且使用带蓄电池后备电源的缓存cache,防止系统断电异常。
锁
InnoDB锁的特性
1、在不通过索引条件查询的时候,InnoDB使用的确实是表锁!
2、由于 MySQL 的行锁是针对索引加的锁,不是针对记录加的锁,所以虽然是访问不同行 的记录,但是如果是使用相同的索引键,是会出现锁冲突的。
3、当表有多个索引的时候,不同的事务可以使用不同的索引锁定不同的行,另外,不论 是使用主键索引、唯一索引或普通索引,InnoDB 都会使用行锁来对数据加锁。
4、即便在条件中使用了索引字段,但是否使用索引来检索数据是由 MySQL 通过判断不同 执行计划的代价来决定的,如果 MySQL 认为全表扫 效率更高,比如对一些很小的表,它 就不会使用索引,这种情况下 InnoDB 将使用表锁,而不是行锁。因此,在分析锁冲突时, 别忘了检查 SQL 的执行计划(explain查看),以确认是否真正使用了索引。
MySQL 锁种类
共享锁与排他锁(S/X锁)
共享锁和共享锁可以兼容,排他锁和其它锁都不兼容。
- 共享锁(S):允许获得该锁的事务读取数据行(
读锁
)(即读读并行)。 - 排他锁(X):允许获得该锁的事务更新或删除数据行(
写锁
),同时阻止其他事务取得该数据行上的共享锁和排他锁(即写写/写读互斥)。
意向锁 (Intention Lock)
意向锁是一种表级别的锁,意味着事务在更细的粒度上进行加锁。
InnoDB 除了支持行级锁,还支持由 MySQL 服务层实现的表级锁。当这两种锁同时存在时,可能导致冲突。意向锁由 InnoDB 自动添加,不需要用户干预。
- 意向共享锁(IS):事务在给数据行加行级共享锁之前,必须先取得该表的 IS 锁。
- 意向排他锁(IX):事务在给数据行加行级排他锁之前,必须先取得该表的 IX 锁。
举个例子,事务1在表1上加了S锁后,事务2想要更改某行记录,需要添加IX锁,由于不兼容,所以需要等待S锁释放;如果事务1在表1上加了IS锁,事务2添加的IX锁与IS锁兼容,就可以操作,这就实现了更细粒度的加锁。
表级锁与行级锁
InnoDB 存储引擎同时支持行级锁(row-level locking)和表级锁(table-level locking),默认情况下采用行级锁。
锁算法
在 InnoDB 存储引擎中,SELECT 操作的不可重复读问题通过 MVCC 得到了解决,而 UPDATE、DELETE 的不可重复读问题通过 Record Lock 解决,INSERT 的不可重复读问题是通过 Next-Key Lock(Record Lock + Gap Lock)解决的。
InnoDB行级锁(Record Lock)
InnoDB 通过给索引上的索引记录加锁
的方式实现行级锁。
记录锁Record Lock
针对索引记录
(index record)的锁定。
SELECT * FROM t WHERE id = 1 FOR UPDATE
间隙锁 (gap锁)
间隙锁是可重复读级别(Repeatable read )下才会有的锁。
锁定的是索引记录之间的间隙、第一个索引之前的间隙或者最后一个索引之后的间隙。
SELECT * FROM t WHERE c1 BETWEEN 1 and 10 FOR UPDATE
Next-key 锁 (行锁+gap锁) 【REPEATABLE READ 隔离级别通过MVCC和next-key lock实现解决幻读】
行记录锁+记录前的gap锁。为了防止幻读,next-key lock阻止特定条件的新记录的插入,因为插入时要获取Insert Intention Locks,与已持有的next-key lock冲突。Insert Intention Locks,插入意向锁并非意向锁,而是一种特殊的间隙锁。
- 前面两种的组合,对记录及前面的间隙加锁。
如果索引有唯一属性,则 InnnoDB 会自动将 next-key 锁降级为记录锁。
SELECT FROM t WHERE id <1 for update; 使用的就是间隙锁;
SELECT FROM t WHERE id <=1 for update; 使用的就是 next-key 锁,因为表中存在 id = 1 的数据。
MySQL死锁的分析与解决
Deadlock found when trying to get lock
死锁是指两个或两个以上事务在执行过程中因争抢锁资源而造成的互相等待的现象。
解决死锁的方法
- 超时等待:即当两个事务互相等待时,当一个事务等待时间超过设置的阈值时,就将其回滚,另外事务继续进行。(缺点:如果回滚的事务更新了很多行,占用了较多的undo log,那么在回滚的时候花费的时间比另外一个正常执行的事务花费的时间可能还要多,就不太合适);
- wait-for graph(等待图):死锁碰撞检测,是一种较为主动的死锁检测机制,要求数据库保存锁的信息链表和事务等待链表两部分信息,通过这两个部分信息构造出一张图,在每个事务请求锁并发生等待时都会判断是否存在回路,如果在图中检测到回路,就表明有死锁产生,这时候InnoDB存储引擎会选择回滚undo量最小的事务。
解决方案
事务1在执行sql 1的时候发生死锁,说明sql 1对应的行记录的锁被事务2占有;
事务2在执行sql 2的时候发生死锁,说明sql2对应的行记录的锁被事务1占有;
产生死锁的原因是第一条SQL语句用到的索引区分度不高
,只用到了lockstatsu索引,导致5万条行记录被锁。
因此解决方案就是要用区分度更高的索引,根据第一条SQL建立唯一键引:
唯一索引和普通索引的抉择
change buffer
在执行更新操作时,如果要更新的数据页在内存中就直接更新,否则的话,在不影响数据一致性的前提下,InnoDB 会将更新操作缓存在 change buffer 中
,从而省去了从磁盘读取数据页的过程。在下次查询操作读取
到恰好需要更新的数据页时,会将 change buffer 的更新语句执行,写入数据页
。将操作应用到硬盘的过程叫 merge
. 后台线程会定期 merge 或 数据库正常关闭时,也会进行 merge 操作。
因此对于写多读少的业务change buffer 发挥的作用也就越大。
主要的差异是在更新过程中,唯一索引由于需要唯一性检查,不能利用 change buffer
。多了从磁盘到内容读取数据的过程,其中涉及随机 IO 的访问,相对来说效率就低了。
如果服务器异常掉电,会不会导致 change buffer 丢失?
并不会,因为 change buffer 中的数据已经被记录到 redo log 中,所以不会丢失。
由于 change buffer 一部分数据在磁盘,一部分在内存。对于在磁盘的数据已经 merge 所以不会丢失。
es和mysql之间是怎么做数据一致性的维护
canal会模拟成一台mysql的slave去接收mysql的master节点返回的binlog信息,然后将这些binlog数据解析成一个json字符串,再投递到mq当中。在rocketmq的接收端会做消息的监听,一旦有接收到消息就会写入到es中。
canal做日志订阅的时候可以借助一个“缓冲池”角色的帮助。这个缓冲池可以是一些分布式缓存,用于临时接收数据,当全量同步完成之后,进入一个加锁的状态,此时将缓存中的数据也一同刷入到db中,最后释放锁。由于将redis中的数据刷入到磁盘中是个非常迅速的瞬间,因此整个过程可以看作为几乎平滑无感知。
该如何全量将binlog都发送给到canal呢?
binlog的产生主要是依靠数据发生变动导致的,假设我们需要同步的表里面包含了update_time字段的话,这里只需要更新下全表的update_time字段为原先值+1 就可以产生出全表的binlog信息了。
其它
Innodb和MyISAM的区别
- InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
- InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;
- InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。
- InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
- InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;
MySQL常用的四种引擎
- MyISAM存储引擎 (不支持事务、也不支持外键,只支持表级锁。优势是访问速度快,对事务完整性没有要求,MyISAM 支持压缩表和空间数据索引。)
- InnoDB存储引擎 (具有提交、回滚和崩溃恢复能力的事务安全,并且InnoDB 支持真正的在线热备份)
- MEMORY存储引擎 (使用存在于内存中的内容来创建表,但是一旦服务关闭,表中的数据就会丢失掉。)
- MERGE存储引擎 (一组MyISAM表的组合,这些MyISAM表必须结构完全相同,merge表本身并没有数据,对merge类型的表可以进行查询,更新,删除操作,这些操作实际上是对内部的MyISAM表进行的。)
分区表
CREATE TABLE xxx PARTITION BY创建分区表,必须指定一个分区规则。
PARTITION BY LIST子句在指定列中输入的值的基础上对表进行分区。
优点:
- 与单个磁盘或文件系统分区相比。可存储更多的数据。
- 很容易删除与数据有关的分区。
- 查询得到优化。(只查询一个特定分区数据)
SUM() 和 COUNT 聚合函数的查询,可以在每个分区上同时进行。
Order by工作原理(小于sort_buffer_size)
全字段排序:Mysql会给每个查询线程分配一个小内存sort_buffer,用于排序.
Mysql为对应线程初始化sort_buffer,放入需要查询的字段。
- 从索引树拿到第一个满足条件的主键id
- 从主键id索引树拿到对应的数据,存到sort_buffer
- 从索引树拿到下一个记录的主键id
- 重复3/4,直到不满足条件
- 现在所有满足条件的数据都在sort_buffer中,根据条件进行排序
-
如果sort_buffer放不下怎么办
sort_buffer由sort_buffer_size控制,当它大于该值时,需要借助磁盘文件来排序。(临时文件)
Order by工作原理(大于sort_buffer_size) 【归并排序算法】 从主键id索引树拿到对应的数据,存到sort_buffer。当sort_buffer快存满时,对它进行排序处理,将数据存在临时文件。
- 继续重复步骤1,最后将磁盘的临时文件合并成有序的大文件。
rowid排序
只把查询SQL需要用到的排序字段和主键id放到sort_buffer【比全字段排序流程多了一次回表】
- InnoDB存储引擎,Max_length_for_sort_data参数默认1024,排序字段不会超过这个值,一般都会走全字段排序
Show processlist 查看当前线程执行情况。遇到执行时间长的直接kill -9。 SQL注入导致数据库连接过多,根本原因就是长时间锁表。
预编译不一定安全?like语句时,传入“%%%”
是的,当用like语句时,传入“%%%”会返回所有数据,需要对%进行转义。
开发中数据库使用注意事项
- 使用预编译
- 对特殊字符转义
- 捕获异常,不能直接返回异常
- 代码检测工具
- sql要有监控工具
- 数据库账号需控制权限,只分配DML相关权限
代码review,如排序字段需要手写工具过滤关键字