【聊聊MySQL】七.MySQL-InnoDB的数据库事务介绍
一.数据库事务
刚开始接触编程,要写业务代码的时候我们知道。某个业务动作必须通过 事务 完成对数据库的修改。那么这个事务是什么,InnoDB 是怎样实现事务的要求的,就是接下来老夫想说的内容。
那么事务是怎样的,也就是说,我们在做业务操作的时候,最简单的比如用户下单:
- 用户按下下单的按钮,开始请求后端进行创建订单;
Java开始着手创建订单的信息对象;- 与此同时,产品的库存需要扣减比如 iPhone 的库存需要减掉 10 个;
- 插入订单 + 扣减库存,在
MySQL中开始执行;
结果:此时针对最后一步来看,我们要求两个操作的 SQL 操作必须一并成功,或者一并失败,而且要求数据库发生故障重启的时候数据还存在,而且数据是准确的。
上面这句话包含太多数据库事务的内容了,我们需要一个一个来说。
这个数据库事务的概念就是我们常说的 ACID。
1.1 A-Atomicity-原子性
每个点我打算拆分上面的执行结果来说,何为 原子性,对应我那句话:插入订单 + 扣减库存必须一并成功,或者一并失败。原子性就是两个操作,插入订单和扣减库存,对应数据库操作是一个 INSERT 和一个 UPDATE,那么这两个语句要么同时都对我们的数据表成功的修改,如果其中有一个失败了,另外一个语句必须不执行,或者已经执行了,必须把修改的数据还原成修改前的数据。这就说创建订单和扣减库存是一个原子操作。我们的业务不可能允许创建了订单但是没有扣减库存,这时候后续的用户下单了就没有库存发货了,那程序就没什么意义,也不允许扣减了库存但是没有创建订单,要不然赚什么钱。
所以开启事务后,我们必须满足,在这个事务里面执行的语句要么一并成功,要么全部回滚。
1.2 C-Consistency-一致性
我们知道,我们的业务代码是现实生活的映射,所以在上面扣减库存的动作中,订单下了 15 个 iPhone,那库存就应该扣减 15 个,而不是其他任何数字,更不可能是一个负数。当然现在的编码生活中,留给数据库处理的事情越来越少,所以这个一致性在某种意义上需要我们程序去保证,数据库只能够帮助我们同时执行一个语句并保证他们的执行是原子性的。
1.3 I-Isolation-隔离性
这个东西可能我们在程序中最注重的了。上面的例子已经不足以说明。那就拓展一下这个例子。
iPhone 总库存只有 15 台,A用户 和 B用户 同时下单 10台。
那么按照我们现实生活中的例子,就是两个用户看谁先排队在前面,就可以先买到 10 台 iPhone。
而程序则不是,他有可能就是这样子:

OK,老板卖了 20台 还剩下 5台 可以卖!
所以,我们在处理这个问题的时候,思路又跟生活靠近,买同一个产品的客户,你们排队,这样上面的流程将会被撸成串行(即排队),而买一个 iPhone 和一个 小米,又可以使用上面的方式处理,即提高处理速度又不担心超卖的问题。
所以如果两个请求在操作同一条数据的时候,即库存这条数据,我们需要让这两个用户读取到的数据是具有隔离性质的,也就是 A用户 完成一系列原子性操作的时候,才可以让 B用户 来执行其他的操作。
1.4 D-Durability-持久性
这个很好理解,我的业务进行后,数据状态被修改了,当系统关机或者因为其他原因导致突然暂停的时候,我的数据要恢复成宕机前的亚子。
二.InnoDB事务状态
InnoDB 将事务分为五个状态,分别是:活动的、部分提交的、失败的、中止的、提交的。
那这五个状态分别对应哪些事情,
活动的:对应开启事务的时候,也就是 START TRANSACTION;
此时,如果我们输入 COMMIT,那么该事务就是 部分提交的,为啥,因为在之前说过 BufferPool 的事情,就是说我们提交了修改,修改的还只是内存中的数据,还没有刷到硬盘,所以我们提交的时候就是 部分提交的。
那如果我们 ROLLBACK 呢,就是 中止的 的状态了,也就是回滚修改的时候。
失败的 我们日常应该很少遇到,就是我们在事务处于 活动的 或者 部分提交的 状态,导致内存中的数据没有持久化到硬盘(注意是持久化,后面会说),那这个事务就是 失败的。
提交的 数据已经完完全全被保存到硬盘中,就算重启或者宕机都不用害怕,就说这个事务是 提交的。
三.事务并发执行可能出现的问题
通常来说,我们的数据库不可能只有一条连接在执行业务。而一条连接就是一个会话(Session),所以下面遇到的问题基本是多个 Session 在同时操作数据库时可能会出现的问题。
那我依然使用上面下单扣库存的例子来说说这些问题:
3.1 脏写(Dirty Write)
脏写基本是所有数据库都不允许的,但是为了完整还是说下。
脏写可以理解成两个用户同时下单一个产品时可能出现的问题:
| 执行步骤 | Session_A_客户 | Session_B客户 |
|---|---|---|
| 1 | START TRANSACTION; |
START TRANSACTION; |
| 2 | UPDATE prod_stock SET stock = 10 |
其他操作 |
| 3 | COMMIT; |
UPDATE prod_stock SET stock = 10 |
| 4 | — | 由于其他操作失败 ROLLBACK; |
此时,A客户 下单了 10 台 iPhone,并且顺利提交,但是 B客户 可能由于其他问题,比如是个黑号,下单失败,回滚了,那么导致 A客户 提交的库存只有 10 台 iPhone 这条修改也不见了,称为 脏写。 |
那么怎么看,所有业务,基本都不可能容许这种错误的存在吧,所以下面聊隔离级别的时候,基本排除这个错误的发生。
3.2 脏读(Dirty Read)
如果 客户A 读取到的数据是 客户B 未提交的数据,我们成为 脏读。乍一想扣库存的时候,好像挺好的,但是如果 客户B 回滚数据呢,那数据就完全混乱了呀:
| 执行步骤 | Session_A_客户 | Session_B客户 |
|---|---|---|
| 1 | START TRANSACTION; |
START TRANSACTION; |
| 2 | UPDATE prod_stock SET stock = 10 |
|
| 3 | SELECT * FROM prod_stock WHERE prod_id = XXX读取到 10 |
其他操作 |
| 4 | — | 由于其他操作失败 ROLLBACK; |
3.3 不可重复读(Non-Repeatable Read)
指的是一个事务重复读取一个 SQL 时,因为其他事务提交了数据,导致在此次事务读取两次的数据不一样,成为 不可重复读。
| 执行步骤 | Session_A_客户 | Session_B客户 |
| :——: | :——————————–: | :——————: |
| 1 | START TRANSACTION; | START TRANSACTION; |
| 2 | SELECT * FROM prod_stock WHERE prod_id = XXX
读取到 15 | — |
| 3 | — | UPDATE prod_stock SET stock = 10 |
| 4 | — | COMMIT |
| 5 | SELECT * FROM prod_stock WHERE prod_id = XXX
读取到 10 | —|
3.4 幻读(Phantom)
跟不可重复读有点相似,但是这个是由于另外的事务插入数据导致本事务相同条件读到的数据条目数不一样。
| 执行步骤 | Session_A_客户 | Session_B客户 |
|---|---|---|
| 1 | START TRANSACTION; |
START TRANSACTION; |
| 2 | SELECT * FROM prod_stock WHERE prod_id = XXX读取到 15 |
— |
| 3 | — | INSERT INTO prod_stock VALUES (XXX, 10) |
| 4 | — | COMMIT |
| 5 | SELECT * FROM prod_stock WHERE prod_id = XXX读取到 15 和 10 |
— |
客户A 准备下单在读取产品的库存数据,这时候 客户B 新增了一个其他类型的库存记录,客户A 重新查询的时候,被查出来了,所以就可以说这个事务产生了 幻读。
三.InnDB事务隔离级别
那么 SQL标准 就规定了四种隔离机制,InnoDB 也同时对这几个隔离级别做了支持:
READ UNCOMMITTED:读未提交的READ COMMITTED:读已提交的REPEATABLE READ:可重复读SERIALIZABLE:串行化
当然因为 脏写 产生的后果是 所有业务都不允许发生 的,所以并不在讨论范围之内,因为所有的级别都不允许发生 脏写
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
READ UNCOMMITTED:读未提交的 |
√ | √ | √ |
READ COMMITTED:读已提交的 |
× | √ | √ |
REPEATABLE READ:可重复读 |
× | × | √ |
SERIALIZABLE:串行化 |
× | × | × |
我们可以看到,串行化 是效果最好的,但是因为是 串行化(俗话说就是每个请求都要排队,等前面做好了再来做下一个),所以性能也是最低的。
MySQL_InnoDB 的默认隔离机制是 REPEATABLE READ,也就是可重复读。
可重复读会出现幻读,但是 InnoDB 又把 幻读 的问题给解决了,所以可以说 MySQL_InnoDB 用了一些手段,将数据库事务的性能提高又不影响数据的准确性。
怎么解决的,简单的说在查询的时候判断当前事务开启时,会生成一个 ReadView,这个 ReadView 会记录当前事务的信息,然后在查询的时候,只需要判断查询到的数据的所有版本在当前 ReadView 中是否可见,如果可见就显示出来,不可见就隐藏掉。那么就相当于解决了 REPEATABLE READ:可重复读 这个级别的 幻读 问题。至于怎么比较的,现在还不是时候知道,因为需要先知道 REDO 和 UNDO 日志。
四. 完
OK,有了 事务 的一些相关内容以后,就可以来深入 事务 的执行原理了。