hello大家好,我是魔笑,笑看人生,如果对你有用,那就给个三连吧,好人一生平安。话不多说,直入主题。
我们先来说说,mysql三大特性之一的索引。
mysql存储数据是以页(page用于存储多个Row行记录,大小为16K)的方式存储的。
和树的目录一样,为了快速定位到数据,所以mysql就利用了B+Tree存储索引和数据。
话不罗嗦,我们看图说话,如下是B+Tree。
B+Tree是按照索引顺序升序构建的。非叶子节点不存储data数据,只存储索引值,这样便于存储更多的索引值。叶子节点包含了所有的索引值和data数据(行记录)。叶子节点用指针连接,提高区间的访问性能。
B+树进行范围查找时,只需要查找定位两个节点的索引值,定位到叶子节点,叶子节点之间用指针连接,然后利用叶子节点的指针进行遍历即可。
而B树需要遍历范围内所有的节点和数据。
聚簇索引和非聚簇索引:
B+Tree的叶子节点存放主键索引值和行记录就属于聚簇索引;
如果索引值和行记录分开存放就属于非聚簇索引。
解释:
1,InnoDB的聚簇索引就是按照主键顺序构建 B+Tree结构。B+Tree 的叶子节点就是行记录,行记录和主键值紧凑地存储在一起。 这也意味着 InnoDB 的主键索引树就是数据表本身,它按主键顺序存放了整张表的数据,占用的空间就是整个表数据量的大小。通常说的主键索引就是聚集索引。
2,MyISAM使用非聚集索引(非聚簇索引),索引和记录分开。
注意:
InnoDB的表要求必须要有聚簇索引(也叫做主键索引)。
1)如果表定义了主键,则主键索引就是聚簇索引
2)如果表没有定义主键,则第一个非空unique列作为聚簇索引
3)否则InnoDB会从建一个隐藏的row-id作为聚簇索引
MyISAM和InnoDB是怎么存储的?
1,InnoDB表对应两个文件,一个.frm表结构文件,一个.ibd数据文件。InnoDB表最大支持64TB;
2,MyISAM表对应三个文件,一个.frm表结构文件,一个MYD表数据文件,一个.MYI索引文件。从
MySQL5.0开始默认限制是256TB。
*主键索引和辅助索引:
B+Tree的叶子节点存放的是主键字段值就属于主键索引;
如果存放的是非主键值就属于辅助索引(二级索引)。
解释什么是辅助索引:
不是以主键建立的索引都是辅助索引,辅助索引的索引树,叶子节点存放的是索引和主键。
mysql中innodb引擎中是怎么利用主键索引和辅助索引查找数据的?
我们已主键索引作为条件查找,那么我们找到了索引也就找到了行数据,因为叶子节点包含了所有的索引值和data数据(行记录),查找结束。
我们已非主键索引(辅助索引)条件查找,首先找到该叶子节点对应的索引值,叶子节点存放的是索引和主键,然后再根据主键在主键索引树上找到其他数据
例:
表里有 id,name,age,salary这几个字段
查询语句:
select id,name,age,salary from user where name="魔笑"。
那么先找到索引name=“魔笑”,以及它的主键id=1。
然后再根据主键在主键索引树上找到其他数据age,salary两个字段的数据。
*普通索引:
这是最基本的索引类型,基于普通字段建立的索引,没有任何限制
1,创建普通索引的方法如下:CREATE INDEX <索引的名字> ON tablename (字段名);
2,删除索引:DROP INDEX <索引名> ON <表名>
*唯一索引:
索引字段的值必须唯一,但允许有空值
创建唯一索引的方法如下:CREATE UNIQUE INDEX <索引的名字> ON tablename (字段名);
*主键索引:
它是一种特殊的唯一索引,不允许有空值。在创建或修改表时追加主键约束即可,每个表只能有一个主键。
创建主键索引的方法如下:CREATE TABLE tablename ( [...], PRIMARY KEY (字段名) );
*复合索引:
用户可以在多个列上建立索引,这种索引叫做组复合索引(组合索引)
创建组合索引的方法如下:CREATE INDEX <索引的名字> ON tablename (字段名1,字段名2...);
*全文索引:
查询操作在数据量比较少时,可以使用like模糊查询。
但是对于大量的文本数据检索,效率很低。
如果使用全文索引,查询速度会比like快很多倍。
创建全文索引的方法如下:
CREATE FULLTEXT INDEX <索引的名字> ON tablename (字段名);
全文索引有自己的语法格式,使用 match 和 against 关键字,比如
select * from user where match(name) against('aaa');
*覆盖索引:
只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快,这就叫做索引覆盖,我们写sql时尽量达到覆盖索引。
MySQL 提供了一个 EXPLAIN 命令,它可以对 SELECT 语句进行分析,并输出 SELECT 执行的详细信息,供开发人员有针对性的优化。例如:
EXPLAIN SELECT * from user WHERE id < 3;
如下显示:
我们要善用这些信息,当我们写查询语句时,就用EXPLAIN检查,然后根据上面的信息,优化自己的sql语句。
*SIMPLE : 表示查询语句不包含子查询或union,例如(这些例子是在终端上执行的):
mysql> explain select * from user where dno=1 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: user
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: const
rows: 1
filtered: 100.00
Extra: NULL
*PRIMARY:表示此查询是最外层的查询
*SUBQUERY:SELECT子查询语句
*DEPENDENT UNION:UNION中的第二个或后续的查询语句,使用了外面查询结果
*DEPENDENT SUBQUERY:SELECT子查询语句依赖外层查询的结果,例如。
依赖外层的查询语句: select * from user a where a.dno=(select max(b.dno) from user b where a.name='java') ;
mysql> explain select * from user a where a.dno=(select max(b.dno) from user b where a.name='java') \G;
*************************** 1. row ***************************
id: 1
select_type: PRIMARY
table: a
partitions: NULL
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 6
filtered: 100.00
Extra: Using where
*************************** 2. row ***************************
id: 2
select_type: DEPENDENT SUBQUERY
table: b
partitions: NULL
type: index
possible_keys: NULL
key: PRIMARY
key_len: 4
ref: NULL
rows: 6
filtered: 100.00
Extra: Using where; Using index
*UNION:表示此查询是UNION的第二个或后续的查询
*UNION RESULT:UNION的结果
比较重要的一个属性,通过它可以判断出查询是全表扫描还是基于索引的部分扫描。常用属性值如下,从上至下效率依次增强。
*ALL:表示全表扫描,性能最差。
*index:表示基于索引的全表扫描,先扫描索引再扫描全表数据。
*range:表示使用索引范围查询。使用>、>=、<、<=、in等等。
mysql> explain SELECT * FROM user WHERE DNO>3 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: user
partitions: NULL
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: NULL
rows: 3
filtered: 100.00
Extra: Using where
1 row in set, 1 warning (0.00 sec)
*ref:表示使用非唯一索引进行单值查询。
*eq_ref:一般情况下出现在多表join查询,表示前面表的每一个记录,都只能匹配后面表的一行结果。
*const:表示使用主键或唯一索引做等值查询,常量查询。
mysql> explain select dno,name,dept_id from user where dno='1' \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: user
partitions: NULL
type: const
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: const
rows: 1
filtered: 100.00
Extra: NULL
1 row in set, 1 warning (0.00 sec)
*NULL:表示不用访问表,速度最快。
MySQL查询优化器会根据统计信息,估算SQL要查询到结果需要扫描多少行记录。原则上rows是越少效率越高,可以直观的了解到SQL效率高低。
1)Using where
表示查询需要通过索引回表查询数据。
什么是回表查询?
在之前介绍过,InnoDB索引有聚簇索引和辅助索引。聚簇索引的叶子节点存储行记录,InnoDB必须要有,且只有一个。辅助索引的叶子节点存储的是主键值和索引字段值,通过辅助索引无法直接定位行记录,通常情况下,需要扫码两遍索引树。先通过辅助索引定位主键值,然后再通过聚簇索引定位行记录,这就叫做回表查询,它的性能比扫一遍索引树低。
总结:通过索引查询主键值,然后再去聚簇索引查询记录信息
例如:
name是一个辅助索引,查询语句:EXPLAIN SELECT * FROM USER WHERE NAME='魔笑';
2)Using index
表示查询需要通过索引,索引就可以满足所需数据。
3)Using filesort
表示查询出来的结果需要额外排序,数据量小在内存,大的话在磁盘,因此有Using filesort建议优化。
例如:
EXPLAIN SELECT id,name,age,salary FROM USER ORDER BY age;
这样查询,它的Extra是Using filesort,所以我们得加以优化
1,加上索引
CREATE INDEX age1 ON USER(age);
2,避免回表查询
EXPLAIN SELECT age,id FROM USER ORDER BY age;
再执行一次,这次就是Using index
4)Using temprorary
查询使用到了临时表,一般出现于去重、分组等操作。
1,我们再写sql时尽量能使用“覆盖索引”,我们为什么要使用覆盖索引呢?
只需要在一棵索引树上就能获取SQL所需的所有列数据,无需回表,速度更快,这就叫做索引覆盖。
实现索引覆盖最常见的方法就是:
将被查询的字段,建立到组合索引。
2,使用索引时,复合索引要遵守“最左前缀原则”
复合索引使用时遵循最左前缀原则,最左前缀顾名思义,就是最左优先,即查询中使用到最左边的列,那么查询就会使用到索引,如果从索引的第二列开始查找,索引将失效。
例如,
创建索引名为“name_age”的索引,是name,age两列的组合
CREATE INDEX name_age ON USER(NAME,AGE)
我们遵守最左前缀原则,使用如下查询:
EXPLAIN SELECT NAME,AGE FROM USER WHERE name='魔笑'
EXPLAIN SELECT NAME,AGE FROM USER WHERE name='魔笑' and AGE='28'
看type,是使用了索引,rows只扫描了1行数据
如果使用如下查询语句:
EXPLAIN SELECT NAME,AGE FROM USER WHERE AGE='28';
如下,type是index,全范围扫描,行数是5,扫描了全部数据,因为违反了复合索引的顺序,所以索引失效
3, LIKE查询
面试题:MySQL在使用like模糊查询时,索引能不能起作用?回答:MySQL在使用Like模糊查询时,索引是可以被使用的,只有把%字符写在后面才会使用到索引。
select * from user where name like '%o%'; //不起作用
select * from user where name like 'o%'; //起作用
select * from user where name like '%o'; //不起作用
4,索引和排序
MySQL查询支持filesort和index两种方式的排序,filesort是先把结果查出,然后在缓存或磁盘进行排序操作,效率较低。使用index是指利用索引自动实现排序,不需另做排序操作,效率会比较高。
filesort有两种排序算法:双路排序和单路排序。
双路排序:
需要两次磁盘扫描读取,最终得到用户数据。第一次将排序字段读取出来,然后排序;第二次去读取其他字段数据。
单路排序:
从磁盘查询所需的所有列数据,然后在内存排序将结果返回。如果查询数据超出缓存
sort_buffer,会导致多次磁盘读取操作,并创建临时表,最后产生了多次IO,反而会增加负担。解决方
答案:少使用select *;增加sort_buffer_size(对数据排序的排序缓冲区的大小)容量和max_length_for_sort_data容量。
1,如果我们Explain分析SQL,结果中Extra属性显示Using filesort,表示使用了filesort排序方式,需要优化。
2,如果Extra属性显示Using index时,表示覆盖索引,也表示所有操作在索引上完成,也可以使用index排序方式,建议大家尽可能采用覆盖索引。
题目:
如果在user上建立复合索引(age,name)那么哪些方式会出现覆盖索引”Using index“,哪些方式会使得”Using filesort“
*下面这些情况是采用的”Using index“
例1:完全使用复合索引,如果查询的字段,跟索引不符,就回造成回表查询
EXPLAIN SELECT NAME,age FROM USER ORDER BY age,name;
例2:满足最左前缀的order by 排序,
SELECT NAME,age FROM USER ORDER BY age;
例3:WHERE子句+ORDER BY子句索引列组合满足索引最左前列
EXPLAIN SELECT id FROM USER WHERE age=28 ORDER BY NAME;
*下面这些情况采用的是”Using filesort“
例1:对索引列同时使用了ASC和DESC
EXPLAIN SELECT NAME,age FROM USER ORDER BY age ASC,NAME DESC;
例2:WHERE子句+ORDER BY子句中,where使用了范围查询(例如>、<、in
等)
EXPLAIN SELECT id FROM USER WHERE age>28 ORDER BY NAME;
例3:ORDER BY或者WHERE+ORDER BY索引列没有满足索引最左前列
EXPLAIN SELECT NAME,age FROM USER ORDER BY NAME;
例4:分别建立索引,id索引,和name索引,ORDER BY涉及了两个索引
EXPLAIN SELECT NAME,age FROM USER ORDER BY age,name;
什么是慢查询:
MySQL判断一条语句是否为慢查询语句,主要依据SQL语句的执行时间,它把当前语句的执
行时间跟 long_query_time 参数做比较,如果语句的执行时间 > long_query_time,就会把
这条执行语句记录到慢查询日志里面。long_query_time 参数的默认值是 10s,该参数值可
以根据自己的业务需要进行调整
下面说说慢查询把,当我们感觉系统查询特别慢时,我们可以打开慢查询,这样就可以记录,哪些sql语句查询的比较慢
看 MySQL 数据库是否开启了慢查询日志和慢查询日志文件的存储位置的命令如下:
SHOW VARIABLES LIKE 'slow_query_log%'
用如下命令开启慢查询,或者直接在/etc/my.cnf文件里面填加上如下参数(推荐)
命令
#开启慢查询
SET global slow_query_log = ON;
#慢查询文件名称
SET global slow_query_log_file = 'OAK-slow.log';
#表示会记录没有使用索引的查询SQL。前提是slow_query_log的值为ON,否则不会奏效
SET global log_queries_not_using_indexes = ON;
#写入慢查询日志的sql执行时长
SET long_query_time = 10;
或者文件修改
global slow_query_log = ON;
global slow_query_log_file = 'OAK-slow.log';
global log_queries_not_using_indexes = ON;
long_query_time = 10;
查看都有哪些sql是慢查询,那就去上面定义的OAK-slow.log文件里查看就行了。
慢查询原因总结:
全表扫描:explain分析type属性all
全索引扫描:explain分析type属性index
索引过滤性不好:靠索引字段选型、数据量和状态、表设计
频繁的回表查询开销:尽量少用select *,使用覆盖索引
分页查询使用简单的 limit 子句就可以实现。limit格式如下:
SELECT * FROM 表名 LIMIT [offset,] rows
*第一个参数指定第一个返回记录行的偏移量,注意从0开始;
*第二个参数指定返回记录行的最大数目;
*如果只给定一个参数,它表示返回最大的记录行数目;
思考1:如果偏移量固定,返回记录量对执行时间有什么影响?
select * from user limit 10000,1;
select * from user limit 10000,10;
select * from user limit 10000,100;
select * from user limit 10000,1000;
select * from user limit 10000,10000;
结果:在查询记录时,返回记录量低于100条,查询时间基本没有变化,差距不大。随着查询记录
量越大,所花费的时间也会越来越多。
思考2:如果查询偏移量变化,返回记录数固定对执行时间有什么影响?
select * from user limit 1,100;
select * from user limit 10,100;
select * from user limit 100,100;
select * from user limit 1000,100;
select * from user limit 10000,100;
结果:在查询记录时,如果查询记录量相同,偏移量超过100后就开始随着偏移量增大,查询时间
急剧的增加。(这种分页查询机制,每次都会从数据库第一条记录开始扫描,越往后查询越慢,而
且查询的数据越多,也会拖慢总查询速度。)
分页优化方案
第一步:利用覆盖索引优化,先只查id索引树
select id from user limit 10000,100;
第二步:利用子查询优化,先利用覆盖索引查出id的10001,再在这个基础上去查数据
select * from user where id>= (select id from user limit 10000,1) limit 100;
我们也可以,把每次分页查询的id记录下来,下次从该id查询,也可以优化分页查询
InnoDB支持事务和外键,具有安全性和完整性,适合大量insert或update操作
MyISAM不支持事务和外键,它提供高速存储和检索,适合大量的select查询操作
mysql的事务有如下几种特性:
事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行,这就是原子性
那么mysql是怎么保证mysql的原子性呢?
每一个写事务,都会修改BufferPool,从而产生相应的Redo/Undo日。每个数据再写入数据库磁盘中之前,先存在bufferPool中,每当有新的page数据读取到buffer pool时,InnoDb引擎会判断是否有空闲页,是否足够,如果有就将free page从free list列表删除,放入到LRU列表中。没有空闲页,就会根据LRU算法淘汰LRU链表默认的页,将内存空间释放分配给新的页,在Buffer Pool 中的页被刷到磁盘之前,这些日志信息都会先写入到日志文件中(Redo/Undo),在写入两个日志文件时都是先写入LogBuffer中,再由LogBuffer写入(Redo/Undo)日志文件,如果 Buffer Pool 中的脏页没有刷成功,此时数据库挂了,那在数据库再次启动之后,可以通过 Redo 日志将其恢复出来,以保证脏页写的数据不会丢失。如果脏页刷新成功,此时数据库挂了,就需要通过Undo来实现了。
知识点1:首先我们再查询sql时都会先查BufferPool(相当于内存缓存),如果没有再去磁盘读取数据
*BufferPool:
缓冲池,简称BP。BP以Page页为单位,默认大小16K,BP的底层采用链表数
据结构管理Page。在InnoDB访问表记录和索引时会在Page页中缓存,以后使用可以减少磁
盘IO操作,提升效率。
Page管理机制
Page根据状态可以分为三种类型:
free page : 空闲page,未被使用
clean page:被使用page,数据没有被修改过
dirty page:脏页,被使用page,数据被修改过,页中数据和磁盘的数据产生了不一致
针对上述三种page类型,InnoDB通过三种链表,free list ,flush list,lru list结构来维护和管理
free list :
表示空闲缓冲区,管理free page
flush list:
表示需要刷新到磁盘的缓冲区,管理dirty page,内部page按修改时间
排序。脏页即存在于flush链表,也在LRU链表中,但是两种互不影响,LRU链表负
责管理page的可用性和释放,而flush链表负责管理脏页的刷盘操作。
lru list:
表示正在使用的缓冲区,管理clean page和dirty page,缓冲区以
midpoint为基点,前面链表称为new列表区,存放经常访问的数据,占63%;后
面的链表称为old列表区,存放使用较少数据,占37%。
LRU算法:
链表分为new和old两个部分,加入元素时并不是从表头插入,而是从中间
midpoint位置插入,如果数据很快被访问,那么page就会向new列表头部移动,如果
数据没有被访问,会逐步向old尾部移动,等待淘汰。
说到BufferPool那么就不得不说说Change Buffer,logBuffer
Change Buffer:
写缓冲区,简称CB。在进行DML操作时,如果BP没有其相应的Page数据,
并不会立刻将磁盘页加载到缓冲池,而是在CB记录缓冲变更,等未来数据被读取时,再将数
据合并恢复到BP中。ChangeBuffer占用BufferPool空间,默认占25%,最大允许占50%,可以根据读写业务量来
进行调整。参数innodb_change_buffer_max_size;
总结:
当更新一条记录时,该记录在BufferPool存在,直接在BufferPool修改,一次内存操作。如
果该记录在BufferPool不存在(没有命中),会直接在ChangeBuffer进行一次内存操作,不
用再去磁盘查询数据,避免一次磁盘IO。当下次查询记录时,会先进性磁盘读取,然后再从
ChangeBuffer中读取信息合并,最终载入BufferPool中。
Log Buffer:
日志缓冲区,用来保存要写入磁盘上log文件(Redo/Undo)的数据,日志缓冲
区的内容定期刷新到磁盘log文件中。日志缓冲区满时会自动将其刷新到磁盘,当遇到BLOB
或多行更新的大事务操作时,增加日志缓冲区可以节省磁盘I/O。
LogBuffer主要是用于记录InnoDB引擎日志,在DML操作时会产生Redo和Undo日志。
LogBuffer空间满了,会自动写入磁盘。可以通过将innodb_log_buffer_size参数调大,减少
磁盘IO频率,innodb_flush_log_at_trx_commit参数控制日志刷新行为,默认为1
0 : 每隔1秒写日志文件和刷盘操作(写日志文件LogBuffer-->OS cache,刷盘OS cache-->磁盘文件),最多丢失1秒数据
1:事务提交,立刻写日志文件和刷盘,数据不丢失,但是会频繁IO操作
2:事务提交,立刻写日志文件,每隔1秒钟进行刷盘操作
Redo Log:
重做日志是一种基于磁盘的数据结构,用于在崩溃恢复期间更正不完整事务写入的数据。MySQL以循环方式写入重做日志文件,记录InnoDB中所有对Buffer Pool修改的日志。当出
现实例故障(像断电),导致数据未能更新到数据文件,则数据库重启时须redo,重新把数
据更新到数据文件。读写事务在执行的过程中,都会不断的产生redo log。默认情况下,重
做日志在磁盘上由两个名为ib_logfile0和ib_logfile1的文件物理表示。
Undo Log:
消日志是在事务开始之前保存的被修改数据的备份,用于例外情况时回滚事务。撤消日志属于逻辑日志,根据每行记录进行记录。撤消日志存在于系统表空间、撤消表空间和临时表
空间中。
说到日志文件,不得不说
Binary log:
是属于InnoDB引擎所特有的日志,而MySQL Server也有自己的日志,即 Binary
log(二进制日志),简称Binlog。Binlog是记录所有数据库表结构变更以及表数据修改的二进制日志,不会记录SELECT和SHOW这类操作。Binlog日志是以事件形式记录,还包含语句所执行的消耗时间。开启Binlog日志有以下两个最重要的使用场景。
主从复制:在主库中开启Binlog功能,这样主库就可以把Binlog传递给从库,从库拿到
Binlog后实现数据恢复达到主从数据一致性。
数据恢复:通过mysqlbinlog工具来恢复数据。(我们再也不用删库跑路了)
指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不应该对其有任何影响,不会丢失。
指的是一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对其他的并发事务是隔离的。
InnoDB 支持的隔离性有 4 种,隔离性从低到高分别为:读未提交、读提交、可重复读、可串行化。锁和多版本控制(MVCC)技术就是用于保障隔离性的。
由于事务并发,从而出现,更新丢失、脏读、不可重复读、幻读等问题,所以得对事务进行隔离控制。
mysql默认的隔离级别是可重复读,Oracle、SQLServer默认隔离级别:读已提交,下面我们看看Mysql几种隔离级别都会出现哪些问题。
演示,这几种隔离级别,出现的问题。
MySQL默认的事务隔离级别是Repeatable Read,查看MySQL当前数据库的事务隔离级别命令如下:
//查看隔离级别
show variables like 'tx_isolation';
//设置隔离级别
set tx_isolation='READ-UNCOMMITTED';//读未提交
set tx_isolation='READ-COMMITTED';//读已经提交
set tx_isolation='REPEATABLE-READ';//可重复读
set tx_isolation='SERIALIZABLE';//串性化
读未提交(READ-UNCOMMITTED),问题演示
脏读:一个事务读取到了另一个事务修改但未提交的数据。
//window用window+R,打开终端,连接数据库:mysql -uroot -p
//设置隔离级别
mysql> set tx_isolation='READ-UNCOMMITTED';
//开启事务
mysql> begin;
//插入数据,但是没有提交事务
mysql> insert into user values('1','java1',50);
mysql>
//再打开一个终端连接数据库,设置相同的隔离级别
mysql> set tx_isolation='READ-UNCOMMITTED';
//开启事务
mysql> begin;
//读取数据,能读到上一个事务没有提交的事务
mysql> select * from user;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 1 | java1 | 50 |
+----+-------+------+
不可重复读:一个事务中多次读取同一行记录不一致,后面读取的跟前面读取的不一致。
//事务1开启事务
mysql-1> begin;
//事务2开启事务
mysql-2> begin;
//事务2查询数据
mysql-2> select * from user where id-1;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 1 | java2 | 50 |
+----+-------+------+
//事务1跟新id=1的数据
mysql-1> update user set name='java3' where id=1;
//事务2在查,数据name不一致,能读到事务没有提交的事务,出现不可重复读
mysql-2> select * from user where id=1;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 1 | java3 | 50 |
+----+-------+------+
幻读:一个事务中多次按相同条件查询,结果不一致。后续查询的结果和面前查询结果不同,多了或少了几行记录。
//事务1开启事务
mysql-1> begin;
//事务2开启事务
mysql-2> begin;
//事务1读取数据
mysql-1> select * from user;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 1 | java4 | 50 |
+----+-------+------+
//事务2新增一条数据
mysql-2> insert into user values(2,'java5',50);
//事务1查询数据。查询出来的数据多了一条,能读到事务没有提交的事务,出现幻读
mysql-1> select * from user;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 1 | java4 | 50 |
| 2 | java5 | 50 |
+----+-------+------+
读已提交(READ-COMMITTED),问题演示
不可重复读:一个事务中多次读取同一行记录不一致,后面读取的跟前面读取的不一致。
//window用window+R,打开终端,连接数据库:mysql -uroot -p
//设置隔离级别
mysql> set tx_isolation='READ-COMMITTED';
//事务一开始事务
mysql-1> begin;
//事务二开始事务
mysql-2> begin;
//事务一读取id=1的数据
mysql-1> select * from user where id=1;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 1 | java1 | 50 |
+----+-------+------+
//事务二跟新数据
mysql-2> update user set name='java' where id=1;
//事务二查询数据,数据没有改变
mysql> select * from user where id=1;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 1 | java1 | 50 |
+----+-------+------+
//事务二提交事务
mysql-2>commit;
//事务一读取事务,两次读取不一致(读取到了已经提交的事务),导致不可重复读
mysql-1> select * from user where id=1;
+----+------+------+
| id | name | age |
+----+------+------+
| 1 | java | 50 |
+----+------+------+
幻读:一个事务中多次按相同条件查询,结果不一致。后续查询的结果和面前查询结果不同,多了或少了几行记录。
//事务一开始事务
mysql-1> begin;
//事务二开始事务
mysql-2> begin;
//事务一读取id=1的数据
mysql-1> select * from user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | java1 | 50 |
| 2 | java5 | 50 |
| 3 | java7 | 50 |
| 4 | java9 | 50 |
| 5 | java10 | 50 |
+----+--------+------+
//事务二插入数据
mysql-2> insert into user values(6,'java11',50);
//事务二查询数据,数据没有改变
mysql> select * from user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | java1 | 50 |
| 2 | java5 | 50 |
| 3 | java7 | 50 |
| 4 | java9 | 50 |
| 5 | java10 | 50 |
+----+--------+------+
//事务二提交事务
mysql-2>commit;
//事务一读取事务,两次读取不一致(读取到了已经提交的事务),导致不可重复读
mysql-1> select * from user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | java1 | 50 |
| 2 | java5 | 50 |
| 3 | java7 | 50 |
| 4 | java9 | 50 |
| 5 | java10 | 50 |
| 6 | java11 | 50 |
+----+--------+------+
隔离级别是可重复读( REPEATABLE-READ)就不会出现不可重复读问题:
//事务1开启事务
mysql-1> begin;
//事务2开启事务
mysql-2> begin;
//事务1查询id=1的数据
mysql-1> select * from user where id=1;
+----+------+------+
| id | name | age |
+----+------+------+
| 1 | java | 50 |
+----+------+------+
//事务2跟新id=1的数据
mysql-2> update user set name='java1' where id=1;
//事务1再查询id=1的数据,数据没有变
mysql> select * from user where id=1;
+----+------+------+
| id | name | age |
+----+------+------+
| 1 | java | 50 |
+----+------+------+
//事务2提交
mysql-2> commit;
//事务1再次查询,数据还是没有变
mysql-1> select * from user where id=1;
+----+------+------+
| id | name | age |
+----+------+------+
| 1 | java | 50 |
+----+------+------+
//事务1提交
mysql-1> commit;
//事务1再查询就能查询到变化的数据了,所以不会出现不可重复读
mysql-1> select * from user where id=1;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 1 | java1 | 50 |
+----+-------+------+
数据库的事务隔离级别越高,并发问题就越小,但是并发处理能力越差(代价)。读未提交隔离级别最低,并发问题多,但是并发处理能力好。以后使用时,可以根据系统特点来选择一个合适的隔离级别,比如对不可重复读和幻读并不敏感,更多关心数据库并发处理能力,此时可以使用Read Commited隔离级别。
事务隔离级别和锁的关系
1)事务隔离级别是SQL92定制的标准,相当于事务并发控制的整体解决方案,本质上是对锁和MVCC使用的封装,隐藏了底层细节。
2)锁是数据库实现并发控制的基础,事务隔离性是采用锁来实现,对相应操作加不同的锁,就可以防止其他事务同时对数据进行读写操作。
3)对用户来讲,首先选择使用隔离级别,当选用的隔离级别不能解决并发问题或需求时,才有必要在开发中手动的设置锁。
和持久性(Durability):
指的是事务开始之前和事务结束之后,数据库的完整性限制未被破坏。一致性包括两方面的内容,分别是约束一致性和数据一致性
1.1从操作的粒度可分为表级锁、行级锁
表级锁:每次操作锁住整张表。锁定粒度大,发生锁冲突的概率最高,并发度最低。应用在
MyISAM、InnoDB、BDB 等存储引擎中。
行级锁:每次操作锁住一行数据。锁定粒度最小,发生锁冲突的概率最低,并发度最高。应
用在InnoDB 存储引擎中(MyISAM不支持行级锁)。
1.2,从操作的类型可分为读锁和写锁。
读锁(S锁):共享锁,针对同一份数据,多个读操作可以同时进行而不会互相影响。
写锁(X锁):排他锁,当前写操作没有完成前,它会阻断其他写锁和读锁。
S锁:事务A对记录添加了S锁,可以对记录进行读操作,不能做修改,其他事务可以对该记录追加
S锁,但是不能追加X锁,需要追加X锁,需要等记录的S锁全部释放。
X锁:事务A对记录添加了X锁,可以对记录进行读和修改操作,其他事务不能对记录做读和修改操
作(即不能追加X锁,也不能追加X锁)。
1.3,从锁的性质可分,共享锁(行级锁-读锁),排他锁(行级锁-写锁)
共享锁(行级锁-读锁)
共享锁又称为读锁,简称S锁。共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数
据,但是只能读不能修改。使用共享锁的方法是在select ... lock in share mode,只适用查询语
句。
总结:事务使用了共享锁(读锁),只能读取,不能修改,修改操作被阻塞。
排他锁(行级锁-写锁)
排他锁又称为写锁,简称X锁。排他锁就是不能与其他锁并存,如一个事务获取了一个数据行的排
他锁,其他事务就不能对该行记录做其他操作,也不能获取该行的锁。使用排他锁的方法是在SQL末尾加上for update,innodb引擎默认会在update,delete语句加上for update。行级锁的实现其实是依靠其对应的索引,所以如果操作没用到索引的查询,那么会锁住全表记录。
总结:
事务使用了排他锁(写锁),当前事务可以读取和修改,其他事务不能修改,也不能获取记录锁(select... for update)。如果查询没有使用到索引,将会锁住整个表记录。
1.4,从操作的性能可分为乐观锁和悲观锁
乐观锁:比如再用一个字段版本号(时间戳)作为记录数据版本,首先读取数据,记录它的版本(时间戳),再更新时,将时间戳和数据库时间戳做比较,在数据更新提交的时候才会进行冲突检测(时间戳检测),如果发现冲突了(不一致),则提示错误信息。
悲观锁:在对一条数据修改的时候,为了避免同时被其他人修改,在修改数据之前先锁定,
再修改的控制方式,前面提到的行锁、表锁、读锁、写锁、共享锁、排他锁等,这些都属于悲观锁
范畴。。
lock table 表名称 read|write,表名称2 read|write;
例如 : lock table user read;
//查看表上加过的锁
show open tables;
//删除表锁
unlock tables;
总结:表级读锁会阻塞写操作,但是不会阻塞读操作。而写锁则会把读和写操作都阻塞
RecordLock锁(记录锁):
锁定单个行记录的锁。(记录锁,RC、RR隔离级别都支持)
GapLock锁(间隙锁或者范围锁):
锁定索引记录间隙,确保索引记录的间隙不变。(范围锁,RR隔离级别支持)
Next-key Lock 锁:
记录锁和间隙锁组合,同时锁住数据,并且锁住数据前后范围。(记录锁+范
围锁,RR隔离级别支持)
总结:
在RR隔离级别,InnoDB对于记录加锁行为都是先采用Next-Key Lock,但是当SQL操作含有唯一索引时,Innodb会对Next-Key Lock进行优化,降级为RecordLock,仅锁住索引本身而非范围。
如下:
1)select ... from 语句:
InnoDB引擎采用MVCC机制实现非阻塞读,所以对于普通的select语句,InnoDB不加锁
2)select ... from lock in share mode语句:
追加了共享锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
例如:有索引条件的查询(id是唯一索引),所以会将Next-key降级为RecordLock锁
//这里是打开两个终端,模拟两个事务的
mysql> select * from user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | java1 | 50 |
| 2 | java5 | 50 |
| 3 | java7 | 50 |
| 4 | java9 | 50 |
| 5 | java10 | 50 |
| 6 | java11 | 50 |
+----+--------+------+
//开启事务1
mysql> begin;
//开启事务2
mysql-2> begin;
//对事务1中id=2的数据加上共享锁
mysql-1> select * from user where id=2 lock share in mode;
+----+------+------+
| id | name | age |
+----+------+------+
| 2 | php | 50 |
+----+------+------+
//事务2执行跟新操作,事务2会一直等待执行,直到事务1事务提交,释放锁。事务2才执行
mysql-2> update user set name='php' where id=2;
//如果事务1执行查询,可以操作,不会等待
mysql> select * from user where id=2;
+----+------+------+
| id | name | age |
+----+------+------+
| 2 | php | 50 |
+----+------+------+
3)select ... from for update语句:
追加了排他锁,InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
例如:name没有添加索引,没有索引条件的查询,会导致全表锁定,因为InnoDB引擎
锁机制是基于索引实现的记录锁定。
//开启事务1
mysql-1> begin;
//开启事务2
mysql-2> begin;
//事务1查询第二行数据,加上排他锁,name是没有索引的
mysql-1> select name from user where name='java2' for update;
+-------+
| name |
+-------+
| java2 |
+-------+
//事务2一直等待执行,事务1执行完毕,释放锁,说明:导致全表锁定
mysql-2> update user set name='java' where id=5;
//事务1提交
mysql-1>commit;
//事务1提交完了,释放了锁,事务2才执行
如果给name加上索引呢?就变成有索引条件的查询
//给name加上索引
mysql> create index name1 on user(name);
//数据库数据
mysql> select * from user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | java | 50 |
| 2 | java2 | 50 |
| 3 | java7 | 50 |
| 4 | java9 | 50 |
| 5 | java10 | 50 |
| 6 | java11 | 50 |
+----+--------+------+
//开启事务1
mysql-1> begin;
//开启事务2
mysql-2> begin;
//事务1查询第二行数据,加上排他锁,name是有索引的
mysql-1> select name from user where name='java2' for update;
+-------+
| name |
+-------+
| java2 |
+-------+
//事务2也可以执行更新操作,不用等待执行,说明:使用的是RecordLock锁进行处理,只锁住第2行数据
mysql-2> update user set name='java' where id=1;
4)update ... where 语句:
InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
5)delete ... where 语句:
InnoDB会使用Next-Key Lock锁进行处理,如果扫描发现唯一索引,可以降级为RecordLock锁。
6)insert语句:
InnoDB会在将要插入的那一行设置一个排他的RecordLock锁。
总结:
如果用唯一索引条件查询,会使用RecordLock锁(记录锁)锁定单个行记录的锁。如果用非唯一索引条件查询,会使用Next-key Lock 锁。如果在事务中执行了一条没有索引条件的查询,导致全表锁定,会降低并发。
一、表锁死锁
产生原因:
用户A访问表A(锁住了表A),然后又访问表B;另一个用户B访问表B(锁住了表B),然后企图
访问表A;这时用户A由于用户B已经锁住表B,它必须等待用户B释放表B才能继续,同样用户B要
等用户A释放表A才能继续,这就死锁就产生了。
用户A--》A表(表锁)--》B表(表锁)
用户B--》B表(表锁)--》A表(表锁)
解决方案:
这种死锁比较常见,是由于程序的BUG产生的,除了调整的程序的逻辑没有其它的办法。仔细分
析程序的逻辑,对于数据库的多表操作时,尽量按照相同的顺序进行处理,尽量避免同时锁定两个
资源,如操作A和B两张表时,总是按先A后B的顺序处理, 必须同时锁定两个资源时,要保证在任何时刻都应该按照相同的顺序来锁定资源。
产生原因1:
如果在事务中执行了一条没有索引条件的查询,引发全表扫描,把行级锁上升为全表记录锁定(等
价于表级锁),多个这样的事务执行后,就很容易产生死锁和阻塞,最终应用系统会越来越慢,发
生阻塞或死锁。
产生原因2:
两个事务分别想拿到对方持有的锁,互相等待,于是产生死锁
当用户A访问表A(锁住了表A),等待访问B表
当用户B访问表B(锁住了表B),等待访问A表
导致互相等待对方释放锁,从而发生死锁。
解决方案2:
在同一个事务中,尽可能做到一次锁定所需要的所有资源
按照id对资源排序,然后按顺序进行处理
产生原因:
事务A 查询一条纪录,然后更新该条纪录;此时事务B 也更新该条纪录,这时事务B 的排他锁由于
事务A 有共享锁,必须等A 释放共享锁后才可以获取,只能排队等待。事务A 再执行更新操作时,
此处发生死锁,因为事务A 需要排他锁来做更新操作。但是,无法授予该锁请求,因为事务B 已经
有一个排他锁请求,并且正在等待事务A 释放其共享锁。
事务A: select * from dept where deptno=1 lock in share mode; //共享锁,1
update dept set dname='java' where deptno=1;//排他锁,3
事务B: update dept set dname='Java' where deptno=1;//由于1有共享锁,没法获取排他锁,需
等待,2
例如:这样就发生死锁了,报错,Deadlock found .......
解决方案:
对于按钮等控件,点击立刻失效,不让用户重复点击,避免引发同时对同一条记录多次操
作;
使用乐观锁进行控制。乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量
下的系统性能。需要注意的是,由于乐观锁机制是在我们的系统中实现,来自外部系统的用
户更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中;
MySQL提供了几个与锁有关的参数和命令,可以辅助我们优化锁操作,减少死锁发生。
查看死锁日志
通过show engine innodb status\G命令查看近期死锁日志信息。
如图,可以查看到死锁的一些信息
1、查看近期死锁日志信息;
2、使用explain查看下SQL执行计划查看锁状态变量
通过show status like'innodb_row_lock%‘命令检查状态变量,分析系统中的行锁的争夺
情况
Innodb_row_lock_current_waits:当前正在等待锁的数量
Innodb_row_lock_time:从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg: 每次等待锁的平均时间
Innodb_row_lock_time_max:从系统启动到现在等待最长的一次锁的时间
Innodb_row_lock_waits:系统启动后到现在总共等待的次数
如果等待次数高,而且每次等待时间长,需要分析系统中为什么会有如此多的等待,然后着
手定制优化。
如图: