MySQL的事务隔离级别及特性如下:
SERIALIZABLE(串行化):无问题,性能差;
REPEATABLE-READ(可重复读):幻读,默认隔离级别;
READ-COMMITTED(已提交读):不可重复读&幻读;
READ-UNCOMMITTED(未提交读):脏读&不可重复读&幻读。
串行化就是事务按顺序执行,没有并行,自然也就没有问题。但是性能就会比较差;
可重复读是MySQL默认的隔离级别,就是一个事务中多次读取数据,读到的数据都是一致的,不受其他事务修改的影响;
已提交读就是可以读到其他事务已经提交的数据,所以就没有了可重复读的特性;
未提交读能读到其他事务已执行但是未提交的数据,这就会产生脏读。
数据库事务隔离级别的查看和修改
sql
-- 查看(5.7)
select @@global.tx_isolation;
select @@tx_isolation;
-- 查看(8)
select @@global.transaction_isolation;
select @@transaction_isolation;
-- 修改
set session transaction isolation level serialiable/repeatable read/......
可重复读出现幻读的情况
A事务:
sql
start transaction;
select * from table where id = 2; //此时表中无id为2的数据
B事务:
sql
start transaction;
insert into table(id, name) values(2, 'Wang');
commit;
A事务:
sql
select * from table where id = 2; // 此时虽然表中已经有了,但是由于可重复读的特性,同一事务读到的数据是一致的,所以还是读不到
insert into table(id, name) values(2, 'Wang'); //报错
commit;
已提交读出现不可重复读的情况
A事务:
sql
start transaction;
select * from table where id = 2; // 此时可以看到2的name是'Wang'
B事务:
sql
start transaction;
update table set name = 'Yan' where id = 2;
commit;
A事务:
sql
select * from table where id = 2; // 此时2的name变成了'Yan',这样就出现了不可重复度的问题
commit;
已提交读出现幻读的情况
A事务:
sql
start transaction;
select * from table where id = 3; // 此时没有id为3的数据,
B事务:
sql
start transaction;
insert into table(id, name) values(3, 'Li'); // 只插入不提交
A事务:
sql
select * from table where id = 3; // 此时还是读不到3的数据
insert into table(id, name) values(3, 'Li'); // 此时插入数据就会有问题,因为B事务还没提交,但是已经执行了,A事务只能等待,看B事务是回滚还是提交
未提交读出现不可重复读和脏读的情况
A事务:
sql
start transaction;
select * from table where id = 2; // 此时读到2的name为'Yan'
B事务:
sql
start transaction;
update table set name = 'Sun' where id = 2; // 只执行不提交
A事务:
sql
select * from table where id = 2; // 此时读到2的name为'Sun',这就已经出现了不可重复读。假设B事务现在回滚了,那么这个数据就是错的,就出现了脏读。
为提交读出现幻读的情况
A事务:
sql
start transaction;
select * from table; // 查出全表数据为[1-'Wu'], [2-'Sun'], [3-'Li']
B事务:
sql
delete from table where id = 3; // 删除了但是没提交
A事务:
sql
select * from table; // 查出全表数据为[1-'Wu'], [2-'Sun'],查到3被删除了。此时B事务回滚,数据3又出来了,一会有一会没有,就出现了幻读。
MVCC
MVCC即多版本并发控制(Multi-Version Concurrent Control)。
使用可重复读和已提交读这两种隔离级别时,SELECT操作版本链的过程。
MySQL数据行中存在隐藏的3个列:row_id、trx_id、roll_pointer
row_id如果表中有主键,或者有非NULL唯一键,就不需要row_id。没有则会自动生成6个字节的row_id。
trx_id为事务id,每次开启事务都会生成,记录在数据行中。
roll_pointer则是会指向undo_log中的上一次更新的数据。更新多次就会形成版本链。
SELECT操作会生成ReadView,已提交读同一个事务中对相同数据的查询每次SELECT都会生成一个,而可重复读一个事务多个SELECT只会生成一个ReadView。
ReadView中有m_ids、min_trx_id、max_trx_id、creator_trx_id。
m_ids:生成ReadView时所有未提交的事务id。
min_trx_id:m_ids的最小值。
max_trx_id:生成ReadView时,系统应该分配给下一个事务的事务id。
creator_trx_id:生成ReadView的事务的事务id,如果只有SELECT则为0。
当SELECT生成ReadView后,从当前B+树中的数据开始查,顺着版本链从上往下查,知道找到ReadView生成前提交的最新记录,返回即可。有以下几种情况:
- 被访问数据的事务id小于min_trx_id,返回。
- 被访问数据的事务id大于等于max_trx_id,不返回。
- 被访问数据的事务id在m_ids中,不返回。
- 被访问数据的事务id在min_trx_id和max_trx_id之间,且不在m_ids中,返回。
已提交读举例
有数据[1-Wang], [2-Yan], [3-Li],插入事务id均为80。
A事务id为100更新1 = Sun,不提交。
B事务id为200更新其他表,不提交。
C事务读id为1的数据,生成ReadView,m_ids=[100, 200],min_trx_id=100, max_trx_id=201,creator_trx_id=0。
这时就该从版本链中开始寻找数据:
- id为1的数据目前的trx_id为100,100在m_ids中,说明那个事务在当前事务开始的时候还没执行完,不应该读出来,继续向下寻找。
- 此时roll_pointer指向的上次undo_log记录trx_id为80,80小于min_trx_id,说明这个事务在生成ReadView之前已经提交了,那就会读到'Wang'。
A事务提交。
B事务更新1 = Liu,不提交。
C事务读id为1的数据,再次生成ReadView,m_ids=[200],min_trx_id=200,max_trx_id=201,creator_trx_id=0。
从版本链中开始寻找数据:
- id为1的数据目前的trx_id为200,200在m_ids中,继续向下寻找。
- 下面roll_pointer指向的上一条记录trx_id为100,小于min_trx_id,所以返回了A事务修改的'Sun'。
可重复读举例
有数据[1-Wang], [2-Yan], [3-Li],插入事务id均为80。
A事务id为100更新1 = Sun,不提交。
B事务id为200更新其他表,不提交。
C事务读id为1的数据,生成ReadView,m_ids=[100, 200],min_trx_id=100, max_trx_id=201,creator_trx_id=0。
这时就该从版本链中开始寻找数据:
- id为1的数据目前的trx_id为100,100在m_ids中,说明那个事务在当前事务开始的时候还没执行完,不应该读出来,继续向下寻找。
- 此时roll_pointer指向的上次undo_log记录trx_id为80,80小于min_trx_id,说明这个事务在生成ReadView之前已经提交了,那就会读到'Wang'。
A事务提交。
B事务更新1 = Liu,不提交。
C事务读id为1的数据,由于是可重复读隔离级别,所以不再生成新的ReadView,还是用之前的,m_ids=[100, 200],min_trx_id=100, max_trx_id=201,creator_trx_id=0。
从版本链中开始寻找数据:
- id为1的数据目前的trx_id为200,200在m_ids中,继续向下寻找。
- 下面roll_pointer指向的上一条记录trx_id为100,还是在m_ids中,继续向下寻找。
- 再一条记录trx_id为80,小于min_trx_id,所以返回了最初的'Wang'。