美文网首页Java成长之路
手撕 MySQL 事务,发生了什么?

手撕 MySQL 事务,发生了什么?

作者: 路人甲java | 来源:发表于2020-05-06 14:59 被阅读0次
    image.png

    什么是事务?
    数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由开始和结束之间执行的全部数据库操作组成。

    这是百科的定义,也是我们在面试的时候最常回答的关键字。

    可以这么说:事务是数据库执行过程的一个逻辑单位,由一个有限的数据库操作序列组成。

    举例来说,当我们购物下订单时,有这么两个操作(当然不止这俩):付款,减库存。这两个操作序列就是一个事务,他不能拆分执行,必须同时成功和失败。

    当我们面试时最常遇到的问题是什么?
    或者不那么世俗的说——毕竟我们学习并不全是为了面试嘛,啊呸!不面试谁看那么多,脑子内存本来就不大。


    image.png

    事务的4大特性

    A (Atomicity) 原子性:事务的操作序列不可再拆分:这也是都成功都失败的意思。
    C (Consistent) 一致性:事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定。
    I(Isolation)隔离性:隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
    D(Durable)持久性:久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
    关于ACID抢眼回答:

    数据库的每一个操作其实是一条日志。

    原子性,在 InnoDB 里面是通过 undo log 来实现的,它记录了数据修改之前的值(逻辑日志),一旦发生异常,就可以用 undo log 来实现回滚操作。

    持久性怎么实现呢?数据库崩溃恢复(crash-safe)是通过什么实现的?

    是通过 redo log 和 double write 双写缓冲来实现的,我们操作数据的时候,会先写到内存的buffer-pool中,同时记录 redo log,如果在刷盘之前出现异常,在重启后就可以读取 redo log 的内容,写入到磁盘,保证数据的持久性。

    当然,恢复成功的前提是数据页本身没有被破坏,是完整的,如果数据页本身损坏了,这个可以通过双写缓冲 (double write)保证。

    说了这么多事务,你甚至可能在实际编程中并没有手动操作过事务,那么如何手动开启和关闭事务呢?


    image.png
    image.png

    Mysql数据库默认是开启自动提交事务的,也就是说当你执行update/delete/insert操作时数据库引擎会自动开启一个事务,并且在操作完成后自动提交事务。

    事务并发产生的问题

    脏读,读到了其他事务还没提交的数据
    不可重复读,读到了其他事务提交之后的数据,数据变化了(update/delete)
    幻读,读到了其他事务提交之后的数据,只有数据增加了行才叫幻读(insert)
    你可能会说,事务不是有隔离性吗,为什么还有这么多问题?好了继续往下看

    事务隔离级别

    Read Uncommited 未提交读,顾名思义,可以读到其他事务没有提交的数据,会产生脏读
    事务操作时并不隔离其他的事务,比如以下操作:


    image.png

    而如果此时事务1 回滚了事务,事务2就读到了脏数据;

    这种隔离机制在数据库实际使用中,显然不合适,但有助于理解其他事务。

    Read Commited 已提交读,只能读取其他事务已经提交的数据,不能读取到其他事务未提交的数据,它解决了脏读的问题, 但是会出现不可重复读的问题。
    事务只能读取其他事务提交之后的数据,我们来模拟以下这个过程会出现的问题:


    image.png

    可以看到,这种隔离级别会导致数据在一个事务两次读取的数据不一致,也就是不可重复读;

    Repeatable Read 可重复读,它解决了不可重复读的问题, 也就是在同一个事务里面多次读取同样的数据结果是一样的,但是在这个级别下,没有解决幻读的问题。
    这也是最常用的事务隔离机制,他可以保证在一个事务里读取的数据是一样的,也就是数据的可重复读。你可能会问,数据库是如何实现可重复读的?这个问题会在之后解答,先来看看什么是幻读。

    一个事务前后两次读取数据数据不一致,是由于其他事务插入数据造成的,这种情况我们把它叫做幻读。

    比如:


    image.png

    第二次查询我们发现多了一条数据,这就叫幻读。需要记住的是只有其他事务插入导致的数据不一致才叫幻读。

    Serializable(串行化),最后一个是串行的隔离级别,也是最严格的隔离级别,它要求事务必须排队执行,解决了幻读。但这大大影响了效率。也比较少用。
    所以你会说,既然我们比较常用的是RR(可重复读)的隔离级别会有幻读的情况,那为什么还经常使用它?其实在MySQL的InnoDB引擎使用RR的隔离级别但配合锁的机制已经避免了幻读情况。

    我们先来看一下MySQL是如何实现数据的可重复的。

    有两种方案。

    LBCC (Lock Based Concurrency Control),意思是读取数据的时候锁定它,不允许其他事务操作,数据自然也不会变化。这种方案我们叫做基于锁的并发控制简称LBCC。一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地影响操作数据的效率。
    MVCC (Multi Version Concurrency Control), 意思是多版本并发控制,另一种解决方案是如果要让一个事务前后两次读取的数据保持一致, 那么我们可以在修改数据的时候给它建立一个备份或者叫快照,后面再来的事务读取这个快照就行了。
    MVCC的核心思想是:可以查到在当前事务开始之前已经存在的数据,即使它在后面被其他事务修改或者删除了。而当前事务之后新增的数据,当前事务是查不到的。

    那么问题来了,如何保证当前事务数据的一致性呢?也就是说怎么保证数据被其他事务修改和删除或者新增了数据,而当前事务并不受影响呢?

    读取数据事务开始的时候,MySQL为事务创建了快照,也就是在事务内查询的数据都是快照版本,这样就可以保证数据的一致性。

    那么快照又是如何实现的呢?
    InnoDB 为每行记录都实现了两个隐藏字段:

    DB_TRX_ID,6 字节:插入或更新行的最后一个事务的事务ID,事务编号是自动递增的(我们把它理解为创建版本号,在数据新增或者修改为新数据的时候,记录当前事务ID)
    DB_ROLL_PTR,7 字节:回滚指针(我们把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务 ID)。
    我们把这两个事务ID理解为版本号。

    从插入数据开始,我们来看一下MySQL如何用这两个版本号来隔离事务。


    image.png

    此时又有一个事务进来,增加了一条数据并提交结束。


    image.png
    MVCC 的查找规则1:只能查找创建时间小于等于当前事务 ID 的数据
    image.png

    再次回到事务2查询


    image.png
    MVCC 的查找规则2: 能查找删除时间大于当前事务id的数据,也就是在事务之后删除的数据在当前事务依然能查得到。

    事务5,尝试修改数据


    image.png

    此时回到事务2再次查询数据


    image.png

    按照查找规则:只能查找创建时间小于等于当前事务 ID 的数据,和删除时间大于当前事 务 ID 的行(或未删除)。

    解释:id为1的数据删除版本大于当前事务2,创建版本小于事务2,依然可以查到,

    id为2的数据创建版本为1的数据,小于事务2,删除版本大于事务2,可以查到

    id为3的数据创建版本为3大于事务2,查不到;

    id为2的数据创建版本为5的数据,大于事务2,依然查不到;

    所以,在事务2内,无论外部发生了什么翻天覆地的变化,事务2自始自终查到的都是自己的快照数据,保证了数据的一致性,一定要理解MVCC的核心思想。

    之后会更新结合锁机制,InnoDB是如何在RR级别解决幻读的。

    相关文章

      网友评论

        本文标题:手撕 MySQL 事务,发生了什么?

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