# Mysql学习(六)——事务
## ACID事务特性
在关系型数据库中,一个逻辑操作要称为事务,需要满足四个特性:原子性(Atomicity),一致性(Consistency),隔离性(Isolation),持久性(Durability)。简称为ACID
**原子性**:
- 事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行
- 每次写事务都会先去修改Buffer pool,在事务前或事务期间会产生对应的Undo/Redo日志,在Buffer pool的脏页被刷到磁盘之前,这些日志都会先写入到文件中。而如果在脏页刷新到磁盘前时,数据库挂了,那么这个时候mysql就会使用崩溃恢复,使用Redo日志将其恢复,确保原子性。如果是需要rollback事务,那么mysql会执行Undo日志中记录的数据,将数据恢复,确保原子性
**持久性**:
- 指的是一个事务一旦提交,它对数据库中数据的改变就应该是永久性的,后续的操作或故障不应该对其有任何影响,不会丢失数据
**隔离性**:
- 指的是事务执行期间不会被其他事务干扰,一个事务的内部操作对其他的事务是隔离的
- InnoDB支持的事务隔离级别有:读未提交、读已提交、可重复读、可串行化。锁和多版本并发控制(MVCC)用于保障系统隔离性。
**一致性**:
- 指的是事务开始之前和事务结束之后,数据库的完整性限制未被破坏。一致性包括两方面的内容,分别是约束一致性和数据一致性。
- 约束一致性:创建表结构时所指定的外键、Check、唯一索引等约束
- 是一个综合性的规定,因为它是由原子性、持久性、隔离性共同保证的结果,而不是
单单依赖于某一种技术
## 事务的演进
在并发事务的场景下,会导致有4种常见的问题,分别为:更新丢失、脏读、不可重复读、幻读
**更新丢失**:
- 如果没有事务控制机制,A事务将行id = 1的数据+1,此时B事务将行id的数据+2,导致我们得到的结果与预期的不相同,造成更新丢失
- A事务发现数据不对,将数据进行回滚,而此时B事务已经提交了事务,A却将数据直接回滚到初始态,导致B事务更新的数据丢失
**脏读**:
- 一个事务读取到了另外一个事务未提交的数据
**不可重复读**:
- 一个事务相同的查询条件,多次查询同一行记录结果不一致
**幻读**:
- 一个事务,相同的查询条件,查询出的数据量不一致
## 多个事务并发的解决机制
### 所有事务排队
将数据库变成单线程处理,按照顺序对事务进行排序,一次只执行一个事务,这样的结果一定是线程安全的,但是效率非常低

### 排他锁
排队机制效率太多低下,我们需要引入锁机制,锁机制原理就是相同的数据项上增加排他锁(互斥锁),确保一个数据项同时只有一个事务对其进行查询或修改。

### 读写锁
读写锁的操作就是给对应的数据项加上对应的读锁或者写锁,当先加上读锁时,其他的事务也可以继续追加读锁,但是一旦有写锁,需要等待读锁释放,并且写锁未释放前,无法再加锁。

### MVCC机制
多版本控制(MVCC)的实现原理,是在读写锁的基础上做了优化,读写锁只能针对读读的场景并发,但是MVCC可以支持读读、读写、写读,只有写写还无法支持。
如图:在事务1开始执行时,会将对应的数据项,拷贝出来一个副本,事务1未释放时,其他的事务读取的就是这个副本,又称为**快照读**。因此一个事务在操作相同的数据项时,不会影响其他事务的读取。

#### InnoDB中MVCC的实现
上面介绍了一个事务操作对应数据项时,需要生成对应的副本供其他线程进行快照读。那么InnoDB如何实现的?用的是之前介绍过的Undo日志,在一个事务开启前,会将此时的数据复制一份到Undo日志中,这个备份记录可以给其他事务读取,也可以用作数据的回滚
#### MVCC的实现原理
MVCC对读操作是不加锁的,读写之前不会发生冲突,在读多写少的应用中,可以提升极大的效率,MVCC目前在**读已提交**、**可重复读**两种隔离级别下进行工作。
这里可以介绍下读已提交和可重复读,为何使用的都是MVCC机制,但是读已提交和可重复读在事务中读取出来的结果不一致?在可重复读的隔离级别下,读取数据时会针对对应的数据项生成一个readView,相同的事务后续查询,每次查询的就是这个readView,会将readview中不符合本次查询条件的数据给过滤掉,而读已提交就是每次都会去读取最新的快照。
在MVCC的机制下,我们通常读取会有两种场景:快照读、当前读
- **快照读**:读取的是记录的快照版本,无需加锁
- **当前读**:读取的是最新的版本,并且会进行加锁,确保数据不会被其他事务修改,使用命令可以加锁
```sql
select * from table for update;
select * from table lock in share mode;
```
insert/update/delete其实也是在执行时默认在最后自动增加了for update,确保修改的是当前最新的记录
#### MVCC实例
F1-F6是表中的字段,innoDB会在每一行后面增加几个隐藏的字段,分别为:DB_ROW_ID,DB_TRX_ID,DB_ROLL_PT;
- DB_ROW_ID:隐含ID,InnoDB在生成聚集索引时,聚集索引会生成并包含这个值
- DB_TRX_ID:当前的事务版本ID
- DB_ROLL_PT:回滚指针,意味着当前事务的回滚事务版本指针

- 我们将F1-F6数据进行更新,将原有数据更新为10-60

此时经历的过程为:
- 使用排他锁,锁定本行
- 将该行数据,复制一份到Undo Log中
- 修改数据,修改当前行事务版本号,并将回滚指针指向对应Undo Log
- 此时再来一个事务,针对刚才那条数据继续修改,依旧按照上面的步骤,将旧的数据备份,新的数据回滚指针指向旧的数据

## 事务隔离级别
前面提到多个事务并发会导致4个问题:更新丢失、脏读、不可重复读、幻读四种问题。Mysql用了四种事务隔离级别帮助我们避免这些问题

- **读未提交(read Uncommited)**:
解决了回滚覆盖类型的更新丢失,但是会产生脏读现象,即其他事务还没提交的数据,此时也被本事务查询出来,导致数据不一致
- **读已提交(read commited)**:
只能读取到其他事务已经提交的数据,解决了脏读的问题,但是会导致一个事务相同的条件下,查询同一行记录数据不一致
- **可重复读(Repeatable read)**:
解决了不可重复读的问题,第一次去查询时会给对应的数据行生成一个readview,后面事务每次去查询时都会用这个readview返回,readview中会将所有事务版本大于本事务版本的数据给过滤掉,这样可以解决了不可重复读的问题,并且也避免了其他事务插入时,导致的本事务幻读的问题,但是无法解决本事务范围更新或者删除数据时的幻读现象。
- **串行化(Serializable)**:
所有的增删改都使用串行化排序进行执行,避免了幻读现象,但是也带来了效率低下的问题
数据库的事务隔离级别,事务的隔离级别越高,并发的问题越少,同样并发的性能就越差。需要针对具体业务场景进行选择,在Mysql中默认的级别是可重复读,当业务对可重复读以及幻读不敏感时,可以使用read commited隔离级别,提升一定的性能。
**事务设置相关命令**:
```sql
show variables like 'tx_isolation';--查看当前事务隔离级别
select @@tx_isolation;--查看当前事务隔离级别
set tx_isolation='READ-UNCOMMITTED';--设置读未提交
set tx_isolation='READ-COMMITTED';--设置读已提交
set tx_isolation='REPEATABLE-READ';--设置可重复读
set tx_isolation='SERIALIZABLE';--设置串行化
```
Mysql学习(六)——事务