注:本篇是《高性能MySQL》第三版的读书笔记。基于MySQL5.5
MySQL的架构与历史
如下为MySQL的逻辑架构图
最上层是正常的连接处理、授权认证、安全等。
中间是核心的服务。查询解析、分析、优化、缓存预计所有内部函数,所有跨越存储引擎的功能都在这一层实现:存储过程、触发器、视图。
第三层包含了存储引擎,存储引擎负责数据的存储和提取。
MySQL服务器通过API与存储引擎通信,来做到屏蔽存储引擎之间的差异。
连接管理与安全
每个客户端打进来都会在服务器中拥有一个线程池的线程,这个连接的查询只会在这个单独的线程中进行。线程在CPU和核心中轮流获取资源,服务器维护了一个连接池(线程池),保证不会为每个连接都创建一个新的线程。
当客户端连接到MySQL服务器时,首先进行认证,并查看是否使用了SSL的方式(用X.509证书认证)、认证过了查询客户端对某个查询是否有相应的权限。 没有权限需要对这个登录的用户授权。grant
mysql -uroot -p123456 -h127.0.0.1
优化与执行
Mysql会解析查询,并创建内部数据结构(解析树),然后对其进行各种优化,包括重写查询,决定表的读取顺序,选择索引等。
并发控制
MySQL在两个层面进行并发控制,服务器层和存储引擎层。
想到并发,第一想的肯定是锁,那么锁,首先想到的肯定是类似Java的synchronized 一个一个来。但是这是数据库这么玩就凉了。所以读写锁也就成为一个好的解决办法了。
读写锁分为读锁和写锁。即共享锁和排他锁。共享锁就是多个用户可以访问同一个资源。排他锁是只要这行数据拿到排它锁,其他对这行数据的读写操作都是被阻塞的、这样确保在给定时间,只有一个用户能写入,并且其他用户读也不会出错。
颗粒度是提高并发的一种选择。
表锁:基本的锁,类似上面的读写锁加在表上,读互相不影响,写就把表锁住了。谁也读不了。
行锁:最大程度支持并发,类似上面读写锁加在一行数据。写这行数据会导致其他人读不了这行数据,别的能读。锁开销大。
事务
事务就是一组SQL语句,如果数据库引擎能成功都执行就成功,有执行失败的就全部都失败。
关系型数据库事务的ACID
example:
查账号是否有200块 ----- 当前账号减200 ---- 儿子账号多200
select balance from checking where uid = 1000
update checking set balance = balance - 200 where uid = 1000
update savings set balance = balance +200 where uid = 1000
1.原子性:一个事务要么都执行,要么都失败。
2.一致性:数据库总是从一个一致性状态转换到另外一个一致性状态。这两个一致性可以理解为确定的、中间出问题就不行了
3.隔离性:多个事务之间,如果第一个事务得到的balance是 500 此时另一个事务把balance改成了400并持久化到数据库了。那么再执行 balance - 200 set 的结果就是300 了。这不合适、应该本次事务失败,在重新获取balance = 400 然后再减。
4.持久性:一旦事务提交,其所做的修改就被持久化到数据库中了。
四种隔离级别
1.读未提交:事务中的修改即使没提交,其他事务也是可以看到的。其他事务可以读取没有被提交的数据,也叫脏读。
2.提交读:事务开始时只能看见已经提交的事务所做的修改。如果在这个事务进行时,其他事务提交他是不知道的。如果事务回滚再次读到的数据就不一样了也叫不可重复读。
3.可重复读:当一个事务在读取某个范围内的记录时,其他事务往这个范围内又插入了数据,之前的事务再读,会产生幻行,也叫做幻读。(MySQL默认隔离级别)行级锁
4.可串行化:强制事务串行执行,会在读取每一行数据的时候加锁。表级锁
事务的隔离级别可以通过 set session transation isolation level read committed; 来改。
死锁:多个事务在同一个资源上互相占用。
A
transation
update price_table set price = price -10 where uid = 10
update price_table set price = price -10 where uid = 11
commit
B
transation
update price_table set a= a-10 where uid = 11
update price_table set a= a-10 where uid = 10
commit
当A事务执行第一个update时B事务也在执行第一个update AB对对应的行数据上了锁,那么AB都在等待对方释放锁才能继续修改第二条语句,此时就是死锁了。
innodb目前将持有最少行级排它锁的事务进行回滚。有些死锁也是由于存储引擎的实现方式导致的。
事务日志
事务日志可以提高事务的效率。事务日志存储操作记录,比如insert一个数据,存储引擎只需要修改数据的内存拷贝,并将操作存到事务日志中,而不是直接将数据持久化到磁盘。跟elasticsearch的translog一个套路。事务日志是顺序写入的,不会导致磁盘的随机IO。即使我们机器宕掉了,再启动也会根据translog恢复数据。
对于Mysql常见的存储引擎,innodb和myisam中,innodb是支持事务显示创建commit和rollback的。而myisam不存在commit和rollback的操作。事务都是自动提交的。 对于一些alter对表的操作,会commit之前SQL并开始使用新的事务操作。
innodb会根据隔离级别在需要的时候自动加锁。事务是属于存储引擎层的。不属于服务器层。不同存储引擎的事务不一样。
多版本并发控制(MVCC)
MySQL大多数事务型存储引擎都不是简单的行级锁,基于对于并发场景性能的提升,一般都实现了多版本并发控制。
MVCC可以理解为行级锁的变种,性能更高了。典型的有乐观并发控制和悲观并发控制。
InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的。这两个列,一个保存了行的创建版本号,一个保存了过期系统版本号。每开启一个事务,系统版本号会递增,事务开始时刻的系统版本号会作为事务的版本号,用来和查询的记录对比。
下面是可重复读隔离级别的MVCC操作。
select 满足1.版本早于或等于当前事务版本号的行。2.行的删除版本要么未定义要么大于当前事务版本号。
insert 插入的行用当前系统版本号作为行版本号,删除版本号为空。
delete 删除的行用当前系统版本号作为删除版本号。
update innodb的实现是插入了一条新记录,用当前版本号作为行版本号,当前版本号也作为之前记录的删除版本号。
可以看到跟elasticsearch又是一个玩法。所以更新是比想像的更麻烦的。
MVCC只在可重复读和读已提交两个隔离级别下工作。
Mysql将每个数据库报错为数据目录下的一个子目录。创建表时,MySQL会在数据库子目录下创建一个和表同名的.frm文件保存表的定义。不同存储引擎保存数据和索引的方式是不同的。
可以使用 show table status like 'user' 来查询某个表的相关信息。
InnoDB存储引擎
InnoDB是基于聚簇索引建立的,聚簇索引对主键查询有很高的性能,非主键索引中包含主键列,所以主键太大会导致索引文件特别大,因此主键应该尽可能少。有行级锁,5.6版本之前不支持全文索引。之后支持了(不理想)。
MyISAM存储引擎
不支持事务和行级锁。崩溃后无法恢复。加锁的维度是整张表。myisam存储引擎会对表进行压缩。如果发现所有查询属于Locked状态,就很有可能是表锁导致的。
InnoDB有事务,备份,崩溃恢复等特性。使用MySQL基本就选它没错了。
MyISAM的压缩使得数据传输比较快。但是一些缺点导致InnoDB的优先于它。
不建议中途修改数据的存储引擎,可能会导致一些意想不到的问题。后文会详细描述存储引擎。