1.隔离级别
1.1.理论
1.1.1.序列化(SERIALIZABLE)
如果隔离级别为序列化,则用户之间通过一个接一个顺序地执行当前的事务
,这种隔离级别提供了事务之间最大限度的隔离;
1.1.2.可重复读(REPEATABLE READ,MySQL默认的隔离级别)
在可重复读在这一隔离级别上,事务不会被看成是一个序列
.不过,当前正在执行事务的变化仍然不能被外部看到
,也就是说,如果用户在另外一个事务中执行同条SELECT语句数次
,结果总是相同的.(因为正在执行的事务所产生的数据变化不能被外部看到);
1.1.3.读已提交(READ COMMITTED)
READ COMMITTED隔离级别的安全性比REPEATABLE READ隔离级别的安全性要差
.处于READ COMMITTED级别的事务可以看到其他事务对数据的修改
.也就是说,在事务处理期间,如果其他事务修改了相应的表,那么同一个事务的多个 SELECT语句可能返回不同的结果
;
1.1.4.读未提交(READ UNCOMMITTED)
READ UNCOMMITTED提供了事务之间最小限度的隔离
.除了容易产生虚幻的读操作和不能重复的读操作
外,处于这个隔离级别的事务可以读到其他事务还没有提交的数据
,如果这个事务使用其他事务未提交的变化作为计算的基础,然后那些未提交的变化被它们的父事务撤销,这就导致了大量的数据变化;
1.2.实践
1.2.1.查看隔离级别
1>.MySQL8.0之前,
通过如下命令查看全局隔离级别和当前session的隔离级别:
SELECT @@GLOBAL.tx_isolation, @@tx_isolation;
2>.MySQL8.0开始,
通过如下命令查看MySQL默认的隔离级别以及当前session隔离级别:
SELECT @@GLOBAL.transaction_isolation, @@transaction_isolation;
3>.修改当前session隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
注意:只是修改了当前session的隔离级别,其他session的隔离级别还是默认的!
1.2.2.读未提交
1.2.2.0.准备测试数据
读未提交是
最小限度的隔离级别
,这种隔离级别中存在脏读,不可重复读以及幻读等
问题;
1.2.2.1.脏读
一个事务读到另一个事务还没有提交的数据,称之为脏读;
1>.首先打开两个SQL操作窗口,假设分别为A和B,在A窗口
中输入如下几条SQL (注意:输入完成后不用执行
);
START TRANSACTION;
UPDATE account set balance=balance+100 where name='zhangsan';
UPDATE account set balance=balance-100 where name='lisi';
COMMIT;
2>.在B窗口
执行如下SQL,修改默认的事务隔离级别为READ UNCOMMITTED,如下:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
3>.在B窗口
中输入如下SQL,输入完成后,首先执行第一行开启事务(注意:只需要执行一行即可
):
START TRANSACTION;
SELECT * from account;
COMMIT;
4>.接下来执行A窗口
中的前两条SQL
,即开启事务,给zhangsan这个账户添加100元(事务未提交):
5>. 进入到B窗口
,执行B窗口的第二条查询SQL
(SELECT * from user;),结果如下:
可以看到,A窗口中的事务,虽然还未提交,但是B窗口中已经可以查询到数据的相关变化了,这就是脏读的问题;
1.2.2.2.不可重复读
不可重复读是指一个事务先后读取同一条记录,但是两次读取的数据不同,称之为不可重复读;
1>.首先打开两个查询窗口A和B,并且将B窗口的数据库事务隔离级别设置为READ UNCOMMITTED
,再将数据恢复到原始状态:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
2>.在B窗口
中输入如下SQL,然后只执行前两条SQL开启事务并查询zhangsan的账户
:
START TRANSACTION;
SELECT * from account where name='zhangsan';
COMMIT;
3>.在A窗口
中执行如下SQL,给zhangsan这个账户添加100块钱,如下:
START TRANSACTION;
UPDATE account set balance=balance+100 where name='zhangsan';
COMMIT;
4>.再次回到B窗口
,执行B窗口的第二条SQL查看zhangsan的账户
,结果如下:
可以看到,此时zhangsan的账户已经发生了变化,即前后两次查看zhangsan账户,结果不一致,这就是不可重复读;
脏读和不可重复读的区别:
脏读是看到了其他事务未提交的数据,而不可重复读是看到了其他事务已经提交的数据(由于当前SQL也是在事务中,因此有可能并不想看到其他事务已经提交的数据);
1.2.2.3.幻读
幻读和不可重复读非常像,看名字就是产生幻觉了;
1>.首先打开两个查询窗口A和B,并且将B窗口的数据库事务隔离级别设置为READ UNCOMMITTED
,再将数据恢复到原始状态:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
2>.在A窗口
中输入如下SQL:
START TRANSACTION;
insert into account(id,name,balance) values(3,'wangwu',1000);
COMMIT;
3>.在B窗口
输入如下SQL:
START TRANSACTION;
SELECT * from account;
delete from account where name='wangwu';
COMMIT;
4>.执行步骤:
①.执行
B窗口
的前两行SQL,开启一个事务,同时查询数据库中的数据,此时可以查询到原始的两个用户;②.执行
A窗口
的前两行,向数据库中插入一个名为wangwu的用户,注意不用提交事务;③.执行
B窗口
的第二行,由于脏读的问题,此时可以查询到wangwu的用户;④.执行
B窗口
的第三行,去删除name为wangwu的记录,这个时候删除就会出现问题,虽然可以在B窗口中查询到wangwu,但是这条记录还没有提交,是因为脏读的原因才看到,所以无法删除(一直等待).此时就产生了幻觉,明明可以看到wangwu这条数据,但是却无法删除,这就是幻读;
1.2.3.读已提交
和读未提交相比,读已提交这种隔离级别
主要解决了脏读的问题
,对于不可重复读和幻读则未解决;
将事务的隔离级别改为"READ COMMITTED"之后,重复上面关于脏读案例的测试,发现已经不存在脏读的问题了;再重复上面关于不可重复读案例的测试,发现不可重复读的问题依然存在;
1.2.3.1.幻读
1>.打开两个窗口A和B,将B窗口的隔离级别改为READ COMMITTED
,再将数据恢复到原始状态:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
2>.在A窗口
输入如下SQL:
START TRANSACTION;
insert into account(id,name,balance) values(3,'wangwu',1000);
COMMIT;
3>.在B窗口
输入如下SQL:
START TRANSACTION;
SELECT * from account;
insert into account(id,name,balance) values(3,'wangwu',1000);
COMMIT;
4>.执行步骤:
①.执行
B窗口
的前两行SQL,开启事务并查询数据,此时可以查到原始的两个用户;②.执行
A窗口
的前两行SQL,插入一条记录,但是并不提交事务;③.执行
B窗口
的第二行SQL,由于现在已经没有了脏读的问题,所以此时查不到在A窗口添加的数据;④.执行
B窗口
的第三行SQL,由于name字段唯一,因此这里会出现无法插入的情况(一直等待).此时就产生幻觉了,明明没有wangwu这个用户,却无法插入wangwu的数据,这就是幻读;
1.2.4.可重复读(InnoDB引擎默认的数据库事务隔离级别)
和读已提交相比,
可重复读进一步解决了不可重复读的问题
,但是幻读则未解决;
1>.可重复读中关于幻读的测试和上一小节基本一致,不同的是第二步中执行完插入SQL之后需要提交事务;
2>.由于可重复读已经解决了不可重复读的问题,因此第二步即使提交了事务,第三步也查不到已经提交的数据,第四步继续插入仍然不成功;
1.2.5.序列化
1>.序列化提供了事务之间最大限度的隔离,在这种隔离级别中,事务一个接一个顺序的执行,不会发生脏读,不可重复读以及幻象读的问题,最安全
;
2>.如果设置当前事务的隔离级别为序列化,那么此时开启其他事务时就会阻塞,
必须等待当前事务提交了,其他事务才能开启成功,因此前面的脏读,不可重复读以及幻象读的问题这里都不会发生
;
1.3.总结
总的来说,隔离级别和脏读,不可重复读以及幻象读的对应关系如下:
性能关系图如下:
2.快照读和当前读
2.1.快照读
1>.快照读(SnapShot Read)是一种一致性不加锁的读,是InnoDB存储引擎并发如此之高的核心原因之一
;
2>.在默认的可重复读的隔离级别下,事务开启的时候,就会针对当前库生成一个快照,快照读读取到的数据要么就是生成快照时的数据,要么就是当前事务自身插入/修改过的数据;
3>.日常所用的不加锁的查询,包括上一小节中涉及到的所有查询,都属于快照读
;
2.2.当前读
与快照读相对应的就是当前读,当前读就是读取最新的数据,而不是历史数据
,换言之,在可重复读的隔离级别下,如果使用了当前读,也可以读到其他事务已经提交的数据
;
2.2.1.案例
1>.MySQL事务开启两个会话窗口A和B;
2>.首先在A窗口
中开启事务并查询id为1的记录:
BEGIN;
SELECT * FROM account WHERE id = 1;
3>.在B窗口
中对id为1的数据进行修改:
UPDATE account SET balance = 1000 WHERE id = 1;
注意: B会话中不要开启事务或者开启了事务要及时提交,否则update语句会占用一把排它锁导致之后在A会话中用锁时发生阻塞;
4>.回到A窗口
继续做查询操作:
SELECT * FROM account WHERE id = 1;
SELECT * FROM account WHERE id = 1 FOR UPDATE;
SELECT * FROM account WHERE id = 1 LOCK in SHARE MODE;
可以看到,A会话中第一个查询是快照读,读取到的是当前事务开启时的数据状态,后面两个查询则是当前读,读取到了当前最新的数据,即B会话中修改后的数据;
3.undo log日志
1>.数据库事务有回滚的能力, 既然能够回滚,那么就必须要在数据改变之前先把旧的数据记录下来,作为将来回滚的依据,那么这个记录就是undo log日志
;
2>.当我们要添加一条记录的时候,就把添加的数据id记录到undo log中,将来回滚的时候就据此把(添加的)数据删除;当我们要删除或者修改数据的时候,就把原数据记录到undo log中,将来据此恢复数据.查询操作因为不涉及到回滚操作,所以就不需要记录到undo log中;
3>.redo log日志和undo log日志区别:
1>.
redo log: 记录的是物理级别上的页修改操作
,比如页号,偏移量,写入的数据,主要是为了保证数据的可靠性(持久化)
;2>.
undo log: 记录的是逻辑操作日志
,比如对某一行数据进行了insert操作,那么undo log就记录一条与之相反的delete操作.主要用于事务的回滚和一致性非锁定读
;
4.行格式
1>.行格式就是InnoDB存储引擎在保存每一行数据的时候,究竟是以什么样的格式来保存这行数据
;
2>.数据库中的行格式有好几种,例如COMPACT、REDUNDANT、DYNAMIC、COMPRESSED等,不过无论是哪种行格式,都绕不开以下几个隐藏的数据列:
上图中的列1,列2,列3…列N,就是我们数据库中表的列,保存着我们正常的数据,除了这些保存数据的列之外,还有三列额外加进来的数据,这也是我们需要重点关注的DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR三列;
①.DB_ROW_ID: 该列占用6个字节,是一个行ID,用来唯一标识一行数据.如果用户在创建表的时候没有设置主键,那么系统会根据该列建立主键索引;
②.DB_TRX_ID: 该列占用6个字节,是一个事务ID.在InnoDB存储引擎中,当我们要开启一个事务的时候,会向InnoDB的事务系统申请一个事务id,这个事务id是一个严格递增且唯一的数字,当前数据行是被哪个事务修改的,就会把对应的事务id记录在当前行中;
③.DB_ROLL_PTR: 该列占用7个字节,是一个回滚指针,这个回滚指针指向一条undo log日志的地址,通过这个undo log日志可以让这条记录恢复到前一个版本;
5.MVCC
1>.MVCC(Multi-Version Concurrency Control)多版本并发控制
,其核心思想就是保存数据行的历史版本,通过对数据行的多个版本进行管理来实现数据库的并发控制
;
2>.简单来说,我们平时看到的一条一条的记录,在数据库中保存的时候,可能不仅仅只有一条记录,而是有多个历史版本;
如图:
下面结合不同的隔离级别来说这张图;
5.1.可重复读
1>.首先,当我们通过INSERT/DELETE/UPDATE去操作一行数据的时候,就会产生一个事务ID,这个事务ID也会同时保存在行记录中(DB_TRX_ID)
,也就是说,当前数据行是哪个事务修改后得到的,是有记录的
;
2>.INSERT/DELETE/UPDATE操作都会产生对应的undo log日志
,每一行记录都有一个DB_ROLL_PTR指向undo log日志,每一行记录,通过执行undo log日志,就可以恢复到前一个记录,前前记录,前前前记录…;
3>.当我们开启一个事务的时候,首先会向InnoDB的事务系统申请一个事务id
,这个事务id是一个严格递增的数字,在当前事务开启的一瞬间系统会创建一个数组,数组中保存了目前所有的活跃事务id,所谓的活跃事务就是指已开启但是还没有提交的事务
;
这个数组中的最小值好理解就是本次第一个申请的事务id,但是最大值并不一定是当前事务的id.因为从申请好trx_id到创建数组这个过程是需要时间的,期间可能有其他会话也申请到了trx_id;
4>.如果当前事务想要去查看某一行数据,会先去查看该行数据的DB_TRX_ID:
①.如果这个值等于当前事务id,说明这就是当前事务修改的,那么这行数据对当前事务可见;
②.如果这个值小于数组中的最小值,说明当我们开启当前事务的时候,这行数据修改所涉及到的事务已经提交了,当前数据行对当前事务是可见的;
③.如果这个值大于数组中的最大值,说明这行数据是我们在开启事务之后,还没有提交的时候,有另外一个会话也开启了事务.并且修改了这行数据,那么此时这行数据对当前事务就是不可见的;
④.如果这个值的大小介于数组中最大值与最小值之间(闭区间),且该值不在数组中,说明这也是一个已经提交的事务修改的数据,这行数据对于当前事务也是可见的;
⑤.如果这个值的大小介于数组中最大值与最小值之间(闭区间),且该值存在数组中(不等于当前事务id),说明这是一个未提交的事务修改的数据,那么该行数据对当前事务是不可见的;
例如:
我们有 A、B、C、D 四个会话,首先A、B、C分别开启一个事务,事务ID是3、4、5,然后C会话提交了事务,A、B未提交.接下来D会话也开启了一个事务,事务ID是6,那么当D会话开启事务的时候,数组中的值就是[3,4,6].现在假设有一行数据的DB_TRX_ID是5(第四种情况),那么该行数据对会话D的事务就是可见的(因为会话D事务开启的时候它已经提交了),如果有一行数据的DB_TRX_ID是4,那么该行数据对会话D的事务就不可见(因为还未提交);
注意: 如果当前事务中涉及到数据的更新操作,那么更新操作是在当前读的基础上更新的,而不是在快照读的基础上更新的,如果是后者(快照读)则有可能导致数据丢失;
5>.案例
①.开启两个会话窗口A和B,首先在A窗口
中开启事务:
BEGIN;
SELECT * FROM account;
②.在B窗口
中做一次修改操作(不用显式开启事务,更新操作MySQL内部会开启事务,更新完成后事务会自动提交):
UPDATE account SET balance = balance + 100 WHERE id = 1;
SELECT * FROM account;
③.回到A窗口
中,查询这行数据:
可以看到,该行数据并没有发生变化,符合预期(目前数据库事务隔离级别为可重复读);
④.在A窗口
中做一次修改操作,然后再去查询:
UPDATE account SET balance = balance + 200 WHERE id = 1;
SELECT * FROM account;
可以看到该行数据是基于当前读(最新数据)做的修改操作,符合预期.如果基于快照读的来做修改,那么会话B的修改就会丢失,这显然是不对的;
其实MySQL中的update就是先读再更新,读的时候默认就是当前读,即会加锁,所以在上面的案例中,如果会话B中显式的开启了事务并且没有提交事务,那么会话A中的update语句就会被阻塞;这就是MVCC,一行记录存在多个版本.实现了读写并发控制,读写互不阻塞;同时MVCC中采用了乐观锁,读数据不加锁,写数据只锁行,降低死锁概率,并且还能据此实现快照读
;
5.2.读已提交
读已提交和可重复读类似,区别主要是后者在每次事务开始的时候创建一致性视图(创建数组列出活跃事务id),而前者则每一个语句执行前都会重新算出一个新的视图
,所以可重复读这种隔离级别会看到别的会话已经提交的数据(即使别的会话比当前会话开启的晚);
6.小结
1>.MVCC在一定程度上实现了读写并发,不过它只在读已提交和可重复读两个隔离级别下有效
;
2>.读未提交总是会读取最新的数据行,序列化则会对所有读取的行都加锁,这两个隔离级别都和MVCC不兼容
;