赞
踩
关于MySQL的简介就不再赘述,我们首先来看一下MySQL的逻辑架构:
MySQL的引擎常用的有以下两种:
(1)InnoDB:默认引擎,支持事务,行锁,外键,适合高并发;
(2)MyISAM:不支持外键,不支持事务,不适合高并发。
对于我们手写的SQL来说,一般的顺序如下:
select 字段 from 表名 where 条件 group by 字段 having 字段 order by 字段
但是对于MySQL来说,解析的顺序如下:
(1)from;
(2)on(join的条件);
(3)join;
(4)where;
(5)group by;
(6)having;
(7)select;
(8)distinct;
(9)order by;
(10)limit。
详细的select语句执行顺序的鱼骨图如下:
以下是7种join理论:
(1)select xxx from TableA A inner join TableB B on A.key = B.key;
(2)select xxx from TableA A left join TableB B on A.key = B.key;
(3)select xxx from TableA A right join TableB B on A.key = B.key;
(4)select xxx from TableA A left join TableB B on A.key = B.key where B.key is null;
(5)select xxx from TableA A right join TableB B on A.key = B.key where A.key is null;
(6)select xxx from TableA A full outer join TableB B on A.key = B.key;
(7)select xxx from TableA A full outer join TableB B on A.key = B.key where A.key is null or B.key is null;
除了数据外,数据库还维护着满足特定算法(B树)的数据结构这些数据结构指向数据,这种数据结构就是索引。索引是帮助MySQL高效获取数据的一种数据结构;可以理解为“排好序的可以快速查找的数据结构”(即拥有排序和查找的功能);所以索引会影响where和order by。
索引的优缺点如下:
(1)优点:
<1>提高检索效率,降低数据库I/O成本;
<2>降低数据库排序成本,降低CPU消耗;
(2)缺点:
<1>索引需要占用空间,太多的索引会消耗性能(一张表尽量少于5个索引);
<2>索引可以提高查询速度,但是会降低更新表的速度(如增、删、改),因为更新表后还需要更新索引(所以经常改动的表不建议建索引);
<3>如果数据量大,就需要花时间研究最优的索引。
索引(B树)的结构图如下:
上图中,只有叶子节点存在真实数据,非叶子节点只存放搜索方向的数据项;浅蓝色称为一个磁盘块,可以看到每个磁盘块包含几个深蓝色的数据项和黄色的指针(如磁盘1包含数据项17和35,包含指针p1、p2和p3)。
利用索引进行一次查找的过程如下:如果我们要查找数据项29,那么首先把磁盘块1加载到内存,发生第1次I/O,在内存中用二分查找确定29在17到35之间,锁定磁盘块1的p2指针(因为内存速度非常快,相比于磁盘I/O可以忽略不计);通过p2指针把磁盘块3加载到内存,发生第2次I/O,在内存中用二分查找确定29在26到30之间,锁定磁盘块3的p2指针;通过p2指针把磁盘块8加载到内存,发生第3次I/O,在内存中用二分查找确定到29;查询结束,总计3次I/O。
事实上在真实情况下,3层的B+树可以表示上百万的数据,即查找上百万的数据只需要3次I/O;性能提升巨大。
建立索引的时机如下:
(1)适合建索引:
<1>主键自动建立唯一索引;
<2>频繁作为查询条件的字段;
<3>查询中与其他表关联字段,外键关系的字段;
<4>高并发下倾向建立组合索引;
<5>查询中排序的字段;
<6>查询中统计或分组的字段。
(2)不适合建索引:
<1>频繁更新的字段;
<2>where条件用不到的字段;
<3>表记录太少的表;
<4>数据重发且分布平均的字段,如性别、国家等。
【注】关于所有的几个概念如下:
(1)覆盖索引: 即查询的字段顺序正好和建立的索引一样,所以select的数据列只从索引中就能取得,而不需要读取数据行;(所以注意不要用select *,否则用不上覆盖索引)
(2)最左匹配原则: 对于建立的联合索引,从最左优先匹配;比如建立(a,b,c)的联合索引,相当于同时建立了(a)、(a,b)、(a,b,c)这三个索引,如果直接按(b,c)去查询是用不到索引的;
(3)回表: 是一种数据库检索过程,通常发生在使用二级索引检索非主键索引数据的过程中。举个例子:表tbl有a,b,c三个字段,其中a是主键,b上建了索引,然后编写sql语句SELECT * FROM tbl WHERE a=1这样不会产生回表,因为所有的数据在a的索引树中均能找到SELECT * FROM tbl WHERE b=1这样就会产生回表,因为where条件是b字段,那么会去b的索引树里查找数据,但b的索引里面只有a,b两个字段的值,没有c,那么这个查询为了取到c字段,就要取出主键a的值,然后去a的索引树去找c字段的数据。查了两个索引树,这就叫回表。
(4)索引下推(5.6及之后版本): 相比于回表,索引下推减少回表查询全行数据的操作(mysql默认启用索引下推);索引下推在非主键索引上的优化,可以有效减少回表的次数,大大提升了查询的效率。举个例子:表tbl有a,b,c三个字段,建立联合索引(b,c),然后编写sql语句SELECT * FROM tbl WHERE b=1 AND c>2;InnoDB并没有忽略c这个字段,而是在索引内部就判断了c是否大于2,对于不大于2的记录直接跳过,因此在(b,c)这棵索引树中只匹配到了一个记录,此时拿着这个id去主键索引树中回表查询全部数据,这个过程只需要回表一次,有效地减少了回表的次数。
【注】关于索引的优化方案我放在下面的章节来讨论。
使用explain关键字可以模拟优化器执行SQL查询语句的过程,从而知道MySQL是如何处理你写的SQL语句的;进而分析性能瓶颈。explain的字段包含如下的字段:
接下来我们逐个介绍各个字段:
【1】id:
这里的id字段和我们平时理解的id不太一样,这里的id用来表示执行select子句的顺序;id有3种情况,如下:
(1)id相同:表示从上往下顺序执行;
(2)id不同:如果是子查询,id的序号会递增;id值越大执行的优先级越高,越先被执行;
(3)id相同与不同的同时存在:id相同,则认为是同一组,从上往下顺序执行;在所有组中,id值越大,优先级越高,越先执行;如果出现“derived+数字”,如derived2,则表示“id为2”的那个组执行完衍生出的“虚表”,即执行完“id为2”的那个组后就执行derived2的那个组。
【2】select_type:
select_type主要有6个,如下:
(1)simple:普通查询,不包含子查询或者union;
(2)primary:查询中若包含任何复杂的子部分,则最外层被标记为primary,即最后被执行的;
(3)subquery:子查询,在select或where列表中包含子查询;
(4)derived:在from列表中包含的子查询被标记为derived,即把子查询结果放入临时表;
(5)union:若第二个select在union之后,则标记为union;如union包含在from子句的子查询中,外层的select将被标记为derived;
(6)union result:从union表获取结果的select。
【3】table:
表示查询的表名。
【4】type:
type表示访问类型,如下:
(1)all:表示全表扫描,即直接在硬盘上扫描全表找到匹配的行;
(2)index:表示全索引扫描,与all的区别在于只遍历全索引树,比all快;
(3)range:表示检索给定范围的行(比较常见),如使用了“between,<,>”;
(4)ref:表示非唯一性索引扫描(比较常见),即返回匹配单独值的所有行;是一种索引访问,但是可能找到多个行;(例如查找公司中名字为张三的员工,可能有多个)
(5)eq_ref:表示唯一性索引扫描,对于每个索引键,表中只有一条记录匹配,常见于主键或唯一索引扫描;(例如查找公司中的CEO,只能有一个)
(6)const:表示通过索引一次就找到了,用于比较主键或唯一索引,只匹配一行数据,在where中的条件可转换为常量;(例如查找公司中工号为1的员工,只能有一个,且可转换为常量)
(7)system:表示表中只有一条记录,类似于系统表,一般不出现;
(8)null:无。
以上type的性能从最好到最差依次为:
system>const>eq_ref>ref>range>index>all
【5】possible_keys:
表示可能应用在这张表上的索引,可以是一个或者多个;查询涉及到的字段上若存在索引,则该索引将被列出,但不一定被查询实际使用。
【6】key:
表示实际使用的索引,若为null,则没有用到索引(没用到或者没建索引);如果查询中使用了覆盖索引,则该索引只出现在key列表中,不出现在possible_keys中。
【7】key_len:
表示索引使用的字节数,可通过该列计算查询中使用的索引长度;在不损失精确性的情况下,长度越短越好;显示的值是索引字段的可能最大长度,不是实际使用的长度。
【8】ref:
表示索引的哪一列被使用了,如果可能的话,是一个常数;哪些列或常量被用于查找索引列上的值。这个属性进一步说明索引用到哪几个数据库中的字段上了。
【9】rows:
表示根据表统计信息及索引选用情况,大致估算出找到结果所需要读取的行数,数值越少越好。
【10】extra:
表示包含不合适在其他列中显示但十分重要的额外信息。
extra包括以下几个属性:
(1)using fileSort(比较危险,推荐优化):说明MySQL会对数据使用一个外部的索引排序,而不是按表内的索引顺序进行读取;当MySQL无法利用索引完成排序是就会使用这个“文件排序”;比如order by之后字段没有按照索引排序,就会导致MySQL使用“文件排序”;
(2)using temporary(比较危险,推荐优化):说明MySQL使用了临时表保存了中间结果,使用了临时表;常见于order by和group by,比如group by没有用到索引,就会导致MySQL使用临时表;
(3)using index(效率不错):说明相应的select操作中使用了覆盖索引,避免了访问表的数据行;若同时出现using where,表明索引被用来执行索引值得查找(用了where);若没有同时出现using where,表明索引用来读取数据而非执行查找动作(没用where);
(4)using where:说明使用了where条件过滤;
(5)using join buffer:说明使用了连接缓存;
(6)impossible where:where子查询总为false,即条件永远不成立;
(7)select tables optimized away:在没有group by情况下,基于索引优化min/max;
(8)distinct:说明优化了distinct操作,即使用distinct情况下,找到第一个匹配的就不再继续找相同的了。
【注】要么不建索引,要么建索引后group by就要按照索引的顺序和个数来,否则容易产生“using fileSort”和“using temporary”。
show profile是MySQL提供的可用来分析当前会话中语句执行的资源消耗情况;用于SQL语句的调优测量。show profile默认关闭;打开后默认保存最近15次的运行结果。
利用show profile分析的步骤如下:
(1)查看当前版本是否支持show profile:show variables like ‘profiling’;
(2)开启show profile功能:set profiling = on;
(3)运行有问题的sql;
(4)运行show profile查看结果:show profiles;
(5)诊断SQL语句,show profile写法如下:
show profile 可用参数 for query SQL编号
可用的参数如下:
(1)ALL:所有开销信息;
(2)BLOCK IO:显示I/O相关开销;
(3)CONTEXT SWITCHES:上下文切换相关开销;
(4)CPU:CPU相关开销;
(5)IPC:发送和接收相关开销;
(6)MEMORY:内存相关开销;
(7)PAGE FAULTS:页面错误相关开销;
(8)SOURCE:显示source_function、source_file、source_line相关开销;
(9)SWAPS:显示交换次数相关开销。
如果出现以下内容就需要优化SQL:
(1)converting HEAP to MyISAM:查询结果太大,内存不够用往磁盘搬;
(2)creating tmp table:创建了临时表;
(3)copying to tmp table on disk:把内存中临时表拷贝到磁盘上;
(4)lock:触发锁机制。
MySQL的慢查询日志是MySQL提供的一种日志记录,它用来记录在MySQL中响应时间超过某阈值的语句;具体指运行时间超过“long_query_time”值得SQL,则会被记录到慢查询日志中,MySQL默认为10秒;默认MySQL不打开慢日志查询,可以手动打开,一般只在调优时打开,因为打开会影响性能。
使用慢查询日志步骤如下:
(1)查看慢查询日志是否开启,及默认日志文件存储位置:show variables like ‘%slow_query_log%’;
(2)开启慢查询日志:set global slow_query_log=1;(只对当前数据库生效,重启后失效;在my.cnf中修改会永久生效)
(3)查看慢查询日志时间阈值:show variables like ‘%long_query_time%’;(只有大于阈值才会记录,等于阈值不会记录)
(4)设置慢查询日志时间阈值:set global long_query_time=3;(修改后,需要重新打开一个会话才会生效)
(5)产生慢查询后,会记录慢查询的SQL,然后我们到对应存放日志的位置查看即可;
(6)查看有多少条慢SQL:show global status like ‘%slow_queries%’;
这里MySQL的优化我主要介绍关于索引的优化,即要使用到索引。这里提供一段口诀(๑•̀ㅂ•́)و✧;说明什么情况下能使用上索引:
“全值匹配我最爱,最左前缀要遵守;
带头大哥不能死,中间兄弟不能断;
索引列上少计算,范围之后全失效;
LIKE百分写最右,覆盖索引不写星;
不等空值还有OR,索引失效要少用;
VAR引号不可去,SQL高级也不难!”
对于以上口诀的解释如下:
(1)当按索引中所有列进行精确匹配(这里精确匹配指“=”或“IN”匹配)时,索引可以被用到;
(2)联合索引需要遵守最左前缀原则,即在联合索引中第一个字段不能去掉而且联合索引中间的字段不能断掉,否则索引用不上;
(3)在索引列上最好不要进行计算,否则性能降低;
(4)在范围性的条件(如>、<)后面的字段索引用不上;
(5)在like后的%不要写在最前面,否则索引用不上,但是%写在其他地方可以用上索引;
(6)在用覆盖索引的时候,不要直接select *,否则覆盖索引用不上;
(7)不等号、NULL值、OR会使得索引用不上;
(8)当条件中用到字符型时,需要加上单引号,否则SQL会自动转换,导致索引用不上。
下面给几个关于索引的题目,以便更好的理解索引使用:(假设有索引(c1,c2,c3,c4);select语句省略)
(1)......where c4='a4' and c3='a3' and c2='a2' and c1='a1';
答:用到了索引;虽然顺序为4321,但是MySQL会自动优化;
(2)......where c1='a1' and c2='a2' and c3>'a3' and c4='a4';
答:用到了前3个;因为c3>'a3’使得索引断了;
(3)......where c1='a1' and c2='a2' and c4='a4' order by c3;
答:用到了前2个;因为c3用于排序而不用于查找,索引断了;
(4)......where c1='a1' and c2='a2' order by c4='a4';
答:用到了前2个;同上c3的索引断了,而且排序没有用到索引,会产生using fileSort;
(5)......where c1='a1' and c2='a2' order by c3;
答:用到了前3个;只不过前2个用于查找,第3个用于排序,所以对于第3个,explain不会统计到使用的索引里面;
(6)......where c1='a1' and c5='a5' order by c3,c2;
答:用到了前1个;但是order by后面索引字段顺序不对,用不上索引,会产生using fileSort;
(7)......where c1='a1' and c2='a2' and c5='a5' order by c3,c2;
答:用到了前3个;相比于上一个,多了c2='a2’导致索引用到了c3,只不过c3用于排序,虽然c2也用来排序,但是写在c2索引已经确定了,所以没有产生using fileSort;
(8)......where c1='a1' and c4='a4' group by c2,c3;
答:用到了4个;因为group by之前需要先排序,所以相当于索引全部用到了,并且没有产生using fileSort;
(9)......where c1='a1' and c4='a4' group by c3,c2;
答:用到了1个;这里group by后面索引字段顺序不对,用不上索引,会产生using fileSort,并且还产生using temporary;
MySQL中的锁有如下的分类:
(1)按操作类型:
<1>读锁:多个操作共享;
<2>写锁:排他操作。
(2)按操作粒度:
<1>表锁:偏向MyISAM引擎,不适合高并发场景;
<2>行锁:偏向InnoDB引擎,适合高并发场景。
对应MyISAM引擎来说,读锁会阻塞写操作,但不阻塞读操作;而写锁会把读操作和写操作都阻塞。
我们通常使用的是InnoDB引擎,InnoDB引擎支持事务,采用行锁,适合高并发,但是开销大。以下几个情况会产生行锁:
(1)表中创建索引(当然主键也是唯一索引),然后在where条件后加上索引字段,产生行锁;
(2)必须有事务,才产生行锁;
(3)在select语句后加上for update,或者做update操作时,产生行锁。
需要注意的是,如果索引失效(没建或者没用到索引),则InnoDB引擎下,行锁会变成表锁;示例如下,同时两个update语句更新同一张表:
(前提:phone建立了索引)
(1)第一个SQL为:update user set name='张三' where phone like '%1000';
(2)第二个SQL为:update user set name='李四' where phone = '2000';
正常情况下,这两个SQL会互不影响地更新表中数据;但是由于第一个SQL的like条件在最左边加上了%,导致索引失效;而且又由于索引失效导致整张表被锁定,所以第二个SQL会一直被阻塞直到第一个SQL的事务提交。
对于行锁有以下一些建议:
(1)尽量让所有数据检索条件(where后的)都通过索引完成,避免索引失效造成行锁变成表锁;
(2)合理设计索引,尽量缩小锁的范围;
(3)尽量减少检索条件范围,避免间隙锁;
(4)尽量控制事务大小,减少锁定资源量和时间长度;
(5)尽量做低级别的事务隔离。
【注】对于“间隙锁”有如下说明:
当用范围条件(如<、>)而不是相等条件检索数据,并且请求共享锁或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁;对于键值在范围内即使不存在的记录(叫做“间隙”),InnoDB也会给这个“间隙”加锁;这种机制叫做间隙锁。这样会导致在其他事务插入这些不存在的数据时,被阻塞而无法插入数据,在某些场景下会造成巨大的危害。
事务是由一组SQL语句组成的逻辑处理单元;MySQL默认开启了事务的自动提交,在执行增删改查时,每一条SQL就是一个事务;事务具有4个属性,通常称为“ACID”,具体如下:
(1)原子性(Atomicity):同一个事务中SQL语句的要么都做,要么都不做;
(2)一致性(Consistent):在事务开始和完成时,数据必须保持一致状态,比如银行我转走100元,对方的账户上就必须多出100元,否则事务不一致;
(3)隔离性(Isolation):事务之间不应该互相干扰(有4中隔离级别),事务的中间状态对外不可见,反之亦然;
(4)持久性(Durable):事务一旦提交,对数据的修改就是永久性的,即使系统出现故障也能保持对数据修改的结果。
对于并发场景下的事务,会出现以下的问题:(假设同时有事务A、B)
(1)更新丢失(Lost Update):A、B同时修改同一行数据,而且彼此都不知道彼此的存在,那么最后修改的事务就会覆盖其他事务所做的更新;在一个事务同一行进行更新操作时,其他事务被阻塞,即加行锁即可避免此种情况发生;(一句话:后一个更新覆盖率前一个更新)
(2)脏读(Dirty Reads):事务A正在修改一条数据,但是事务A未提交;此时事务B读到了A正在修改的数据,并且根据这条修改的数据继续做处理,但是事务A又回滚了,造成了“脏读”;(一句话:A读到了B已修改但未提交的数据)
(3)不可重复读(Non-Repeatable Reads):事务A在读取一条数据,同时事务B在修改同一条数据;此时事务A又去读取一遍此数据,并且A读取到了B修改的数据,发现数据和之前不一样了,即在同一个事务中每次读取的数据不一致,造成了“不可重复读”;(一句话:A读到了B已修改且已提交的数据)
(4)幻读(Phanton Reads):事务A在读取一批数据,同时事务B在这批数据中添加了一条数据;此时事务A又去读取一遍此批数据,并且A读取到了B增加的数据,发现数据比之前多了,此时如果B又回滚了,这条数据不存在了,但是A还是读到了,造成了“幻读”。(一句话:A读到了B新增且未提交的数据)
为了解决以上因为事务隔离性而出现的问题,MySQL利用了隔离级别来解决。各隔离级别如下:
读数据一致性 | 脏读 | 不可重复读 | 幻读 | |
---|---|---|---|---|
未提交读 | 最低级别,只能保证不读取物理上损坏的数据 | 会 | 会 | 会 |
已提交读 | 语句级 | 不会 | 会 | 会 |
可重复读 | 事务级 | 不会 | 不会 | 会 |
可序列化 | 最高级别,事务级 | 不会 | 不会 | 不会 |
MySQL的事务隔离级别越严格,并发的副作用越小,但是付出的代价越大,因为事实上串行化和并发是互相矛盾的。根据不同应用对事务隔离程度不同,我们可以选择不同的隔离级别。
【注】InnoDB实现的可重复读隔离级别避免了幻读问题。
事务的ACID特性是如何实现的呢?首先我们需要知道三个技术:日志文件(redo log和undo log)、锁机制和MVCC多版本并发控制。
【1】保证持久性(redo log):
redo log称为重做日志,它用来实现事务的持久性。 redo log由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。
MySQL的数据是存放在磁盘中的,但是如果每次读写数据都需要直接和磁盘交互,那么效率将会大大降低;而利用redo log buffer就可以大大提高效率,redo log buffer中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从buffer中读取,如果buffer中没有,则从磁盘读取后放入buffer;当向数据库写入数据时,会首先写入buffer,buffer中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
但是如果MySQL宕机,而此时redo log buffer中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。所以引入redo log来解决此问题:当数据修改时,除了修改buffer中的数据,还会在redo log记录这次操作;而且redo log采用的是WAL(Write-ahead logging,预写式日志),即所有修改先写入日志,再更新到buffer,这样就保证了数据不会因MySQL宕机而丢失;最后当事务提交时,redo log进行刷盘,如果MySQL宕机,重启时就可以读取redo log中的数据,对数据库进行恢复,从而满足了持久性要求。
【注意】那么既然redo log也是在事务提交时将日志写入磁盘,为什么它比直接将buffer中修改的数据写入磁盘(即刷脏)要快呢?有以下2个原因:
(1)刷脏是随机I/O,因为每次修改的数据位置随机;但写redo log是追加操作,属于顺序对磁盘进行I/O操作(类似Kafka中的存储机制)。
(2)刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效I/O被大大减少。
【2】保证原子性(undo log,它也是保证隔离性的基础):
undo log 称为回滚日志,它用来实现事务的原子性。 当事务对数据库进行修改时,会生成对应的undo log;如果事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
undo log属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚操作时,会根据undo log的内容做与之前相反的工作:即对于“insert”,做“delete”操作;对于“delete”,做“insert”操作;对于“update”(相当于先删除再插入),做一个相反的“update”操作。
【3】保证隔离性(MVCC或者锁机制都能保证):
隔离性分为2个方面:
(1)(一个事务)写操作对(另一个事务)写操作的影响:锁机制保证隔离性(行锁或者说“悲观锁”对写操作的排他性);
(2)(一个事务)写操作对(另一个事务)读操作的影响:MVCC保证隔离性(“乐观锁”的特性)。
锁机制在前面讲过不再赘述,下面我们介绍一下MVCC。MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。
下面的例子很好的体现了MVCC的特点:在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)——在T5时刻,事务A和事务C可以读取到不同版本的数据,如下:
MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。InnoDB实现MVCC,多个版本的数据可以共存,主要基于以下技术及数据结构:
(1)隐藏列:InnoDB中每行数据都有隐藏列,隐藏列中包含了本行数据的事务id(递增的)、指向undo log的指针等;
(2)基于undo log的版本链:上面面说到每行数据的隐藏列中包含了指向undo log的指针,而每条undo log也会指向更早版本的undo log,从而形成一条版本链(可用于读取之前的数据);
(3)ReadView:通过隐藏列和版本链,MySQL可以将数据恢复到指定版本;但是具体要恢复到哪个版本,则需要根据ReadView来确定;所谓ReadView,是指事务(记做事务A)在某一时刻给整个事务系统(trx_sys)打快照,之后再进行“读操作时”,会将读取到的“数据中(隐藏的那个)的事务id”与trx_sys快照比较,从而判断数据对该ReadView是否可见,即对事务A是否可见。
【注】“在可重复读隔离级别中”,ReadView生成的时机是在读操作之前;如果下次还接着读操作,则同样利用上次读操作生成的ReadView来判断可见性。
trx_sys中的主要内容,以及判断可见性的方法如下:
(1)low_limit_id:表示生成ReadView时系统中应该分配给下一个事务的id。如果数据的事务id“大于等于”low_limit_id,则对该ReadView“不可见”;(相当于拍了“快照”之后才有此事务)
(2)up_limit_id:表示生成ReadView时当前系统中“活跃”的读写事务中最小的事务id。如果数据的事务id“小于”up_limit_id,则对该ReadView“可见”;(相当于拍了“快照”之前已经有此事务)
(3)rw_trx_ids:表示生成ReadView时当前系统中“活跃”的读写事务的事务id列表。如果数据的事务id在“low_limit_id和up_limit_id”之间,则需要判断事务id是否在rw_trx_ids中:如果在,说明生成ReadView时事务仍在活跃中(没提交),因此数据对ReadView不可见(因为事务之间要隔离);如果不在,说明生成ReadView时事务已经提交了,因此数据对ReadView可见。
下面以可重复读隔离级别为例,结合上面提到的问题分别说明:
(1)避免脏读:
当事务A在T3时刻读取zhangsan的余额前,会生成ReadView,由于此时事务B没有提交仍然“活跃”,因此其事务id一定在ReadView的rw_trx_ids中,因此根据前面介绍的规则,事务B的修改对ReadView不可见。接下来,事务A根据指针指向的undo log查询上一版本的数据,得到zhangsan的余额为100。这样事务A就避免了脏读。
(2)避免不可重复读:
当事务A在T2时刻读取zhangsan的余额前,会生成ReadView。此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的rw_trx_ids中;另一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。
当事务A在T5时刻再次读取zhangsan的余额时,会根据“T2时刻”生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见;因此事务A根据指针指向的undo log查询上一版本的数据,得到zhangsan的余额为100,从而避免了不可重复读。
(3)避免幻读:
MVCC避免幻读的机制与避免不可重复读非常类似。
同样当事务A在T2时刻读取0<id<5的用户余额前,会生成ReadView。此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的rw_trx_ids中;一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。
当事务A在T5时刻再次读取0<id<5的用户余额时,会根据T2时刻生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见。因此对于新插入的数据lisi(id=2),事务A根据其指针指向的undo log查询上一版本的数据,发现该数据并不存在,从而避免了幻读。
接着进行一些简单的扩展:
(1)对于可重复读(RR)和已提交读(RC)实现隔离的反射方式如下:
<1>RR:在事务开始后第一次执行select前创建ReadView,直到事务提交都不会再创建。所以可以避免脏读、不可重复读和幻读。
<2>RC:每次执行select前都会重新建立一个新的ReadView,因此如果事务A第一次select之后,事务B对数据进行了修改并提交,那么事务A第二次select时会重新建立新的ReadView,因此事务B的修改对事务A是可见的。因此RC隔离级别可以避免脏读,但是无法避免不可重复读和幻读。
(2)按照是否加锁,MySQL的读可以分为两种:
<1>非加锁读:也称作快照读、一致性读,使用普通的select语句,这种情况下使用MVCC避免了脏读、不可重复读、幻读,保证了隔离性;
<2>加锁读:加锁读的查询语句有所不同,如下所示:
加锁读在查询时会对查询的数据加锁(共享锁或排它锁)。由于锁的特性,当某事务对数据进行加锁读后,其他事务无法对数据进行写操作,因此可以避免脏读和不可重复读。而避免幻读,则需要通过next-key lock。next-key lock是行锁的一种,实现相当于record lock(记录锁) + gap lock(间隙锁);其特点是不仅会锁住记录本身(record lock的功能,这样可以避免脏读和不可重复读),还会锁定一个范围(gap lock的功能,这样可以避免幻读)。因此,加锁读同样可以避免脏读、不可重复读和幻读,保证隔离性。
总的来说,InnoDB实现的RR,通过锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)等,实现了一定程度的隔离性,可以满足大多数场景的需要。
【4】保证一致性(之前的持久性、原子性和隔离性共同保证):
一致性是事务追求的最终目标:前面提到的原子性、持久性和隔离性,都是为了保证数据库状态的一致性。当然在分布式系统中,分布式事务的一致性也是需要保证的,这就需要根据我们业务对一致性的需求来实现了。
Spring中利用@Transactional注解来启用事务,以下是@Transactional各参数的说明:
上述有一个参数叫propagation,即事务的传播性,是指加上@Transactional注解的方法和其调用或被调用的方法上的事务是否也会被“传播”到。propagation有以下几个值:
以上参数中,依次简单说明如下:
(1)不管怎么样,都要在一个事务中;
(2)不管是否加上@Transactional,都没有事务;
(3)不管怎么样,都要新建一个事务;
(4)该方法必须在一个事务中,否则抛异常;
(5)该方法必须不在一个事务中,否则抛异常;
(6)不常用;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。