MySQL
Posted java_wxid
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL相关的知识,希望对你有一定的参考价值。
—
文章目录
mysql索引底层数据结构与算法
MySQL数据库在计算机存储的位置
1:MySQL的数据都是以文件的形势存储在磁盘上的。
2:磁盘是由一圈一圈的磁道组成。
3:磁头移动到不同的磁道,磁盘旋转,这样就可以读到数据。
4:磁盘存取原理:1.寻道时间(速度慢,费时) 2.旋转时间(速度较快)
5:CPU读取数据都是从内存读取,内存去磁盘中读取数据,内存读取磁盘数据大小都是以页的大小单位。一页 = 10kb
1:当磁头移动到另一个磁道读取数据就是我们常说的一次I/O操作,MySQL数据是分布在不同的磁道上的,每次读取数据都要把所有的磁道读取一遍。那我们进行I/O操作的次数就很多了,查询效率就很低。
2:索引就是把索引字段数据的地址保存起来,来帮助MySQL直接定位到哪个磁道哪个扇区,这样就减少了I/O的操作次数了,自然查询效率就提高了。
索引
定义
索引到底是什么
索引是帮助Mysql高效获取数据的一种数据结构
索引储存在哪里
和数据一样,索引以文件形式储存在硬盘上,在MyISAM储存引擎中,数据和索引文件试试分开储存的。
MyISAM文件储存示意图
在InnoDB中,数据和索引文件是合起来储存的,注意下图中没有了(index)结尾的文件。
InnoDB文件储存示意图
索引的数据结构
想想JDK8的Hashmap中,当链表长度大于一定限度时,为了能够高效的检索数据,引入了红黑树。索引的思想也是这样,通过一种数据结构高效的数据结构来加速数据检索的过程。
常见的数据结构:
1:二叉树
二叉树是n(n>=0)节点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和二棵不想交的,分别称为根节点的左子树和右子树组成。
不使用原因:一个节点只有一度,就是只有一个子节点,那读取树的一层就是一次I/O操作,性能也不好。
2:红黑树
红黑树即为平衡二叉树的一种
不使用原因:一个节点有二个子节点,那就出现一层只有二个节点的情况,这种性能也不好
3:hash
不使用原因:Hash是把索引数据进行Hash算法对应一个地址,我们会发现这个性能很好啊,直接找到但是我们想想,他能满足我们日常大部分情况么?比如通过大于或小于筛选数据,所以说也不合适,当然MySQL也提供了Hash索引,毕竟有些场合用起来还是不错的。
4: B Tree
特性:1.节点的数据储存个数 2.叶节点具有相同的深度 3.叶节点的指针为空 4.节点中的数据key从左到右递增排列
不使用原因:虽然解决了每一层的节点数的极端情况下,但是每一个节点存储了索引和数据,一层存储的数据太多也不好,毕竟内存能读取的数据大小就10kb.
5: B+Tree
特性:1.B+Tree(BTree的变种) 2.非叶子节点不存储data,只存储key,可以增大度 3.叶子节点不存储指针 4.顺序访问指针,提高区间访问性能。
使用原因:1:非叶子节点不存储data,只存储key,可以增大度。2:叶子节点不存储指针。3:顺序访问指针,提高区间访问的性能。比起红黑树,上面两种数据结构都更加矮胖。他们两之间一个很大的不同是B+树的节点上不储存value,只储存key,而叶子节点上储存了所有kv集合,并且节点之间都是有序的。这样的好处是每一次磁盘IO能够读取的节点更多,也就是树的度可以设置的更大一些,因为每次磁盘IO读取的磁盘页数是一定的,例如每次磁盘IO能够读取1页=4kb,那么省去value的情况下同样一页数据能够读取更多的key,这样就大大减少了磁盘的IO次数。此外,B+树是排序好的数据结构,数据库中><或者order by等都可以直接依赖这一特性。
B+Tree索引的性能分析
- 一般使用磁盘I/O次数评价索引结构的优劣。
- 预读:磁盘一般会顺序向后读取一定长度的数据(页的整倍数)放入内存
- 局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用
- B+Tree节点的大小设为等于一个页,每次新建节点直接申请一个页的空间,这样保证一个节点物理上也存储在一个页里,就实现了一个节点的载入只需要一次I/O
- B+Tree的度d一般会超过100个,因此h非常小(一般为3到5之间)
不同的存储引擎,有不同的索引实现
1:MyISAM索引实现(非聚集) 2.InnoDB索引实现(聚集)
MyISAM索引文件与数据文件是分离的,并且主键索引和辅助索引(二级索引)的储存方式是一样的。
主键索引
辅助索引
InnoDB索引实现(聚集)
InnoDB中索引文件和数据文件是同一个文件。并且主键索引和二级索引储存方式有所不同,如图所示,二级索引的叶子节点不储存数据,仅储存主键ID。
主键索引
辅助索引
1:数据文件本身就是索引文件
2:表数据文件本身就是按B+Tree组织的一个索引结构文件
3:聚集索引-叶节点包含了完整的数据记录
问题:
- InnoDB表中必然有主键,为什么最好一定是有序自增id?
- 为什么二级索引叶子节点储存的是主键值?
问题一
如果id是无序的,那么很有可能新插入的值会导致当前节点分裂,此时MySQL不得不为了将新记录插到合适位置而移动数据,甚至目标页面可能已经被回写到磁盘上而从缓存中清掉,此时又要从磁盘上读回来,这增加了很多开销,同时频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE来重建表并优化填充页面。反之,如果每次插入有序,那就会在当前页后面连续写入,写不下就会重新分配一个节点,内存都是连续的,这样效率自然也就最高了。
问题二
如果二级索引储存的也是数据,那么每次插入mysql都不得不更新每棵索引树,这样就加剧了新增编辑时的性能损耗,并且这样一来空间利用率也不高,产生了大量冗余数据。
最左前缀原理
使用联合索引时,索引列的定义顺序将会影响到最终查询时索引的使用情况。例如联合索引(a,b,c),mysql会从最左边的列优先匹配,如果最左边的带头大哥没有使用到,在未使用覆盖索引的情况下,就只能全表扫描。
联合底层数据结构思考,mysql会优先以联合索引第一列匹配,此后 > < between等这样的范围查询,那B+树中也就无法对下一列进行等值匹配了。
事务隔离级别
脏读
一个事务a修改或添加了一条数据,在a事务提交之前,另一个事务b读到了这条数据,并进行了操作。a如果回滚的话,脏读可能会导致b操作不存在的数据。
不可重复读
在一次事务中的两次相同条件的查询不一致,比如a事务执行select count(*) from user where name=‘alex’ 这时事务b插入了一条数据name=alex并提交这就会导致事务a第二次查询的时候多了一个计数
幻读
事务a与事务b是完全隔离的,事务a执行’select id from user得到的id为1和2.这个时候b事务在user表中添加了一条数据id=3并提交,然后事务a想添加一条id为3的数据,如果id是唯一的,那a就会发现插不进去并提示dumplicate entry 3 for key id,原因是事务a阻止事务b的插入行为。
数据库事务隔离级别
Read uncommitted 读未提交
公司发工资了,领导把20000元打到廖志伟的账号上,但是该事务并未提交,而廖志伟正好去查看账户,发现工资已经到账,是20000元整,非常高兴。可是不幸的是,领导发现发给廖志伟的工资金额不对,是16000元,于是迅速修改金额,将事务提交,最后廖志伟实际的工资只有16000元,廖志伟空欢喜一场。
出现上述情况,即我们所说的脏读,两个并发的事务,“事务A:领导给廖志伟发工资”、“事务B:廖志伟查询工资账户”,事务B读取了事务A尚未提交的数据。当隔离级别设置为Read uncommitted时,就可能出现脏读,如何避免脏读,请看下一个隔离级别。
Read committed 读提交
廖志伟拿着工资卡去消费,系统读取到卡里确实有2000元,而此时她的老婆也正好在网上转账,把廖志伟工资卡的2000元转到另一账户,并在廖志伟之前提交了事务,当廖志伟扣款时,系统检查到廖志伟的工资卡已经没有钱,扣款失败,廖志伟十分纳闷,明明卡里有钱,为何…
出现上述情况,即我们所说的不可重复读,两个并发的事务,“事务A:廖志伟消费”、“事务B:廖志伟的老婆网上转账”,事务A事先读取了数据,事务B紧接了更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。当隔离级别设置为Read committed时,避免了脏读,但是可能会造成不可重复读。大多数数据库的默认级别就是Read committed,比如Sql Server , Oracle。如何解决不可重复读这一问题,请看下一个隔离级别。
Repeatable read 重复读
当廖志伟拿着工资卡去消费时,一旦系统开始读取工资卡信息(即事务开始),廖志伟的老婆就不可能对该记录进行修改,也就是廖志伟的老婆不能在此时转账。这就避免了不可重复读。廖志伟的老婆工作在银行部门,她时常通过银行内部系统查看廖志伟的信用卡消费记录。有一天,她正在查询到廖志伟当月信用卡的总消费金额(select sum(amount) from transaction where month = 本月)为80元,而廖志伟此时正好在外面胡吃海喝后在收银台买单,消费1000元,即新增了一条1000元的消费记录(insert transaction … ),并提交了事务,随后廖志伟的老婆将廖志伟当月信用卡消费的明细打印到A4纸上,却发现消费总额为1080元,廖志伟的老婆很诧异,以为出现了幻觉,幻读就这样产生了。当隔离级别设置为Repeatable read时,可以避免不可重复读,但会出现幻读。注:MySQL的默认隔离级别就是Repeatable read。
Serializable 序列化
Serializable是最高的事务隔离级别,同时代价也花费最高,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻像读。
MySQL四大属性底层实现原理
A(原子性)
要么全部完成,要么完全不起作用。底层实现原理:undo log(当这个事务对数据库进行修改的时候,innodb 生成对应 undo log,他会记录这个 SQL 执行的相关信息,如果 SQL 执行失败发生这个回滚,innodb 根据这个 undo log 内容去做相反的工作,比如说我执行了一个 insert 操作,那么回滚的时候,就会执行一个相反的操作,就是 delete,对应 update,回滚的时候也是执行相反的 update)
C(一致性)
一旦事务完成(不管成功还是失败),业务处于一致的状态,而不会是部分完成,部分失败。事务执行前后,数据库的完整约束没有遭受破坏,事务执行前后都是合法的一个数据状态。
I(隔离性)
多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。底层实现原理:写-写操作:锁(和 java 里面的锁机制是一样的)。写-读操作:MVCC(多版本并发控制,可以通过乐观锁和悲观锁实现,只在读已提交和可重复读二个隔离级别,事务的排它锁形式修改数据,修改之前把数据放到 undolog 里面,通过回滚指针关联,成功了什么都不做,失败了,从 undolog 回滚数据。)
D(持久性)
一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,事务的结果被写到持久化存储器中。底层实现原理:redo log(mysql 的数据是存放在这个磁盘上的,但是每次去读数据都需要通过这个磁盘 io,效率就很低,使用 innodb 提供了一个缓存 buffer,这个 buffer 中包含了磁盘部分数据页的一个映射,作为访问数据库的一个缓冲,当从这个数据库读取一个数据,就会先从这个 buffer 中获取,如果 buffer 中没有,就从这个磁盘中获取,读取完再放到这个 buffer 缓冲中,当数据库写入数据的时候,也会首先向这个 buffer 中写入数据,定期将 buffer 中的数据刷新到磁盘中,进行持久化的一个操作。如果 buffer 中的数据还没来得及同步到这个磁盘上,这个时候 MySQL 宕机了,buffer 里面的数据就会丢失,造成数据丢失的情况,持久性就无法保证了。使用 redolog 解决这个问题,当数据库的数据要进行新增或者是修改的时候,除了修改这个 buffer 中的数据,还会把这次的操作写入到这个 redolog 中,如果 msyql 宕机了,就可以通过 redolog 去恢复数据,redolog 是预写式日志(会先将所有的修改写入到日志里面,然后再更新到 buffer 里面,保证了这个数据不会丢失,保证了数据的持久性))
说明:undolog有多个版本,并且存放的是与上一个版本相反的操作。redolog 属于记录修改的操作,主要为了提交或者恢复数据使用!
MVCC多版本并发控制机制/Innodb引擎SQL执行的BufferPool缓存机制
Mysql在可重复读隔离级别下如何保证事务较高的隔离性,我们上节课给大家演示过,同样的sql查询语句在一个事务里多次执行查询结果相同,就算其它事务对数据有修改也不会影响当前事务sql语句的查询结果。
这个隔离性就是靠MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。
Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。
undo日志版本链与read view机制详解
undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链(见下图)
在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
版本链比对规则:
- 如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
- 如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
- 如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况
a. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的);
b. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。
注意:begin/start transaction命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。
总结:
MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。
Innodb引擎SQL执行的BufferPool缓存机制
为什么Mysql不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL了?
因为来一个请求就直接对磁盘文件进行随机读写,然后更新磁盘文件里的数据性能可能相当差。
因为磁盘随机读写的性能是非常差的,所以直接更新磁盘文件是不能让数据库抗住很高并发的。
Mysql这套机制看起来复杂,但它可以保证每个更新请求都是更新内存BufferPool,然后顺序写日志文件,同时还能保证各种异常情况下的数据一致性。更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是非常高的,要远高于随机读写磁盘文件。正是通过这套机制,才能让我们的MySQL数据库在较高配置的机器上每秒可以抗下几干甚至上万的读写请求。
SQL的执行流程
MySQL的内部组件结构
MySQL 可以分为 Server 层和存储引擎层两部分。
Server层
主要包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
连接器
MySQL是开源的,他有非常多种类的客户端:navicat,mysql front,jdbc,SQLyog等非常丰富的客户端,这些客户端要向mysql发起通信都必须先跟Server端建立通信连接,而建立连接的工作就是有连接器完成的。
第一步,先连接到这个数据库上,这时候接待你的就是连接器。连接器负责跟客户端建立连接、获取权限、维持和管理连接。连接命令一般是这么写的:[root@192 ~]# mysql -h host[数据库地址] -u root[用户] -p root[密码] -P 3306
。连接命令中的 mysql 是客户端工具,用来跟服务端建立连接。在完成经典的 TCP 握手后,连接器就要开始认证你的身份,这个时候用的就是你输入的用户名和密码。
- 如果用户名或密码不对,你就会收到一个"Access denied for user"的错误,然后客户端程序结束执行。
- 如果用户名密码认证通过,连接器会到权限表里面查出你拥有的权限。之后,这个连接里面的权限判断逻辑,都将依赖于此时读到的权限。
这就意味着,一个用户成功建立连接后,即使你用管理员账号对这个用户的权限做了修改,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。用户的权限表在系统表空间的mysql的user表中。
修改user密码
mysql> CREATE USER 'username'@'host' IDENTIFIED BY 'password'; //创建新用户
mysql> grant all privileges on *.* to 'username'@'%'; //赋权限,%表示所有(host)
mysql> flush privileges //刷新数据库
mysql> update user set password=password(”123456″) where user=’root’;(设置用户名密码)
mysql> show grants for root@"%"; 查看当前用户的权限
连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 show processlist 命令中看到它。文本中这个图是 show processlist 的结果,其中的 Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接,关闭连接 kill <id>
。
客户端如果长时间不发送command到Server端,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。
查看wait_timeout
mysql> show global variables like "wait_timeout";
mysql>set global wait_timeout=28800; 设置全局服务器关闭非交互连接之前等待活动的秒数
如果在连接被断开之后,客户端再次发送请求的话,就会收到一个错误提醒: Lost connection to MySQL server during query。这时候如果你要继续,就需要重连,然后再执行请求了。数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接。短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个。开发当中我们大多数时候用的都是长连接,把连接放在Pool内进行管理,但是长连接有些时候会导致MySQL 占用内存涨得特别快,这是因为 MySQL在执行过程中临时使用的内存是管理在连接对象里面的。这些资源会在连接断开的时候才释放。所以如果长连接累积下来,可能导致内存占用太大,被系统强行杀掉(OOM),从现象看就是MySQL 异常重启了。
怎么解决这类问题呢?
- 定期断开长连接。使用一段时间,或者程序里面判断执行过一个占用内存的大查询后,断开连接,之后要查询再重连。
- 如果你用的是 MySQL 5.7 或更新版本,可以在每次执行一个比较大的操作后,通过执行 mysql_reset_connection 来重新初始化连接资源。这个过程不需要重连和重新做权限验证,但是会将连接恢复到刚刚创建完时的状态。
查询缓存
常用的一些操作
mysql>show databases; 显示所有数据库
mysql>use dbname; 打开数据库:
mysql>show tables; 显示数据库mysql中所有的表;
mysql>describe user; 显示表mysql数据库中user表的列信息;
连接建立完成后,你就可以执行 select 语句了。
执行逻辑就会来到第二步:查询缓存。
MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。如果你的查询能够直接在这个缓存中找到 key,那么这个value就会被直接返回给客户端。如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。大多数情况查询缓存就是个鸡肋,为什么呢?因为查询缓存往往弊大于利。查询缓存的失效非常频繁,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能你费劲地把结果存起来,还没使用呢,就被一个更新全清空了。对于更新压力大的数据库来说,查询缓存的命中率会非常低。
一般建议大家在静态表里使用查询缓存,什么叫静态表呢?就是一般我们极少更新的表。比如,一个系统配置表、字典表,那这张表上的查询才适合使用查询缓存。好在 MySQL 也提供了这种“按需使用”的方式。你可以将my.cnf参数 query_cache_type 设置成 DEMAND。
my.cnf
#query_cache_type有3个值 0代表关闭查询缓存OFF,1代表开启ON,2(DEMAND)代表当sql语句中有SQL_CACHE关键词时才缓存
query_cache_type=2
这样对于默认的 SQL 语句都不使用查询缓存。而对于你确定要使用查询缓存的语句,可以用 SQL_CACHE 显式指定,像下面这个语句一样:
mysql> select SQL_CACHE * from test where ID=5;
查看当前mysql实例是否开启缓存机制
mysql> show global variables like "%query_cache_type%";
监控查询缓存的命中率:
mysql> show status like'%Qcache%'; //查看运行的缓存信息
- Qcache_free_blocks:表示查询缓存中目前还有多少剩余的blocks,如果该值显示较大,则说明查询缓存中的内存碎片过多了,可能在一定的时间进行整理。
- Qcache_free_memory:查询缓存的内存大小,通过这个参数可以很清晰的知道当前系统的查询内存是否够用,是多了,还是不够用,DBA可以根据实际情况做出调整。
- Qcache_hits:表示有多少次命中缓存。我们主要可以通过该值来验证我们的查询缓存的效果。数字越大,缓存效果越理想。
- Qcache_inserts:表示多少次未命中然后插入,意思是新来的SQL请求在缓存中未找到,不得不执行查询处理,执行查询处理后把结果insert到查询缓存中。这样的情况的次数,次数越多,表示查询缓存应用到的比较少,效果也就不理想。当然系统刚启动后,查询缓存是空的,这很正常。
- Qcache_lowmem_prunes:该参数记录有多少条查询因为内存不足而被移除出查询缓存。通过这个值,用户可以适当的调整缓存大小。
- Qcache_not_cached: 表示因为query_cache_type的设置而没有被缓存的查询数量。
- Qcache_queries_in_cache:当前缓存中缓存的查询数量。
- Qcache_total_blocks:当前缓存的block数量。
mysql8.0已经移除了查询缓存功能
分析器
如果没有命中查询缓存,就要开始真正执行语句了。首先,MySQL 需要知道你要做什么,因此需要对 SQL 语句做解析。
分析器先会做“词法分析”。你输入的是由多个字符串和空格组成的一条 SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。MySQL 从你输入的"select"这个关键字识别出来,这是一个查询语句。它也要把字符串“T”识别成“表名 T”,把字符串“ID”识别成“列 ID”。做完了这些识别以后,就要做“语法分析”。根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。如果你的语句不对,就会收到“You have an error in your SQL syntax”的错误提醒,比如下面这个语句 from 写成了 “rom”。
mysql> select * fro test where id=1;
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'fro test where id=1' at line 1
词法分析器原理
词法分析器分成6个主要步骤完成对sql语句的分析
1、词法分析
2、语法分析
3、语义分析
4、构造执行树
5、生成执行计划
6、计划的执行
SQL语句的分析分为词法分析与语法分析,mysql的词法分析由MySQLLex[MySQL自己实现的]完成,语法分析由Bison生成。关于语法树大家如果想要深入研究可以参考这篇wiki文章:https://en.wikipedia.org/wiki/LR_parser。那么除了Bison外,Java当中也有开源的词法结构分析工具例如Antlr4,ANTLR从语法生成一个解析器,可以构建和遍历解析树,可以在IDEA工具当中安装插件:antlr v4 grammar plugin。
经过bison语法分析之后,会生成一个这样的语法树
优化器
经过了分析器,MySQL 就知道你要做什么了。在开始执行之前,还要先经过优化器的处理。
优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。比如你执行下面这样的语句,这个语句是执行两个表的 join
mysql> select * from test1 join test2 using(ID) where test1.name=yangguo and test2.name=xiaolongnv;
既可以先从表 test1 里面取出 name=yangguo的记录的 ID 值,再根据 ID 值关联到表 test2,再判断 test2 里面 name的值是否等于 yangguo。
也可以先从表 test2 里面取出 name=xiaolongnv 的记录的 ID 值,再根据 ID 值关联到 test1,再判断 test1 里面 name 的值是否等于 yangguo。
这两种执行方法的逻辑结果是一样的,但是执行的效率会有不同,而优化器的作用就是决定选择使用哪一个方案。优化器阶段完成后,这个语句的执行方案就确定下来了,然后进入执行器阶段。如果你还有一些疑问,比如优化器是怎么选择索引的,有没有可能选择错等等。
执行器
开始执行的时候,要先判断一下你对这个表 T 有没有执行查询的权限,如果没有,就会返回没有权限的错误,如下所示 (在工程实现上,如果命中查询缓存,会在查询缓存返回结果的时候,做权限验证。查询也会在优化器之前调用 precheck 验证权限)。
mysql> select * from test where id=1;
如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。
比如我们这个例子中的表 test 中,ID 字段没有索引,那么执行器的执行流程是这样的:
- 调用 InnoDB 引擎接口取这个表的第一行,判断 ID 值是不是 10,如果不是则跳过,如果是则将这行存在结果集中。
- 调用引擎接口取“下一行”,重复相同的判断逻辑,直到取到这个表的最后一行。
- 执行器将上述遍历过程中所有满足条件的行组成的记录集作为结果集返回给客户端。
至此,这个语句就执行完成了。对于有索引的表,执行的逻辑也差不多。第一次调用的是“取满足条件的第一行”这个接口,之后循环取“满足条件的下一行”这个接口,这些接口都是引擎中已经定义好的。你会在数据库的慢查询日志中看到一个
rows_examined的字段,表示这个语句执行过程中扫描了多少行。这个值就是在执行器每次调用引擎获取数据行的时候累加的。在有些场景下,执行器调用一次,在引擎内部则扫描了多行,因此引擎扫描行数跟rows_examined 并不是完全相同的。
bin-log归档
删库是不需要跑路的,因为我们的SQL执行时,会将sql语句的执行逻辑记录在我们的bin-log当中,什么是bin-log呢?
binlog是Server层实现的二进制日志,他会记录我们的cud操作。Binlog有以下几个特点
- Binlog在MySQL的Server层实现(引擎共用)
- Binlog为逻辑日志,记录的是一条语句的原始逻辑
- Binlog不限大小,追加写入,不会覆盖以前的日志
如果,我们误删了数据库,可以使用binlog进行归档!要使用binlog归档,首先我们得记录binlog,因此需要先开启MySQL的binlog功能。
配置my.cnf
配置开启binloglog-bin=/usr/local/mysql/data/binlog/mysql-bin注意5.7以及更高版本需要配置本项:server-id=123454(自定义,保证唯一性);#binlog格式,有3种statement,row,mixedbinlog-format=ROW#表示每1次执行写入就与硬盘同步,会影响性能,为0时表示,事务提交时mysql不做刷盘操作,由系统决定sync-binlog=1
binlog命令
mysql> show variables like '%log_bin%'; 查看bin-log是否开启mysql> flush logs; 会多一个最新的bin-log日志mysql> show master status; 查看最后一个bin-log日志的相关信息mysql> reset master; 清空所有的bin-log日志
查看binlog内容
mysql> /usr/local/mysql/bin/mysqlbinlog --no-defaults /usr/local/mysql/data/binlog/mysql-bin.000001 查看binlog内容
binlog里的内容不具备可读性,所以需要我们自己去判断恢复的逻辑点位,怎么观察呢?看重点信息,比如begin,commit这种关键词信息,只要在binlog当中看到了,你就可以理解为begin-commit之间的信息是一个完整的事务逻辑,然后再根据位置position判断恢复即可。
数据归档操作
从bin-log恢复数据恢复全部数据/usr/local/mysql/bin/mysqlbinlog --no-defaults /usr/local/mysql/data/binlog/mysql-bin.000001 |mysql -uroot -p tuling(数据库名)恢复指定位置数据/usr/local/mysql/bin/mysqlbinlog --no-defaults --start-position="408" --stop-position="731" /usr/local/mysql/data/binlog/mysql-bin.000001 |mysql -uroot -p tuling(数据库)恢复指定时间段数据/usr/local/mysql/bin/mysqlbinlog --no-defaults /usr/local/mysql/data/binlog/mysql-bin.000001 --stop-date= "2018-03-02 12:00:00" --start-date= "2019-03-02 11:55:00"|mysql -uroot -p test(数据库)
归档测试准
1、定义一个存储过程,写入数据
drop procedure if exists tproc;delimiter $$create procedure tproc(i int)begin declare s int default 1; declare c char(50) default repeat('a',50); while s<=i do start transaction; insert into test values(null,c); commit; set s=s+1; end while;end$$delimiter ;
2、删除数据
mysql> truncate test;
3、利用binlog归档
mysql> /usr/local/mysql/bin/mysqlbinlog --no-defaults /usr/local/mysql/data/binlog/mysql-bin.000001 |mysql -uroot -p tuling(数据库名)
Store层
存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。也就是说如果我们在create table时不指定表的存储引擎类型,默认会给你设置存储引擎为InnoDB。
演示表的DDL:
CREATE TABLE `test` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;
悲观锁
每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。适用于写为居多的场景下。比如行锁,表锁等,读锁,写锁,syncronized实现的锁等。sql中实现悲观锁,使用for update对数据加锁,例如:select num from goods where id = 1 for update;
乐观锁
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,在表中增加一个版本(version)或时间戳(timestamp)来实现。适用于读为居多的场景下。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
工作流程
获取当前数据版本
更新操作版本号+1
提交更新时,获取版本号
比较提交时的版本号与第一次获取的版本号,如果一致,那么认为资源是最新的,可以更新
否则回滚或者抛出异常
案例:
事务一开启,男柜员先执行读操作,取出金额和版本号,执行写操作,此时金额改为 120,版本号为1,事务还没有提交
事务二开启,女柜员先执行读操作,取出金额和版本号,执行写操作,此时金额改为 50,版本号变为 1,事务未提交
现在提交事务一,金额改为 120,版本变为1,提交事务。理想情况下应该变为 金额 = 50,版本号 = 2,但是实际上事务二 的更新是建立在金额为 100 和 版本号为 0 的基础上的,所以事务二不会提交成功,应该重新读取金额和版本号,再次进行写操作。这样,就避免了女柜员 用基于 version=0 的旧数据修改的结果覆盖男操作员操作结果的可能。
乐观锁和悲观锁的使用:(高性能、高可用、高并发)来说,悲观锁的实现用的越来越少了,但是一般多读的情况下还是需要使用悲观锁的,因为虽然加锁的性能比较低,但是也阻止了像乐观锁一样,遇到写不一致的情况下一直重试的时间。使用悲观锁锁住某个数据后,再遇到其他需要修改数据的操作,那么此操作就无法完成金额的修改,对产品来说是灾难性的一刻,使用乐观锁的版本号机制能够解决这个问题。
行锁和表锁
MySQL常用引擎有MyISAM和InnoDB,而InnoDB是mysql默认的引擎。MyISAM不支持行锁,而InnoDB支持行锁和表锁。
如何加锁?MyISAM在执行查询语句(SELECT)前,会自动给涉及的所有表加读锁,在执行更新操作(UPDATE、DELETE、INSERT等)前,会自动给涉及的表加写锁,这个过程并不需要用户干预,因此用户一般不需要直接用LOCK TABLE命令给MyISAM表显式加锁。
显式加锁:
上共享锁(读锁)的写法:lock in share mode,例如:select math from zje where math>60 lock in share mode;
上排它锁(写锁)的写法:for update,例如:select math from zje where math >60 for update;
表级锁
表锁:不会出现死锁,发生锁冲突几率高,并发低。
MySQL的表级锁有两种模式:
表共享读锁
表独占写锁
读锁会阻塞写,写锁会阻塞读和写
对MyISAM表的读操作,不会阻塞其它进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
对MyISAM表的写操作,会阻塞其它进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作。
MyISAM不适合做写为主表的引擎,因为写锁后,其它线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞
行锁:会出现死锁,发生锁冲突几率低,并发高。
行锁
在MySQL的InnoDB引擎支持行锁,与Oracle不同,MySQL的行锁是通过索引加载的,也就是说,行锁是加在索引响应的行上的,要是对应的SQL语句没有走索引,则会全表扫描,行锁则无法实现,取而代之的是表锁,此时其它事务无法对当前表进行更新或插入操作。
for update:如果在一条select语句后加上for update,则查询到的数据会被加上一条排它锁,其它事务可以读取,但不能进行更新和插入操作。
行锁的实现需要注意:
行锁必须有索引才能实现,否则会自动锁全表,那么就不是行锁了。
两个事务不能锁同一个索引。
insert,delete,update在事务中都会自动默认加上排它锁。
行锁场景:
A用户消费,service层先查询该用户的账户余额,若余额足够,则进行后续的扣款操作;这种情况查询的时候应该对该记录进行加锁。否则,B用户在A用户查询后消费前先一步将A用户账号上的钱转走,而此时A用户已经进行了用户余额是否足够的判断,则可能会出现余额已经不足但却扣款成功的情况。为了避免此情况,需要在A用户操作该记录的时候进行for update加锁。
死锁
产生死锁的四个必要条件
互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
不可剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能由获得该资源的进程自己来释放(只能是主动释放)。
请求与保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。即存在一个处于等待状态的进程集合Pl, P2, …, pn,其中Pi等 待的资源被P(i+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有
处理死锁的方法:
预防死锁:通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或几个条件,来防止死锁的发生。
避免死锁:在资源的动态分配过程中,用某种方法去防止系统进入不安全状态,从而避免死锁的发生。
检测死锁:允许系统在运行过程中发生死锁,但可设置检测机构及时检测死锁的发生,并采取适当措施加以清除。
解除死锁:当检测出死锁后,便采取适当措施将进程从死锁状态中解脱出来。
避免死锁的技术:
加锁顺序(线程按照一定的顺序加锁)
加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)
死锁检测 (首先为每一个进程和每一个资源指定一个唯一的号码;而后创建资源分配表和进程等待表)
死锁检测工具:
Jstack命令:用于打印出给定的java进程ID或core file或远程调试服务的Java堆栈信息,生成java虚拟机当前时刻的线程快照。生成线程快照的主要目的是定位线程出现长时间停顿的缘由,如线程间死锁、死循环、请求外部资源致使的长时间等待等。 线程出现停顿的时候经过jstack来查看各个线程的调用堆栈,就能够知道没有响应的线程到底在后台作什么事情,或者等待什么资源。
JConsole工具:用于链接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。
什么是死锁?锁等待?如何优化这类问题?通过数据库哪些表可以监控?
死锁是指两个或多个事务在同一资源上互相占用,并请求加锁时,而导致的恶性循环现象。当多个事务以不同顺序试图加锁同一资源时,就会产生死锁。当多个进程访问同一数据库时,其中每个进程拥有的锁都是其他进程所需的,由此造成每个进程都无法继续下去。InnoDB的并发写操作会触发死锁,InnoDB也提供了死锁检测机制,可以通过设置innodb_deadlock_detect参数可以打开或关闭死锁检测。打开死锁检测数据库发生死锁时自动回滚(默认选项)。关闭死锁检测,发生死锁的时候,用锁超时来处理,通过设置锁超时参数innodb_lock_wait_timeout可以在超时发生时回滚被阻塞的事务
锁等待:mysql数据库中,不同session在更新同行数据中,会出现锁等待
重要的三张锁的监控表innodb_trx,innodb_locks,innodb_lock_waits
如何优化锁:
1、尽可能让所有的数据检索都通过索引来完成,从而避免Innodb因为无法通过索引键加锁而升级为表级锁定
2、合理设计索引。不经常使用的列最好不加锁
3、尽可能减少基于范围的数据检索过滤条件
传播行为
Spring支持7中事务传播行为
一个场景:假设外层方法里面包含二个新增用户和新增角色的方法,二个方法后面还会抛一个异常。
propagation_required(需要传播)
当前没有事务则新建事务,有则加入当前事务。
外围方法未开启事务,插入用户表和用户角色表的方法在自己的事务中独立运行,外围方法异常不影响内部插入,所以两条记录都新增成功。 外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚,所以两个记录都插入失败。
propagation_supports(支持传播)
支持当前事务,如果当前没有事务则以非事务方式执行
外围方法未开启事务,插入用户表和用户角色表的方法以非事务的方式独立运行,外围方法异常不影响内部插入,所以两条记录都新增成功。 外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚,所以两个记录都插入失败。
propagation_mandatory(强制传播)
使用当前事务,如果没有则抛出异常
外围方法开启事务,内部方法加入外围方法事务,外围方法回滚,内部方法也要回滚,所以两个记录都插入失败。 外围方法没有开启事务,两张表数据都为空,在调用用户新增方法时候已经报错了,所以两个表都没有新增数据。
propagation_nested(嵌套传播)
如果当前存在事务,则在嵌套事务内执行,如果当前没有事务,则执行需要传播行为。
外围方法开启事务抛出异常,父事务最后回滚,二个方法也进行回滚的,两个表都没有新增数据。 外层方法未开启事务,插入用户表和用户角色表的方法在自己的事务中独立运行,外围方法异常不影响内部插入,所以两条记录都新增成功。
propagation_never(绝不传播)
以非事务的方式执行,如果当前有事务则抛出异常。
外围方法开启事务则抛出异常,两个表都没有新增数据 外层方法未开启事务会以非事务的方式运行,两个表新增成功;
propagation_requires_new(传播需要新的)
新建事务,如果当前有事务则把当前事务挂起,创建新的事务。
无论当前存不存在事务,都创建新事务,所以两个数据新增成功。
propagation_not_supported(不支持传播)
以非事务的方式执行,如果当前有事务则把当前事务挂起。
外围方法未开启事务,插入用户表和用户角色表的方法在自己的事务中独立运行,外围方法异常不影响内部插入,所以两条记录都新增成功。 外围方法开启事务,两个数据新增成功。
mysql(设置/更改mysql密码,连接MySQL,MySQL常用命令,MySQL两种引擎区别)