美文网首页
MySQL事务隔离级别及MVCC详解

MySQL事务隔离级别及MVCC详解

作者: 王月亮17 | 来源:发表于2024-03-18 21:33 被阅读0次

    MySQL的事务隔离级别及特性如下:

    SERIALIZABLE(串行化):无问题,性能差;
    REPEATABLE-READ(可重复读):幻读,默认隔离级别;
    READ-COMMITTED(已提交读):不可重复读&幻读;
    READ-UNCOMMITTED(未提交读):脏读&不可重复读&幻读。

    串行化就是事务按顺序执行,没有并行,自然也就没有问题。但是性能就会比较差;
    可重复读是MySQL默认的隔离级别,就是一个事务中多次读取数据,读到的数据都是一致的,不受其他事务修改的影响;
    已提交读就是可以读到其他事务已经提交的数据,所以就没有了可重复读的特性;
    未提交读能读到其他事务已执行但是未提交的数据,这就会产生脏读。

    数据库事务隔离级别的查看和修改

    -- 查看(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事务:

    start transaction;
    select * from table where id = 2; //此时表中无id为2的数据
    

    B事务:

    start transaction;
    insert into table(id, name) values(2, 'Wang');
    commit;
    

    A事务:

    select * from table where id = 2; // 此时虽然表中已经有了,但是由于可重复读的特性,同一事务读到的数据是一致的,所以还是读不到
    insert into table(id, name) values(2, 'Wang'); //报错
    commit;
    

    已提交读出现不可重复读的情况

    A事务:

    start transaction;
    select * from table where id = 2; // 此时可以看到2的name是'Wang'
    

    B事务:

    start transaction;
    update table set name = 'Yan' where id = 2;
    commit;
    

    A事务:

    select * from table where id = 2; // 此时2的name变成了'Yan',这样就出现了不可重复度的问题
    commit;
    

    已提交读出现幻读的情况

    A事务:

    start transaction;
    select * from table where id = 3; // 此时没有id为3的数据,
    

    B事务:

    start transaction;
    insert into table(id, name) values(3, 'Li'); // 只插入不提交
    

    A事务:

    select * from table where id = 3; // 此时还是读不到3的数据
    insert into table(id, name) values(3, 'Li'); // 此时插入数据就会有问题,因为B事务还没提交,但是已经执行了,A事务只能等待,看B事务是回滚还是提交
    

    未提交读出现不可重复读和脏读的情况

    A事务:

    start transaction;
    select * from table where id = 2; // 此时读到2的name为'Yan'
    

    B事务:

    start transaction;
    update table set name = 'Sun' where id = 2; // 只执行不提交
    

    A事务:

    select * from table where id = 2; // 此时读到2的name为'Sun',这就已经出现了不可重复读。假设B事务现在回滚了,那么这个数据就是错的,就出现了脏读。
    

    为提交读出现幻读的情况

    A事务:

    start transaction;
    select * from table; // 查出全表数据为[1-'Wu'], [2-'Sun'], [3-'Li']
    

    B事务:

    delete from table where id = 3; // 删除了但是没提交
    

    A事务:

    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生成前提交的最新记录,返回即可。有以下几种情况:

    1. 被访问数据的事务id小于min_trx_id,返回。
    2. 被访问数据的事务id大于等于max_trx_id,不返回。
    3. 被访问数据的事务id在m_ids中,不返回。
    4. 被访问数据的事务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。
    这时就该从版本链中开始寻找数据:

    1. id为1的数据目前的trx_id为100,100在m_ids中,说明那个事务在当前事务开始的时候还没执行完,不应该读出来,继续向下寻找。
    2. 此时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。
    从版本链中开始寻找数据:

    1. id为1的数据目前的trx_id为200,200在m_ids中,继续向下寻找。
    2. 下面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。
    这时就该从版本链中开始寻找数据:

    1. id为1的数据目前的trx_id为100,100在m_ids中,说明那个事务在当前事务开始的时候还没执行完,不应该读出来,继续向下寻找。
    2. 此时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。
    从版本链中开始寻找数据:

    1. id为1的数据目前的trx_id为200,200在m_ids中,继续向下寻找。
    2. 下面roll_pointer指向的上一条记录trx_id为100,还是在m_ids中,继续向下寻找。
    3. 再一条记录trx_id为80,小于min_trx_id,所以返回了最初的'Wang'。

    相关文章

      网友评论

          本文标题:MySQL事务隔离级别及MVCC详解

          本文链接:https://www.haomeiwen.com/subject/rklzzdtx.html