文档
https://mp.weixin.qq.com/s/ZXpS9rs3ywFaSrN5ftbgmw // 文章已经被作者删除
高性能 MySQL 实战 | Laravel 学院 // SQL 更新语句的执行流程与日志写入
14丨什么是事务处理,如何使用COMMIT和ROLLBACK进行操作?-极客时间
15丨初识事务隔离:隔离的级别有哪些,它们都解决了哪些异常问题?-极客时间
31丨为什么大部分RDBMS都会支持MVCC?-极客时间
事务和锁机制是什么关系? 开启事务就自动加锁了吗? - 南哥的天下 - 博客园 -- 推荐阅读,但要验证
并发事务存在的问题和 MySQL 事务隔离级别 - Laravel学院 -- 推荐
通过 MVCC(多版本并发控制)保证数据库事务的一致性 - Laravel 学院
MySQL事务与锁 - 知乎 -- 推荐,并且整理出来思维导图
开始之前,还是有必要说一句,
事务操作「很多数据库机制」都是针对 写操作 而言,如果全是读操作,是没有必要的,
原因想一想就会知道「因为是无害的,无影响的,是否并发访问结果都一样的」
很多服务都是如此,读不用处理,写才是要重点处理的。
引言 照例,我们先来一个场景
面试官: "知道事务的四大特性么?" 你: "懂,ACID嘛,原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)!" 面试官:“你们是用MySQL 数据库吧,能简单说说Innodb中怎么实现这四大特性的么?” 你: "我只知道隔离性是怎么做的balabala~~" 面试官: "还是回去等通知吧~"
OK,回到正题。 说到事务的四大特性原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),懂的人很多。 但是稍微涉及细节一点,这四大特性在数据库中的实现原理是怎么样的?那就没有几个人能够答得上来了。 因此,我们这篇文章着重讨论一下四大特性在MySQL 中的实现原理。 正文 我们以从A账户转账50元到B账户为例【经典举例】进行说明一下ACID,四大特性。 原子性
根据定义,原子性是指一个事务是一个不可分割的工作单位,其中的操作要么都做,要么都不做。
即要么转账成功,要么转账失败,是不存在中间的状态! 如果无法保证原子性会怎么样? OK,就会出现数据不一致的情形,A账户减去50元,而B账户增加50元操作失败。系统将无故丢失50元~
一致性
根据定义,一致性是指事务执行前后,数据处于一种合法的状态,这种状态是语义上的而不是语法上的。 那什么是合法的数据状态呢? OK,这个状态是满足预定的约束就叫做合法的状态,再通俗一点,这状态是由你自己来定义的。
满足这个状态,数据就是一致的,不满足这个状态,数据就是不一致的!
如果无法保证一致性会怎么样? 例一:
A账户有200元,转账300元出去,此时A账户余额为-100元。你自然就发现了此时数据是不一致的,为什么呢?因为你定义了一个状态,余额这列必须大于0。
例二:
A账户200元,转账50元给B账户,A账户的钱扣了,但是B账户因为各种意外,余额并没有增加。你也知道此时数据是不一致的,为什么呢?因为你定义了一个状态,要求A+B的余额必须不变。
隔离性
根据定义,隔离性是指多个事务并发执行的时候,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
如果无法保证隔离性会怎么样? OK,假设A账户有200元,B账户0元。A账户往B账户转账两次,金额为50元,分别在两个事务中执行。
如果无法保证隔离性,会出现下面的情形

如图所示,如果不保证隔离性,A扣款两次,而B只加款一次,凭空消失了50元,依然出现了数据不一致的情形!
PS
可能有细心的读者已经发现了,MySQL 中是依靠锁来解决隔离性问题。
嗯,我们后面来说明。
持久性
根据定义,持久性是指事务一旦提交,它对数据库的改变就应该是永久性的。
接下来的其他操作或故障不应该对其有任何影响。
如果无法保证持久性会怎么样? 在MySQL 中,为了解决CPU和磁盘速度不一致问题,MySQL 是将磁盘上的数据加载到内存,对内存进行操作,然后再回写磁盘。好,假设此时宕机了,在内存中修改的数据全部丢失了,持久性就无法保证。
设想一下,系统提示你转账成功。但是你发现金额没有发生任何改变,此时数据出现了不合法的数据状态,我们将这种状态认为是数据不一致的情形。
实战解答 问题一: MySQL 怎么保证原子性的?
OK,是利用Innodb的undo log。 undo log名为回滚日志,是实现原子性的关键,当事务回滚时能够撤销所有已经成功执行的sql语句,他需要记录你要回滚的相应日志信息。
例如
(1) 当你delete一条数据的时候,就需要记录这条数据的信息,回滚的时候,insert这条旧数据 (2) 当你update一条数据的时候,就需要记录之前的旧值,回滚的时候,根据旧值执行update操作 (3) 当年insert一条数据的时候,就需要这条记录的主键,回滚的时候,根据主键执行delete操作
undo log记录了这些回滚需要的信息,当事务执行失败或调用了rollback,导致事务需要回滚,便可以利用undo log中的信息将数据回滚到修改之前的样子。
PS
具体的undo log日志长啥样,这个可以写一篇文章了。
而且写出来,看的人也不多,姑且先这么简单的理解吧。
问题二: MySQL 怎么保证持久性的?
OK,是利用Innodb的redo log。 正如之前说的,MySQL 是先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上。如果此时突然宕机,内存中的数据就会丢失。
怎么解决这个问题? 简单啊,事务提交前直接把数据写入磁盘就行啊。
这么做有什么问题?
只修改一个页面里的一个字节,就要将整个页面刷入磁盘,太浪费资源了。毕竟一个页面16KB大小,你只改其中一点点东西,就要将16KB的内容刷入磁盘,听着也不合理。 毕竟一个事务里的SQL可能牵涉到多个数据页的修改,而这些数据页可能不是相邻的,也就是属于随机IO。
显然操作随机IO,速度会比较慢。 于是,决定采用redo log解决上面的问题。
当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。
当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。「解析: 事务执行提交的时候,会根据redo log 进行刷盘,但如果这时服务宕机,此时便属于事务已提交但尚未刷盘的情况」
当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
这部分需要结合 3.问题/补充中的「SQL 更新语句的执行流程与日志写入」一起看,思考会比较好~
采用redo log的好处? 其实好处就是将redo log进行刷盘比对数据页刷盘效率高。
具体表现如下:
1. redo log体积小,毕竟只记录了哪一页修改了啥,因此体积小,刷盘快。 2. redo log是一直往末尾进行追加,属于顺序IO。效率显然比随机IO来的快。
PS
不想具体去谈redo log具体长什么样,因为内容太多了。
问题三: MySQL 怎么保证隔离性的?
OK, 利用的是锁和MVCC机制。
还是拿转账例子来说明,有一个账户表如下 表名t_balance
 其中id是主键,user_id为账户名,balance为余额。还是以转账两次为例,如下图所示

至于MVCC, 即多版本并发控制(Multi Version Concurrency Control),。
一个行记录数据有多个版本对快照数据,这些快照数据在undo log中。
如果一个事务读取的行正在做 DELETE 或者 UPDATE 操作,读取操作不会等行上的锁释放,而是读取该行的快照版本。
由于MVCC机制在可重复读(Repeateable Read)和读已提交(Read Commited)的MVCC表现形式不同,就不赘述了。
但是有一点说明一下,在事务隔离级别为读已提交(Read Commited)时,一个事务能够读到另一个事务已经提交的数据,是不满足隔离性的。
但是当事务隔离级别为可重复读(Repeateable Read)中,是满足隔离性的。
问题四:MySQL 怎么保证一致性的?
OK,这个问题分为两个层面来说。 从数据库层面,数据库通过原子性、隔离性、持久性来保证一致性。
也就是说ACID四大特性之中,C(一致性)是目的,A(原子性)、I(隔离性)、D(持久性)是手段,是为了保证一致性,数据库提供的手段。数据库必须要实现AID三大特性,才有可能实现一致性。「这也是为什么将一致性房放在最后提问的原因」
例如,原子性无法保证,显然一致性也无法保证。
但是,如果你在事务里故意写出违反约束的代码,一致性还是无法保证的。例如,你在转账的例子中,你的代码里故意不给B账户加钱,那一致性还是无法保证。
因此,还必须从应用层角度考虑。
从应用层面,通过代码判断数据库数据是否有效,然后决定回滚还是提交数据!
总结
本文讲了Mysql中事务ACID四大特性的实现原理,希望大家有所收获。
其实,还是要尝试去理解,结合自己已有的知识节点,串联起来,形成自己的知识体系~
学习一定要注意思考,举一反三,不能有一学一,而应该学一个就应该学一类, 个人做的也不好,只是在朝着这个方向努力~~
事务隔离的级别有哪些? 脏读、不可重复读和幻读这三种异常情况,是在 SQL-92 标准中定义的,同时 SQL-92 标准还定义了 4 种隔离级别来解决这些异常情况。解决异常数量从少到多的顺序(比如读未提交可能存在 3 种异常,可串行化则不会存在这些异常)决定了隔离级别的高低,这四种隔离级别从低到高分别是:读未提交(READ UNCOMMITTED )、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)和可串行化(SERIALIZABLE)。这些隔离级别能解决的异常情况如下表所示 你能看到可串行化能避免所有的异常情况,而读未提交则允许异常情况发生。 关于这四种级别,我来简单讲解下。 读未提交,也就是允许读到未提交的数据,这种情况下查询是不会使用锁的,可能会产生脏读、不可重复读、幻读等情况。 读已提交就是只能读到已经提交的内容,可以避免脏读的产生,属于 RDBMS 中常见的默认隔离级别(比如说 Oracle 和 SQL Server),但如果想要避免不可重复读或者幻读,就需要我们在 SQL 查询的时候编写带加锁的 SQL 语句(我会在进阶篇里讲加锁)。 可重复读,保证一个事务在相同查询条件下两次查询得到的数据结果是一致的,可以避免不可重复读和脏读,但无法避免幻读。 MySQL 默认的隔离级别就是可重复读。  可串行化,将事务进行串行化,也就是在一个队列中按照顺序执行,可串行化是最高级别的隔离等级,可以解决事务读取中所有可能出现的异常情况,但是它牺牲了系统的并发性。 |