事务的意义

保证数据的正确性,不同的数据之间不会产生矛盾。也就是保证数据状态的一致性。

讨论的范围

数据库事务状态的一致性

1
2
3
4
5
6
7
8
9

### 实现数据库事务状态一致性目标的前提

* 原子性:在同一项业务处理中,事务保证对多个数据的修改要么都成功,要么都被撤销
* 隔离性:在不同的业务处理过程中,事务保证各自读、写的数据互相独立,不会彼此影响
* 持久性:事务应当保证所有被成功提交的数据修改,都能正确的持久化到数据库当中去,不会丢失数据。


```原子性、隔离性、持久性是手段,一致性是目标

关于事务处理的不场景

  • 单个服务使用单个数据源
  • 单个服务使用多个数据源
  • 多个服务使用单个数据源
  • 多个服务使用多个数据源

单个服务使用单个数据源

单个服务使用单个数据源,也就是本地事务场景,也可以叫作局部事务

本地事务说明

指仅操作特定单一事务资源的、不需要“全局事务管理器”参与协调的事务。

本地事务,依赖于数据源本身的事务能力来工作,我们常见的在程序代码中的事务也最多就是对事务接口进行的一层标准化包装。事务的开启、终止、提交、回滚、嵌套、设置隔离级别乃至与应用代码贴近的传播方式都需要依赖底层数据库的支持。

ARIES理论

Algorithms for Recovery and Isolation Exploiting Semantics ,基于语义的恢复与隔离算法

当前主流关系性数据在事务实现上都受到该理论的影响。

如何实现原子性和持久性

实现原子性和持久性最大的困难是什么?

写入磁盘这个操作不会是原子性,不仅存在写入、未写入,还存在“正在写”的中间状态。

这可咋办呀,怎么记录这个中间状态呢?日志?

Commit Logging

把日志顺序追加写入文件方式,记录到磁盘中。见到代表事务提交成功的Commit Record之后 ,数据库才根据日志上的信息对真实数据进行修改,修改完之后,在日志中再加入一条End Record,表示数据库已完成持久化。这种事务实现方法就是Commit Logging

Commit Logging,有几个切入的判断点:

  1. 日志成功写了Commit Record表示事务成功了
  2. 如果发生了崩溃,看到Commit Record不完整,那就需要将已经提交Commit Record的记录回滚掉。

Commit Logging的缺陷:

Record提交之后进行的。才不管你的磁盘I/O是否有空闲。别管有什么理由不能在事务提交之前就开始修改磁盘上的数据。```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38


为了解决Commit Logging的缺陷,基于ARIES理论的"Write Ahead Logging"的日志改进方案就出现了

##### Write Ahead Logging

就是允许事务在提交之前,提前写入变动数据的意思。

* FORCE:当事务提交后,要求变动数据必须同时完成写入则称为FORCE,如果不强制变动数据必须同时完成写入则称为NO-FORCE。现实中绝大多数数据库采用的都是NO-FORCE策略,只要有了日志,变动数据随时可以持久化,从优化磁盘I/O性能考虑,没有必要强制数据写入立即进行。
* STEAL:在事务提交前,允许变动数据提前写入则称为STEAL,不允许则称为NO-STEAL。从优化磁盘I/O性能考虑,允许数据提前写入,有利于利用空闲I/O资源,也有利于节省数据库缓存区的内存。

Commit Logging允许NO-FORCE,但不允许STEAL。
Write-Ahead Logging允许NO-FORCE,也允许STEAL。(性能最高,复杂性最高)

实现方式:增加UnDo log日志,在变动数据写入磁盘之前,必须先记录UnDo log。这个log中明确记录修改了哪个位置的数据,从什么值改成了什么值。在需要回滚的时候,再根据这个日志来进行处理。

##### 使用UnDo log,Write Ahead Logging在崩溃恢复时的三个步骤
* 分析阶段:把没有End Record的事务组成一个待恢复集合
* 重做阶段:从上述的集合中找到包含Commit Record的日志,把它们持久化到磁盘当中
* 回滚阶段:经过上述两个步骤之后,把这个待恢复的集中的剩余事务全部圆润



#### 如何实现隔离性

隔离性保证了每个事务在各自读、写的数据都互相独立,彼此不受影响

##### 数据库提供的三种锁

* 写锁(排他锁)
只有持有写锁的事务才能对数据进行写入操作,数据被加上写锁时,其他事务不能写入数据,也不能加读锁。
写锁禁止其他事务施加读锁,而不是禁止事务读取数据
* 读锁(共享锁)
多个事务可以对一行数据添加多个读锁,数据被加上读锁之后就不能再加写锁了,所有数据都不能对该数据进行写入,但仍然可以读取。
* 范围锁
对于某个范围直接加排他锁,在这个范围内的数据不能被读取,也不能被写入
```sql
select * from books where price<100 for update;##范围锁

四种隔离级别(从高到低)

可串行化:强度最高的隔离性

对事务所有读、写的数据全部加上读锁、写锁和范围锁

可重复读

对事务所涉及到的数据加读锁、写锁,但不加范围锁

可重复读比可串行化弱化的地方在于幻读问题,它是指在事务执行的过程中,两个完全相同的范围查询得到了不同的结果集

特例:MySQL/InnoDB默认隔离级别是可重复读,但是在只读事务中避免了幻读问题。但是在读写事务中幻读问题依然存在

读已提交

对事务所涉及到的数据加的写锁,会一直持续到事务结束。而加的读锁在查询完成之后就会马上释放。
读已提交比可重复读弱化的地方在于不可重复读问题,它是指在事务执行过程中,对同一行数据的两次查询得到了不同的结果。

在这个隔离级别下,两次重复执行的查询结果不一样的原因:读是在查询完成就直接释放了。并没有贯穿整个事务的生命周期,也没有办法禁止读取过的数据发生修改。

读未提交

对事务涉及到数据只加写锁,一直持续到事务结束,但完全不加读锁。

读未提交比读已提交弱化的地方在于脏读问题,它是指在事务执行的过程中,一个事务读取到了另一个事务未提交的数据。

MVCC基础原理

针对一个事务读、另一个事务写的场景而提出的无锁优化方案,是一种读取优化策略。注意只是针对读+写的事务场景(写+写,就令当别论了)

基本思路:对数据的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本同时存在。借用这种手段来达到读取时不加锁的目的。

从全局的角度来看,可以理解为给每一行数据都增加两个默认为空的字段:CREATE_VERSION和DELETE_VERSION。当有数据发生变化时,通过控制这两个字段来进行处理:

  • 数据被插入时:CREATE_VERSION记录插入数据的事务ID,DELETE_VERSION为空。
  • 数据被删除时:DELETE_VERSION记录删除数据的事务ID,CREATE_VERSION为空。
  • 数据被修改时:将修改视为“删除旧数据,插入新数据”,即先将原有数据复制一份,原有数据的DELETE_VERSION记录修改数据的事务ID,CREATE_VERSION为空。复制出来的新数据的CREATE_VERSION记录修改数据的事务ID,DELETE_VERSION为空。

数据是记录好了,怎么用呢?

会根据隔离级别来决定到底应该读取哪个版本的数据:

  • 隔离级别是可重复读:总是读取CREATE_VERSION小于或等于当前事务ID的记录,在这个前提下,如果数据仍有多个版本,则取最新(事务ID最大)的。
  • 隔离级别是读已提交:总是取最新的版本即可,即最近被Commit的那个版本的数据记录。