原子性(Atomicity): 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
一致性(Consistency): 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
隔离性(Isolation): 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
持久性(Durability): 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
脏读
不可重复读
幻读
具体可以参考我的另外一篇博客
Read Uncommitted(读未提交)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果,会产生脏读的现象。
Read Committed(读已提交)
只允许事务读取已经被其他事务提交的变更,虽然避免了脏读,但是会产生不可重复读。
Repeatable Read(可重读),这是mysql的默认隔离级别
确保事务可以多次从一个字段中读取相同的值,在事务期间,不允许其他事务对这个字段进行修改。可以避免脏读和不可重复读,但是会有幻读问题。
Serializable(可串行化)
确保事务可以从一个表中读取相同的行,在事务期间,禁止其他事务对对该表执行删除,修改,添加操作。可以避免所有问题,但是性能底下。
使用指令show engines;
,下图是我本地mysql的情况
关于上图几个参数的解释
Engine参数指存储引擎名称;
Support参数说明MySQL是否支持该类引擎,YES表示支持;
Comment参数指对该引擎的评论;
Transactions 参数表示是否支持事务处理,YES表示支持;
XA参数表示是否分布式交易处理XA规范,YES表示支持;
Savepoints参数表示是否支持保存点,以便事务回滚到保存点,YES表示支持
一般常用的就两种,InnoDB和MyISAM
MyISAM :默认表类型,它是基于传统的ISAM类型,ISAM是Indexed Sequential Access Method (有索引的顺序访问方法) 的缩写,它是存储记录和文件的标准方法。不是事务安全的,而且不支持外键,如果执行大量的select,insert MyISAM比较适合。
InnoDB :支持事务安全的引擎,支持外键、行锁、事务是他的最大特点。如果有大量的update和insert,建议使用InnoDB,特别是针对多个并发和QPS较高的情况。注: 在MySQL 5.5之前的版本中,默认的搜索引擎是MyISAM,从MySQL 5.5之后的版本中,默认的搜索引擎变更为InnoDB。
MyISAM和InnoDB的区别:
InnoDB:frm是表定义文件,ibd是数据文件,MyISAM:frm是表定义文件,MYD是数据文件,MYI是索引文件。
大神博客
InnoDB采用的是分页模式,将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16KB。也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式
。现在基本上存在4中行格式,分别是Compact
、Redundant
、Dynamic
和Compressed
,就拿我现在使用的数据库为例,使用Dynamic
作为行格式。、
我们可以看到,我们存进去的每一条记录的信息并不是那么简单,除了存进去的真实数据之外,mysql还会往记录中添加一些额外的信息。
针对VARCHAR(M)
这类的可变数据类型,我们必须知道所占的长度,不然在记录的真实数据我们怎么知道哪些数据是属于自己的呢。
注意的是,这里只存储变长且为非null的字段
下面是大神的例子,创建一个表,并添加数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e3cit4B2-1584260905633)(https://i.loli.net/2020/03/15/BKRDW9kHvSLVUts.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-izgPzcba-1584260905639)(https://i.loli.net/2020/03/15/KeZH5C6YyvislDu.png)]
那么对可变长数据分析如下
列名 | 存储内容 | 内容长度(十进制表示) | 内容长度(十六进制表示) |
---|---|---|---|
c1 |
'aaaa' |
4 |
0x04 |
c2 |
'bbb' |
3 |
0x03 |
c4 |
'd' |
1 |
0x01 |
最后存储的就是内容长度,因为要求必须逆序放置,所以040301
就要变成010304
另外原文中还有关于是使用1个字节还有2个字节比埃是内容长度的篇幅,可以区阅读。
为了优化空间,所以设立NULL值列表以表示一条记录中哪些字段是null。
注意的是,这里只统计所有列的值允许为NULL的情况
还是刚才表的例子,有c1,c3,c4的列是允许为null的,前面添0是因为长度要是以字节为单位,而且还是和前面一样需要逆序存储
存储一些记录数据需要的内容,更多细节可以查看原文
除了我们插入的列数据以外,mysql还会插入其他的内容,用于事务的处理
列名 | 是否必须 | 占用空间 | 描述 |
---|---|---|---|
row_id |
否 | 6 字节 |
行ID,唯一标识一条记录, 只有当不设置主键时才会创立 |
transaction_id |
是 | 6 字节 |
事务ID |
roll_pointer |
是 | 7 字节 |
回滚指针 |
完整的数据
还有一点要注意的是非可变长度类型,比如CHAR(10)等等,这个字段如果为null,那么就会出现在NULL值列表中,如果不为空,那么如果是10字节,那么就一定会占10字节空间,空位补空格(0x20)。
整体结构
同样的两条数据的显示,更多比较请查看原文
我们知道InnoDB按照页存储数据,而页的一般是16KB,也就是16384字节,但是mysql中一条数据存储的内容很可能高达60000+个字节,(比如一个VARCHAR(M)
类型的列就最多可以存储65532
个字节),那么就会出现在一页中无法存下一条数据的问题。 那么对于Compact
和Reduntant
这两种行格式,采用的策略类似于链表,就是每一页只存储部分数据,然后指定一个地址,表示接下来的内容到这个地址去取。
而Dynamic这种格式,就不会在记录的真实数据处存储字符串的前768
个字节,而是把所有的字节都存储到其他页面中,只在记录的真实数据处存储其他页面的地址。
Compressed
行格式和Dynamic
不同的一点是,Compressed
行格式会把存储到其他页面的数据采用压缩算法进行压缩,以节省空间。
参考博客一
参考博客二
一般使用的树有以下类型
二叉查找树,时间复杂度明显不行,在极端情况下会退化
平衡二叉树,查找性能很好,但是在节点插入和删除的极端数据下,时间复杂度也不理想,会造成log次旋转。
红黑树,优化了插入和删除的性能,稍微降级了一些查询性能,统计性能较好。但是,我们知道mysql数据库的内容是存在硬盘中的,我们需要将内容读到内存中才能方便操作。二叉树的查询时 l o g 2 log_2 log2级别的,换句话说,如果红黑树有x层,那么就必须IO操作x次,这大大限制查询性能,因此也不可取。
B树,本质上是一棵多叉树,且叉数>=2,那么也就是说,一般情况下, B树的高度会远小于红黑树,减少了IO次数,提高了查询速度。
B+树,相较于B树,拥有比B树更低的高度,也就意味着更少的IO次数,更快的查询速度。且B+树拥有范围查询的功能,而这是B树所没有的,同时B+树的查询的稳定性也更高,一般稳定IO次数是B+树的高度。
B+树和B树的比较
放一张B+树的结构图
聚簇索引
将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据
每个非叶子节点保存的是主键+页号,而每个叶子节点存放所有的数据
一个聚簇索引的小问题:
因为每个InnoDB存储引擎表都有一个聚簇索引,所以在默认有主键的情况下,那么这个聚簇索引就是主键索引,但是在没有主键的情况下,会进行如下操作
首先判断表中是否有非空的唯一索引,如果有,则该列即为主键
如果不符合上述条件,InnoDB存储引擎自动创建一个6字节大小的指针ROW_ID
现在的问题就在于这个row_id,这个row_id的值是一个全局计数器,这就意味着所有创建row_id的表,他们的插入性能就会受到影响,因为这个计数器是要线程安全的,以保证主键唯一,所以必然这些表在插入的时候是不能并发插入的,所以性能会受到影响。
二级索引(也叫非聚簇索引)
聚簇索引是按照主键排序的,那么如果不想要按照主键排序,可以使用二级索引,二级索引简单来说就是按照某一个非主键排序。
每个非叶子节点(也就是目录项)存放的是对应的非主键+页号,而叶子节点存放的是主键+对应的非主键。
所以我们如果要利用二级索引获得完整的数据,还必须再拿获得的主键去查询聚簇索引。
联合索引
相较于二级索引,排序的关键字不再只有一个,允许可以有多个关键字,就相当于是有主关键字和副关键字一样,如果主关键字不同就按照主关键字排序,如果相同,就以副关键字排序。
哈希索引
哈希索引就是利用哈希算法来创建索引。
那么特点也就很明显,单点查询快,只需要经过一次哈希计算,就能到到对应数据的位置。
缺点也很明显
覆盖索引
覆盖索引是select的数据列只用从索引中就能够取得,不必读取数据行。可以理解为一种思想,就是查询列表里只包含索引列。
举个例子,假设我们创建一个表
CREATE TABLE `tx`.`student` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(45) NOT NULL,
`birth` DATE NOT NULL,
`gender` CHAR(2) NOT NULL,
`location` VARCHAR(45) NOT NULL,
PRIMARY KEY (`id`));
那么我们下面的操作可以称之为覆盖索引
SELECT name,birth,gender from student where name='aaa'
使用这样的查询语句InnoDB一般使用的是聚簇索引+非聚簇索引,既可以通过主键直接查找数据,也可以通过非聚簇索引查找主键,再通过主键查找的方式查询。
MyISAM的使用的是非聚簇索引,所以对于MyISAM而言,建立按照主键排序的索引还是辅助键排序的索引在本质上来说都没有区别,因为MyISAM维护了一张按照插入顺序的表,这里面存放了所有的数据,且每一条数据都有对应的行号。那么MyISAM就是通过索引找到对应数据的行号,然后通过行号再去查找所有数据的。
详情参考这里
只为用于搜索、排序或分组的列创建索引
为列的基数大的列创建索引
索引列的类型尽量小
可以只对字符串值的前缀建立索引
只有索引列在比较表达式中单独出现才可以适用索引
为了尽可能少的让聚簇索引
发生页面分裂和记录移位的情况,建议让主键拥有AUTO_INCREMENT
属性。
具体可以参考InnoDB如果没有主键或者随机主键真的很可怕吗?,这篇文章中的数据
定位并删除表中的重复和冗余索引
尽量适用覆盖索引
进行查询,避免回表
带来的性能损耗。
索引底层使用的是B+树优化,能够尽可能的减少磁盘IO次数,节省时间,而且在查询的时候,由于数据是有序的,因此可以使用二分查找。这样子就能够加快查询速度。如果没有使用索引,则需要遍历双向链表,那么这个操作是非常耗时的。
这个很明显,索引底层是B+树,B+树实际上是平衡树,既然是平衡树就需要额外的时间消耗去维护平衡
对于一个联合索引,我们当用它去匹配的时候,总是会匹配最左边的字段,然后相同就会向后在比较一个字段,直到遇到范围查询(>、<、between、like)就停止匹配。
比如a = 1 and b = 2 and c > 3 and d = 4
如果建立(a,b,c,d)顺序的索引,d是用不到索引的,只能够以线性的速度查询。但是需要注意的是如果查询语句变成b = 2 and a = 1 and c > 3 and d = 4
还是能够用到a,b的索引。
可以这个样子原因在于mysql内部有查询优化器,可以对你的查询顺序进行内部的修改,以达到最大程度利用索引的效果。
表锁
行锁
InnoDB行锁和表锁都支持!
MyISAM只支持表锁!
InnoDB只有通过索引条件检索数据才使用行级锁,否则,InnoDB将使用表锁。也就是说,InnoDB的行锁是基于索引的!
共享锁(S锁,读锁):允许一个事务去读一行,阻止其他事务获得相同数据集的排他锁。是共享的,多个客户可以同时读取同一个资源,但不允许其他客户修改。
**排他锁(X锁,写锁):**允许获得排他锁的事务更新数据,阻止其他事务取得相同数据集的共享读锁和排他写锁。是排他的,会阻塞其他的写锁和读锁。
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:
参考博客:一文理解Mysql MVCC
数据库通常使用锁来实现隔离性。最原生的锁,锁住一个资源后会禁止其他任何线程访问同一个资源。但是很多应用的一个特点都是读多写少的场景,很多数据的读取次数远大于修改的次数,而读取数据间互相排斥显得不是很必要。所以就使用了一种读写锁的方法,读锁和读锁之间不互斥,而写锁和写锁、读锁都互斥。这样就很大提升了系统的并发能力。之后人们发现并发读还是不够,又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,于是就有了MVCC.
MVCC指的是多版本并发控制,MVCC是读已提交和可重复读取隔离级别的实现。
**MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。**其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。
InnoDB中通过undo log实现了数据的多版本,而并发控制通过锁来实现。
bin log:是mysql服务层产生的日志,常用来进行数据恢复、数据库复制,常见的mysql主从架构,就是采用slave同步master的binlog实现的, 另外通过解析binlog能够实现mysql到其他数据源(如ElasticSearch)的数据复制。
**redo log:**记录了数据操作在物理层面的修改,mysql中使用了大量缓存,缓存存在于内存中,修改操作时会直接修改内存,而不是立刻修改磁盘,当内存和磁盘的数据不一致时,称内存中的数据为脏页(dirty page)。为了保证数据的安全性,事务进行中时会不断的产生redo log,在事务提交时进行一次flush操作,保存到磁盘中, redo log是按照顺序写入的,磁盘的顺序读写的速度远大于随机读写。当数据库或主机失效重启时,会根据redo log进行数据的恢复,如果redo log中有事务提交,则进行事务提交修改数据。这样实现了事务的原子性、一致性和持久性。
undo log: undo log用于数据的撤回操作,它记录了修改的反向操作,比如,插入对应删除,修改对应修改为原来的数据,通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC。
RC
、RR
两种隔离级别的事务在执行普通的读操作时,通过访问版本链的方法,使得事务间的读写操作得以并发执行,从而提升系统性能。RC
、RR
这两个隔离级别的一个很大不同就是生成 ReadView
的时间点不同,RC
在每一次 SELECT
语句前都会生成一个 ReadView
,事务期间会更新,因此在其他事务提交前后所得到的 m_ids
列表可能发生变化,使得先前不可见的版本后续又突然可见了。而 RR
只在事务的第一个 SELECT
语句时生成一个 ReadView
,事务操作期间不更新。
这里需要用到前面的知识,关于[行格式的](# 记录的真实数据)
这里面有3个mysql添加的字段row_id
,transaction_id
,roll_pointer
,实现MVCC的多版本控制就主要依赖于后面2个参数。
transaction_id
:用来标识最近一次对本行记录做修改(insert|update)的事务的标识符, 即最后一次修改(insert|update)本行记录的事务id。
roll_pointer
:指写入回滚段(rollback segment)的 undo log
record (撤销日志记录记录),如果一行记录被更新, 则 undo log
record 包含 ‘重建该行记录被更新之前内容’ 所必须的信息。也就是通过这个可以形成一条版本链。
例子可以参考这里,下面说一下增删查改的不同。
SELECT
读取创建版本小于或等于当前事务版本号,并且删除版本为空或大于当前事务版本号的记录。也就是在这之前创建且没有被删除的记录。
INSERT
将当前事务的版本号保存至行的创建版本号,新插入一行。
UPDATE
新插入一行,并以当前事务的版本号作为新行的创建版本号,同时将原记录行的删除版本号设置为当前事务版本号
DELETE
将当前事务的版本号保存至行的删除版本号
基本信息可以参考我的这篇博客事务隔离性以及乐观锁和悲观锁
悲观锁是数据库层面加锁,都会阻塞去等待锁
乐观锁不是数据库层面上的锁,是需要自己手动去加的锁。
所以,针对乐观锁,一般有以下两种是实现形式
版本号机制
CAS算法
版本号机制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
CAS算法:即compare and swap(比较与交换)
CAS算法涉及到三个操作数
CAS具体执行时,当且仅当预期值A符合内存地址V中存储的值时,就用新值U替换掉旧值,并写入到内存地址V中。否则不做更新。
用下面的图可以更好的理解这个算法
CAS算法带来的问题——ABA问题
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。
解决方法:使用版本号来区分有没有被修改
从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
同时CAS算法也有循环开销时间大的问题,如果长时间不成功,会给CPU带来非常大的执行开销
当我们用范围条件检索数据而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合范围条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”。InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。
值得注意的是:间隙锁只会在Repeatable read
隔离级别下使用~
例子:假如emp表中只有101条记录,其empid的值分别是1,2,…,100,101
Select * from emp where empid > 100 for update;
上面是一个范围查询,InnoDB不仅会对符合条件的empid值为101的记录加锁,也会对empid大于101(这些记录并不存在)的“间隙”加锁。
InnoDB使用间隙锁的目的有两个:
Repeatable read
隔离级别下再通过GAP锁即可避免了幻读)所以在RR隔离级别下使用间隙锁可以解决幻读问题。
死锁概念:死锁是指两个或多个事务在同一资源上互相占用,并请求加锁时,而导致的恶性循环现象。当多个事务以不同顺序试图加锁同一资源时,就会产生死锁。
一些预防措施
以固定的顺序访问表和行。比如对两个job批量更新的情形,简单方法是对id列表先排序,后执行,这样就避免了交叉等待锁的情形;将两个事务的sql顺序调整为一致,也能避免死锁。
大事务拆小。大事务更倾向于死锁,如果业务允许,将大事务拆小。
在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率。
降低隔离级别。如果业务允许,将隔离级别调低也是较好的选择,比如将隔离级别从RR调整为RC,可以避免掉很多因为gap锁造成的死锁。
为表添加合理的索引。可以看到如果不走索引将会为表的每一行记录添加上锁,死锁的概率大大增大。
解决死锁方法
1.等待事务超时,主动回滚。
2.进行死锁检查,主动回滚某条事务,让别的事务能继续走下去。
MySQL 对于千万级的大表要怎么优化?
比较count(1)和count(*)
InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.
Mysql官方的解释count(1)和count(*)是完全一样的,不存在性能上的差异,但是这两者相比较而言,更推荐使用count(*),因为count(*)是SQL92定义的标准统计行数的语法。
关于count(字段)
他的查询比较简单粗暴,就是进行全表扫描,然后判断指定字段的值是不是为NULL,不为NULL则累加。
相比COUNT(*)
,COUNT(字段)
多了一个步骤就是判断所查询的字段是否为NULL,所以他的性能要比COUNT(*)
慢。
mysql对count(*)的优化
前面提到了COUNT(*)
是SQL92定义的标准统计行数的语法,所以MySQL数据库对他进行过很多优化。那么,具体都做过哪些事情呢?
这里的介绍要区分不同的执行引擎。MySQL中比较常用的执行引擎就是InnoDB和MyISAM。
MyISAM和InnoDB有很多区别,其中有一个关键的区别和我们接下来要介绍的COUNT(*)
有关,那就是MyISAM不支持事务,MyISAM中的锁是表级锁;而InnoDB支持事务,并且支持行级锁。
因为MyISAM的锁是表级锁,所以同一张表上面的操作需要串行进行,所以,MyISAM做了一个简单的优化,那就是它可以把表的总行数单独记录下来,如果从一张表中使用COUNT(*)进行查询的时候,可以直接返回这个记录下来的数值就可以了,当然,前提是不能有where条件。
MyISAM之所以可以把表中的总行数记录下来供COUNT(*)查询使用,那是因为MyISAM数据库是表级锁,不会有并发的数据库行数修改,所以查询得到的行数是准确的。
但是,对于InnoDB来说,就不能做这种缓存操作了,因为InnoDB支持事务,其中大部分操作都是行级锁,所以可能表的行数可能会被并发修改,那么缓存记录下来的总行数就不准确了。
但是,InnoDB还是针对COUNT(*)语句做了些优化的。
在InnoDB中,使用COUNT(*)查询行数的时候,不可避免的要进行扫表了,那么,就可以在扫表过程中下功夫来优化效率了。
从MySQL 8.0.13开始,针对InnoDB的SELECT COUNT(*) FROM tbl_name
语句,确实在扫表的过程中做了一些优化。前提是查询语句中不包含WHERE或GROUP BY等条件。
我们知道,COUNT(*)的目的只是为了统计总行数,所以,他根本不关心自己查到的具体值,所以,他如果能够在扫表的过程中,选择一个成本较低的索引进行的话,那就可以大大节省时间。
我们知道,InnoDB中索引分为聚簇索引(主键索引)和非聚簇索引(非主键索引),聚簇索引的叶子节点中保存的是整行记录,而非聚簇索引的叶子节点中保存的是该行记录的主键的值。
所以,相比之下,非聚簇索引要比聚簇索引小很多,所以MySQL会优先选择最小的非聚簇索引来扫表。所以,当我们建表的时候,除了主键索引以外,创建一个非主键索引还是有必要的。
至此,我们介绍完了MySQL数据库对于COUNT(*)的优化,这些优化的前提都是查询语句中不包含WHERE以及GROUP BY条件。
参考内容:不就是SELECT COUNT语句吗,竟然能被面试官虐的体无完肤