美文网首页
数据库系列6-事务和锁

数据库系列6-事务和锁

作者: xgangzai | 来源:发表于2019-11-22 19:27 被阅读0次

    MySQL利用MVCC(MVCC又是依据undo log实现的),在一个可重复读的事务执行过程中,读取到的数据都是事务开始时获取的快照,实现了事务见的隔离。

    而在锁部分,MySQL更新一条数据会获取当前行的写锁,防止其他事务对当前行的并发更新。

    问题:如果获取到锁后,当前事务看到的结果是更新后的还是更新前的???

    结合下面案例,详细了解之

    背景:REPEATABLE_READ, autocommit=1

    表的定义如下

    CREATE TABLE `t` (
      `id` int(11) NOT NULL,
      `k` int(11) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB;
    insert into t(id, k) values(1,1),(2,2);
    
    事务A 事务B 事务 C
    start transaction with consistent snapshot;
    start transaction with consistent snapshot; update t set k=k+1 where id=1;
    update t set k=k+1 where id=1;
    select k from t where id=1;
    select k from t where id=1;
    commit;
    commit;

    在上面的流程中,事务A读到的是1;事务B读到的是3。根据MVCC,可重复读隔离级别下,事务A中读到的k是1,可以理解,同理,事务B中在更新时读到的也是1,更新后,再次读到应该是2才对呀。基于这个问题,再深入了解MVCC机制到底是怎么运作的。

    在MySQL中,有两个“视图”的概念:

    • 一个是view,用于查询时定义的虚拟表,创建视图的语法是create view;视图的查询方法与表的查询方法一致。

    • 另一个是InnoDB实现MVCC用到的一致性读视图,即consistent read view,用于实现在READ_COMMITED和REPEATABLE_READ隔离级别下,每次读到的都是指定的“快照”(read view)。

    InnoDB中,每个事务都有一个唯一的事务ID,叫transaction id,在事务开始的时候向InnoDB的事务系统申请的,严格递增。关于事务启动的时机:

    A autocommit=1

    1. 没有明确指定事务的开启,每一条SQL都有一个属于自己的完整的事务,即执行SQL的时候就启动了事务

    2. 明确指定了事务的开始,有以下两种情况

    • begin 或者 start transaction 后面第一条SQL执行时才会开启事务

    • start transacton with consistent snapshot 会立即开启事务

    B autocommit=0

    在连接建立后,就开启一个事务,直到执行了commit或者rollback,当前事务结束并自动开启一个新事务。

    每行数据除了业务字段,还有三个隐藏的字段

    row_id

    如果指定了主键,row_id是否就是主键??

    DB_TRX_ID

    产生当前版本的transaction id

    DB_ROLL_PTR

    指向undo log的指针,undo log中这行数据应该就是产生这次变更的反向操作

    细看undo log,根据官网介绍,undo log分为两部分,insert和update

    对于insert型,用于事务回滚,这条undo log在事务提交后就被删除了。

    对于update型,用于事务回滚和重建早期版本数据,当没有事务再需要这条log来构建早期版本数据,这条undo log会被删除

    undo log 的清理是通过后台线程 定时执行purge动作来清理的

    <font color="#0000CD">delete 在InnoDB内部当时并没有真正删除,只是打了个标记,后续在purge线程中才会被真正删除,所以delete本质上也是一条update</font>

    有了DB_TRX_ID, DF_POLL_PTR,就可以构造read view了,简单讲,事务在构建read view时,找出当前系统中活跃的事务ID,用数组有序存储。

    关于read view创建的时机,

    在REPEATABLE_READ级别下,事务开启第一次读创建,这样才能保证在整个事务周期内,每次看到的数据都和第一次读到的是一致的;

    在READ_COMMITED级别下,每个statement都会重新创建,这样才能实现如果有其他事务更新提交,当前事务能看到

    在READ_UNCOMMITED级别下,就不再创建了,每次都是读取最新版本的数据。(这样说的话,最新版本的数据并不是要提交后才生成的呢?关于这点,结合几种log buffer 详细了解一个写操作落地的完整流程)

    有了上面read view的数据模型后,当一个事务的read view构造好后续真正读取数据的时候,是怎么判断数据的某个版本对自己是否可见呢????

    再具体了解下read view,数组中最小的trx_id,称为低水位,记为Tidmin。最大的的trx_id, 称为高水位,记为Tidmax。数据的trx_id记为RowTid,当前事务的ID记为Tid

    1. RowTid=Tid,说明这次更新是自己产生的,可见

    2. RowTid<Tidmin,说明这次更新是已经提交的事务产生的,因为没在活跃事务中,说明事务已经提交了(至于回滚的场景再讨论)

    3. RowTid>Tidmax,说明这次更新是由创建read view后的事务产生的,不可见

    4. RowTid 大于Tidmin且小于Tidmax:分以下两种情况讨论

      4.1 在数组中,说明事务还未提交,不可见

      4.2 如果不在数组中了,其实跟场景3类似,说明那个事务执行地很快,在创建read view时就已经提交了,所以对当前事务可见。

    http://mysql.taobao.org/monthly/2015/12/01/

    当数据的当前版本不可见时,根据DB_ROLL_PTR会找对应的undo log,通过计算反向SQL得到前一版的值。

    拿到那条undo log的trx_id再判断,直至对当前事务可见或者没有前一版了。

    4.2 搞了半天都没理解到位 其实现在也不算是很清晰 数据库太复杂了。。。。

    其实理解上面的关键在于,read view的创建到后续的使用过程中,时间跨度可能会"很长",在这个期间就可能会发生一个完整的事务周期(开始到提交),这便是场景3

    对于场景4.2,在read view创建的时候,可能有一个”很久“之前就开启的事务,但是它后面已经有提交过得事务,所以才会出现有比最小事务ID大,但却不在read view中的事务。

    在更新SQL执行中,也会先读后写,这里的读是MySQL内部执行的逻辑,跟select读是两回事,写之前的读叫“当前读”,读的是最新值。如果不读最新的值,那就相当于忽略了其他事务的更新,就有可能会脏读。

    不管什么配置前提下,每条SQL都是在某一个事务中执行的,对于更新类SQL(insert update delete等)是要获取锁的,如果有有其他事务正持有这个锁,当前事务被阻塞。对于读类的SQL(select),默认是不加锁的,根据一般的隔离级别(REPEATABLE_READ),可能是导致读取的不是“最新的数据”,select也是支持加锁的,select for update获取的是X锁,select xxx lock in share mode获取的是S锁。

    再看“当前读”,我的理解为了读取到最新的数据,应该是要加锁。

    只读事务

    怎么指定只读事务,本质是什么,只读事务和不开启事务有何区别?

    set session transaction read only;//只读事务
    set session transaction read write;//读写事务
    

    在只读事务中,如果执行更新类SQL,会返回'Cannot execute statement in a READ ONLY transaction.'这是理所当然的,不然还叫什么只读事务。只读事务的存在意义是什么,仅仅只是为了防止在事务中有更新操作????

    如果指定事务是只读类型的,InnoDB内部会有些优化,比如不申请transaction id。

    锁和事务的产生背景

    MySQL采用的是One Thread per Connection(新版MySQL已支持线程池),这就产生了资源并发访问的问题,直接的解决方案就是加锁,将同一个资源的访问的访问串行化。根据实际使用经验,很多应用都是读多写少的场景,所以读和读互斥显得不是很必要。所以就将锁细化,分成读写锁,这样就避免了读和读之间的互斥,很大地提高了系统的并发能力。当然,读写、写写还是要互斥的。即便是支持并发读读了,那能不能进一步支持读写并发,即不管其他线程怎么修改,不影响当前线程的读。这就有了MVCC。

    SERIALIABLE 隔离级别下的select 和 select lock in share mode 有何异同

    按照SERIALIABLE串行化的隔离级别,应该是要加排它锁的才能保证串行,所以在满足功能性的前提下,优先使用共享锁

    这篇笔记写了两三天 感觉头都大了。。。 复杂

    相关文章

      网友评论

          本文标题:数据库系列6-事务和锁

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