|
|
|
@ -1124,46 +1124,112 @@ kill trx_mysql_thread_id;
|
|
|
|
|
|
|
|
|
|
## 事务特性(ACID)
|
|
|
|
|
|
|
|
|
|
### 原子性(Atomicity)
|
|
|
|
|
### 原子性(Atomicity)
|
|
|
|
|
|
|
|
|
|
**事务是最小的执行单位,不允许分割。原子性确保动作要么全部完成,要么完全不起作用。**
|
|
|
|
|
**原子性是指一个事务(事务是最小的执行单位)是一个不可分割的工作单元,其中的操作要么都做,要么都不做**。如果事务中一个sql语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态。
|
|
|
|
|
|
|
|
|
|
原子性是依赖于回滚日志(`undo log`)实现的。当事务对数据库进行修改时,`InnoDB`会生成对应的 `undo log`;如果事务执行失败或调用了 `rollback`,导致`事务需要回滚`,便可以利用 `undo log` 中的信息将数据回滚到修改之前的样子。`undo log`属于逻辑日志,它记录的是sql执行相关的信息。当发生回滚时,InnoDB 会根据 `undo log` 的内容做与之前相反的工作:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**实现原理**
|
|
|
|
|
|
|
|
|
|
**① 回滚日志(undo log)**
|
|
|
|
|
|
|
|
|
|
InnoDB实现回滚靠的是undo log。当事务对数据库进行修改时,InnoDB会生成对应的undo log。如果事务执行失败或调用了rollback,导致事务需要回滚,便可利用undo log中的信息将数据回滚到修改前。
|
|
|
|
|
|
|
|
|
|
- 对于每个`insert`,回滚时会执行`delete`
|
|
|
|
|
- 对于每个 `delete`,回滚时会执行`insert`
|
|
|
|
|
- 对于每个 `update`,回滚时会执行一个相反的 `update`,把数据改回去
|
|
|
|
|
|
|
|
|
|
以`update`操作为例:当事务执行`update`时,其生成的`undo log`中会包含被修改行的主键、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到`update`之前的状态。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 一致性(Consistency)
|
|
|
|
|
|
|
|
|
|
**一致性是指事务执行结束后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。**
|
|
|
|
|
|
|
|
|
|
数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**实现原理**
|
|
|
|
|
|
|
|
|
|
一致性是事务追求的最终目标。前面提到的**原子性、持久性和隔离性**,都是为了保证数据库状态的一致性。此外,除了数据库层面的保障,一致性的实现也需要应用层面进行保障。实现一致性的措施包括:
|
|
|
|
|
|
|
|
|
|
**① 保证原子性、持久性和隔离性**。如果这些特性无法保证,事务的一致性也无法保证
|
|
|
|
|
|
|
|
|
|
**② 数据库本身提供保障**。如不允许向整型列插入字符串值、字符串长度不能超过列的限制等
|
|
|
|
|
|
|
|
|
|
**③ 应用层面进行保障**。如转账操作只扣除转账者余额,而未增加接收者余额
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 隔离性(Isolation)
|
|
|
|
|
|
|
|
|
|
**隔离性是指事务内部的操作与其它事务是隔离的,并发执行的各个事务之间不能互相干扰。**严格的隔离性,对应了事务隔离级别中的Serializable (可串行化),但实际应用中出于性能方面的考虑很少会使用可串行化。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**实现原理**
|
|
|
|
|
|
|
|
|
|
隔离性追求的是并发情形下事务之间互不干扰。主要分为两个方面:
|
|
|
|
|
|
|
|
|
|
**① 锁机制保证隔离性**:(一个事务)写操作对(另一个事务)写操作的影响
|
|
|
|
|
|
|
|
|
|
事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其它事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
|
|
|
|
|
|
|
|
|
|
**② MVCC(多版本并发控制)保证隔离性**:(一个事务)写操作对(另一个事务)读操作的影响
|
|
|
|
|
|
|
|
|
|
MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议。最大优点是读不加锁,因此读写不冲突,并发性能好。InnoDB的MVCC实现了多个版本的数据可共存,主要基于以下技术及数据结构:
|
|
|
|
|
|
|
|
|
|
### 一致性(Consistency)
|
|
|
|
|
- **隐藏列**:在Innodb引擎中每行数据都会有两个隐藏列(实际是三个列)
|
|
|
|
|
- **隐藏id**(`id`,如果建表时没有显式指定,则会生成这个隐藏id作为主键,实际和mvcc没有关系)
|
|
|
|
|
- **创建版本号**(`data_trx_id`,事务id):用来标识最近对本行记录做修改的事务 id
|
|
|
|
|
- **回滚指针**(`data_roll_pointer`,指向undo log的指针)
|
|
|
|
|
- **基于undo log版本链**:每条undo log也会指向更早版本的undo log,从而形成一条版本链
|
|
|
|
|
- **ReadView**:**通过隐藏列和版本链可以将数据恢复到指定版本,但具体要恢复到哪个版本,则需要根据ReadView来确定。**当进行查询操作时,事务会生成一个ReadView(是一个事务快照),准确来说是当前时间点系统内活跃的事务列表,也就是说系统内所有未提交的事务,都会记录在这个Readview内,事务就根据它来判断哪些数据是可见的,哪些是不可见的。在每一条 SQL 开始的时候被创建,有几个重要属性:
|
|
|
|
|
- **trx_ids:** 当前系统活跃(未提交)事务版本号集合
|
|
|
|
|
- **low_limit_id:** 创建当前 read view 时“当前系统最大**事务版本号**+1”
|
|
|
|
|
- **up_limit_id:** 创建当前read view 时“系统正处于**活跃事务**最小版本号”
|
|
|
|
|
- **creator_trx_id:** 创建当前read view的事务版本号
|
|
|
|
|
|
|
|
|
|
**事务开始前和结束后,数据库的完整性约束没有被破坏**。比如A向B转账,不可能A扣了钱,B却没收到。
|
|
|
|
|
|
|
|
|
|
一致性是事务追求的最终目标,原子性、持久性和隔离性其实都是为了保证数据库状态的一致性。当然,都是数据库层面的保障,一致性的实现也需要应用层面进行保障。也就是你的业务,比如购买操作只扣除用户的余额,不减库存,肯定无法保证状态的一致。
|
|
|
|
|
|
|
|
|
|
**MVCC查询流程**
|
|
|
|
|
|
|
|
|
|
现在开始查询,一个 select 过来了,找到了一行数据。
|
|
|
|
|
|
|
|
|
|
### 隔离性(Isolation)
|
|
|
|
|
- **data_trx_id < up_limit_id**:说明数据在当前事务之前就存在了,显示
|
|
|
|
|
- **data_trx_id >= low_limit_id**:说明该数据是在当前read view 创建后才产生的,数据不显示。不显示怎么办,根据 data_roll_pointer 从 undo log 中找到历史版本,找不到就空
|
|
|
|
|
- **up_limit_id < data_trx_id < low_limit_id**:就要看隔离级别了
|
|
|
|
|
|
|
|
|
|
**并发访问数据库时,一个事务不被其他事务所干扰。**
|
|
|
|
|
|
|
|
|
|
隔离性是通过 `锁` 和 `MVCC`(多版本并发控制) 实现。InnoDB采用的MVCC实现方式是:在需要时,通过undo日志构造出历史版本。
|
|
|
|
|
|
|
|
|
|
**MVCC应用场景**
|
|
|
|
|
|
|
|
|
|
在Mysql的InnoDB引擎中,只有**已提交读**和**可重复读**这两种隔离级别的事务采用了MVCC机制:
|
|
|
|
|
|
|
|
|
|
### 持久性(Durability)
|
|
|
|
|
- **已提交读(READ COMMITTD)**:事务中的每次读操作都会生成一个新的ReadView,也就是说如果这期间某个事务提交了,那么它就会从ReadView中移除。这样确保事务每次读操作都能读到相对比较新的数据
|
|
|
|
|
- **可重复读(REPEATABLE READ)**:事务只有在第一次进行读操作时才会生成一个ReadView,后续的读操作都会重复使用这个ReadView。也就是说如果在此期间有其他事务提交了,那么对于可重复读来说也是不可见的,因为对它来说,事务活跃状态在第一次进行读操作时就已经确定下来,后面不会修改了
|
|
|
|
|
|
|
|
|
|
**一个事务被提交之后。对数据库中数据的改变是持久的,即使数据库发生故障。**
|
|
|
|
|
|
|
|
|
|
`InnnoDB`有很多 log,持久性靠的是`redo log`。持久性肯定和写有关,`MySQL` 里经常说到的 `WAL`技术,`WAL`的全称是`Write-Ahead Logging`,它的关键点就是先写日志,再写磁盘。
|
|
|
|
|
|
|
|
|
|
当事务提交时,会调用`fsync`接口对`redo log`进行刷盘。如果`MySQL`宕机,重启时可以读取`redo log`中的数据,对数据库进行恢复。`redo log`采用的是WAL(`Write-ahead logging`,预写式日志),所有修改先写入日志,再更新到`Buffer Pool`,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。而且这样做还有两个优点:
|
|
|
|
|
### 持久性(Durability)
|
|
|
|
|
|
|
|
|
|
- 刷脏页是随机`IO`,`redo log` 顺序`IO`
|
|
|
|
|
- 刷脏页以Page为单位,一个Page上的修改整页都要写;而redo log 只包含真正需要写入的,无效 IO 减少
|
|
|
|
|
**持久性是指事务一旦提交,它对数据库的改变就应该是永久性的,即使数据库发生故障也不受影响。**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**实现原理**
|
|
|
|
|
|
|
|
|
|
**① redo log**
|
|
|
|
|
|
|
|
|
|
当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘(刷脏页)。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
**为什么redo log写入磁盘比直接将Buffer Pool中修改数据写入磁盘(刷脏)快?**
|
|
|
|
|
|
|
|
|
|
- 刷脏是随机I/O,因为每次修改的数据位置随机,但写redo log是追加操作,属于顺序I/O
|
|
|
|
|
- 刷脏是以数据页(Page)为单位的,MySQL默认页大小是16KB,一个Page上一个小修改都要整页写入;而redo log中只包含真正需要写入的部分,无效I/O大大减少
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -1194,18 +1260,16 @@ kill trx_mysql_thread_id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
![InnoDB存储引擎SQL隔离级别锁比较](images/Database/InnoDB存储引擎SQL隔离级别锁比较.png)
|
|
|
|
|
|
|
|
|
|
### Read Uncommitted(读未提交)
|
|
|
|
|
|
|
|
|
|
**即读取到了其它事务未提交的内容**。在该隔离级别,**所有事务都可以看到其他未提交事务的执行结果**。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为**脏读(Dirty Read)**。
|
|
|
|
|
**即读取到了其它事务未提交的内容(可能会被回滚)**。在该隔离级别,**所有事务都可以看到其他未提交事务的执行结果**。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为**脏读(Dirty Read)**。
|
|
|
|
|
|
|
|
|
|
**特点**:最低级别,任何情况都无法保证
|
|
|
|
|
|
|
|
|
|
**读未提交的数据库锁情况**
|
|
|
|
|
|
|
|
|
|
- 读取数据:**未加锁**
|
|
|
|
|
- 写入数据:**只对数据增加行级共享锁**
|
|
|
|
|
- 读取数据:**未加锁,每次都读到最新数据,性能最好**
|
|
|
|
|
- 写入数据:**只对数据增加行级共享锁,写完释放**
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -1261,39 +1325,6 @@ kill trx_mysql_thread_id;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## MVCC
|
|
|
|
|
|
|
|
|
|
https://mp.weixin.qq.com/s/KbOiJ8SKJ_wFZcIyDVGD9g
|
|
|
|
|
|
|
|
|
|
**MVCC**主要是通过**版本链**和**ReadView**来实现的。在Mysql的InnoDB引擎中,只有**已提交读(READ COMMITTD)**和**可重复读(REPEATABLE READ)**这两种隔离级别下的事务采用了MVCC机制。
|
|
|
|
|
|
|
|
|
|
- **版本链**
|
|
|
|
|
|
|
|
|
|
在InnoDB引擎表中,它的每一行记录中有两个必要的隐藏列:
|
|
|
|
|
|
|
|
|
|
- `DATA_TRX_ID`:表示插入或更新该行的最后一个事务的事务标识符,同样删除在内部被视为更新,在该更新中,行中的特殊位被设置为将其标记为已删除。行中会有一个特殊位置来标记删除。
|
|
|
|
|
- `DATA_ROLL_PTR`:存储了一个指针,它指向这条记录的上一个版本的位置,通过它来获得上一个版本的记录信息。
|
|
|
|
|
|
|
|
|
|
**作用**:解决了读和写的并发执行。
|
|
|
|
|
|
|
|
|
|
- **ReadView**
|
|
|
|
|
|
|
|
|
|
ReadView主要存放的是当前事务操作时,系统中仍然活跃着的事务(事务开启后,没有提交或回滚的事务)。
|
|
|
|
|
|
|
|
|
|
- **ReadView数据结构**:ReadView是MySQL底层使用C++代码实现的一个结构体,主要的内部属性如下:
|
|
|
|
|
- `trx_ids`:数组,存储的是创建readview时,活跃事务链表里所有的事务ID
|
|
|
|
|
- `low_limit_id`:存储的是创建readview时,活跃事务链表里最大的事务ID
|
|
|
|
|
- `up_limit_id`:存储的是创建readview时,活跃事务链表里最小的事务ID
|
|
|
|
|
- `creator_trx_id`:当前readview所属事务的事务版本号
|
|
|
|
|
|
|
|
|
|
- **ReadView创建策略**:对于读提交和可重复读事务隔离级别来说,ReadView创建策略是不同的,这样才能保证隔离性不同
|
|
|
|
|
- `可重复读隔离级别`:事务开启后,第一次查询的时候创建,之后一直不变,直到事务结束
|
|
|
|
|
- `读提交隔离级别`:事务开启后,每一次读取都重新创建
|
|
|
|
|
|
|
|
|
|
也就是说已提交读隔离级别下的事务在每次查询的开始都会生成一个独立的ReadView,而可重复读隔离级别则在第一次读的时候生成一个ReadView,之后的读都复用之前的ReadView。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Spring事务机制
|
|
|
|
|
|
|
|
|
|
### 实现方式
|
|
|
|
|