数据库事务

数据库事务

数据库事务(Database Transaction) ,具有原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)

  • 原子性:原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败
  • 一致性:事务必须使数据库从一个一致性状态变换到另外一个一致性状态
  • 隔离性:并发执行的事务不会相互影响,其对数据库的影响和它们串行执行时一样
  • 持久性:持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的

持久性(Durability):事务一旦提交,其对数据库的更新就是持久的。任何事务或系统故障都不会导致数据丢失。

在事务的ACID特性中,C即一致性是事务的根本追求,而对数据一致性的破坏主要来自两个方面

1.事务的并发执行
2.事务故障或系统故障
数据库系统是通过并发控制技术和日志恢复技术来避免这种情况发生的。
ACID
并发控制技术保证了事务的隔离性,使数据库的一致性状态不会因为并发执行的操作被破坏。
日志恢复技术保证了事务的原子性,使一致性状态不会因事务或系统故障被破坏。同时使已提交的对数据库的修改不会因系统崩溃而丢失,保证了事务的持久性。

理想状态下,事务的隔离性保存了数据库向串行一样的执行,但是在一定程度的并发情况下,严重影响数据库的性能,所以将隔离性化分为不同的等级,在不同的要求或场景下使用不同的等级。就好比安检,在国庆期间安检等级是最高的,必须一个人一个人的接受检查,然后再通过安全通道。平时的话保安看人比较多就管的不那么严格了,有人可能着急就和别人一并过去了。所以在不同的场景下进行不同的隔离要求。
MYSQL数据库设置了四种隔离级别,从低到高分别是读未提交(READ UNCOMMITTED)、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)、串行化(SERIALIZABLE)。其中串行化是最高级别的,保证事务能够串行执行,真正可以实现数据库隔离性要求的。那么其他三种低级别的隔离必然会导致一些问题存在。下面依次看下这些问题。

  • 脏写

一个事务回滚导致别的事务提交的数据也被回滚了

TX-A TX-B
————– ————-
read X=100
read X=100
write X=X+200
commit X=300
write X=X+200
roolback X=100
  • 丢失更新

如果两个事务都更新同一个字段时:

TX-A TX-B
read X=100
read X=100
write X=X+200
write X=X+300
commit X=300
commit X=400
TX-A将X加200,应该是300,然后TX-B将X加300,应该是600,结果最后X是400,相当于把TX-A加的200给丢了
  • 脏读

一个事务读取到了另一个事务未提交的数据:

TX-A TX-B
———– ————-
read X=100
read X=100
write X=X+300
read X=400

在事务TX-A中读取了TX-B还没有提交的数据,如果TX-B回滚,则TX-A计算就出错了

  • 不可重复读
    一个事务读取到了另一个事务已提交的数据:
TX-A TX-B
———– ————-
read X=100
read X=100
write X=X+300
commit X=400
read X=400
在事务TX-A中读取了TX-B已提交的数据,这样会导致TX-A在一个事务内每次读取的数据不一样,其结果依赖于别人并行提交的数据。与脏读的区别就是读取的数据是另一个事务提交前的还是提交后的数据。
  • 幻读
    一个事务读取到了另一个事务插入的数据:
TX-A TX-B
read count(*) = 5
write insert 1
commit X=400
read count(*) = 6

虚读(幻读)是指在一个事务内读取到了别的事务插入的数据,导致前后读取的数据数量不同。

不同的事务隔离级别可能会出现不同的等级错误
|隔离级别|脏写|脏读|不可重复读|幻读|丢失更新|
| ————————– | — | —- | —- | —- | —- |
| 读未提交(READ UNCOMMITTED) | | 可能 | 可能 | 可能 | 可能 |
| 读已提交(READ COMMITTED) | | | 可能 | 可能 | 可能 |
| 可重复读(REPEATABLE READ) | | | | 可能 | |
| 串行化(SERIALIZABLE) | | | | | |

隔离实现

并发控制主要是通过锁的机制实现,从“心里预期”来说可以分为两种,分别是乐观锁、悲观锁。这里说的锁是一个抽象概念,是一种实现机制,并不一定是有锁这个类或者API等实体的东西。

  • 乐观锁:在并发执行时,假定不会发生资源竞争,允许执行,只有在真正发生冲突时才会解决冲突,比如事务回滚,再次尝试等。
  • 悲观锁:在并发执行时,已经认为会发生资源竞争,所以只能按照串行执行,一刀切,爱谁谁。

基于封锁的并发控制

核心思想:对于并发可能冲突的操作,比如读-写,写-读,写-写,通过锁使它们互斥执行

锁通常分为共享锁和排他锁两种类型

  1. 共享锁(S):事务T对数据A加共享锁,其他事务只能对A加共享锁但不能加排他锁。

  2. 排他锁(X):事务T对数据A加排他锁,其他事务对A既不能加共享锁也不能加排他锁
    基于锁的并发控制流程:

  3. 事务根据自己对数据项进行的操作类型申请相应的锁(读申请共享锁,写申请排他锁)

  4. 申请锁的请求被发送给锁管理器。锁管理器根据当前数据项是否已经有锁以及申请的和持有的锁是否冲突决定是否为该请求授予锁。

  5. 若锁被授予,则申请锁的事务可以继续执行;若被拒绝,则申请锁的事务将进行等待,直到锁被其他事务释放。

可能出现的问题:

  1. 死锁:多个事务持有锁并互相循环等待其他事务的锁导致所有事务都无法继续执行。

  2. 饥饿:数据项A一直被加共享锁,导致事务一直无法获取A的排他锁。

对于可能发生冲突的并发操作,锁使它们由并行变为串行执行,是一种悲观的并发控制。

基于时间戳的并发控制

核心思想:对于并发可能冲突的操作,基于时间戳排序规则选定某事务继续执行,其他事务回滚

系统会在每个事务开始时赋予其一个时间戳,这个时间戳可以是系统时钟也可以是一个不断累加的计数器值,当事务回滚时会为其赋予一个新的时间戳,先开始的事务时间戳小于后开始事务的时间戳。

每一个数据项Q有两个时间戳相关的字段:
W-timestamp(Q):成功执行write(Q)的所有事务的最大时间戳
R-timestamp(Q):成功执行read(Q)的所有事务的最大时间戳

时间戳排序规则如下:

假设事务T发出read(Q),T的时间戳为TS

  1. 若TS(T)<W-timestamp(Q),则T需要读入的Q已被覆盖。此read操作将被拒绝,T回滚。
  2. 若TS(T)>=W-timestamp(Q),则执行read操作,同时把R-timestamp(Q)设置为TS(T)与R-timestamp(Q)中的最大值

假设事务T发出write(Q)

  1. 若TS(T)<R-timestamp(Q),write操作被拒绝,T回滚。
  2. 若TS(T)<W-timestamp(Q),则write操作被拒绝,T回滚。
  3. 其他情况:系统执行write操作,将W-timestamp(Q)设置为TS(T)。

基于时间戳排序和基于锁实现的本质一样:对于可能冲突的并发操作,以串行的方式取代并发执行,因而它也是一种悲观并发控制。它们的区别主要有两点:

基于锁是让冲突的事务进行等待,而基于时间戳排序是让冲突的事务回滚。
基于锁冲突事务的执行次序是根据它们申请锁的顺序,先申请的先执行;而基于时间戳排序是根据特定的时间戳排序规则。

基于有效性检查的并发控制

核心思想:事务对数据的更新首先在自己的工作空间进行,等到要写回数据库时才进行有效性检查,对不符合要求的事务进行回滚

基于有效性检查的事务执行过程会被分为三个阶段:

  1. 读阶段:数据项被读入并保存在事务的局部变量中。所有write操作都是对局部变量进行,并不对数据库进行真正的更新。
  2. 有效性检查阶段:对事务进行有效性检查,判断是否可以执行write操作而不违反可串行性。如果失败,则回滚该事务。
  3. 写阶段:事务已通过有效性检查,则将临时变量中的结果更新到数据库中。

有效性检查通常也是通过对事务的时间戳进行比较完成的,不过和基于时间戳排序的规则不一样。

该方法允许可能冲突的操作并发执行,因为每个事务操作的都是自己工作空间的局部变量,直到有效性检查阶段发现了冲突才回滚。因而这是一种乐观的并发策略。

基于快照隔离的并发控制

其核心思想是:数据库为每个数据项维护多个版本(快照),每个事务只对属于自己的私有快照进行更新,在事务真正提交前进行有效性检查,使得事务正常提交更新或者失败回滚。快照隔离是多版本并发控制(mvcc)的一种实现方式

由于快照隔离导致事务看不到其他事务对数据项的更新,为了避免出现丢失更新问题,可以采用以下两种方案避免:

  1. 先提交者获胜:对于执行该检查的事务T,判断是否有其他事务已经将更新写入数据库,是则T回滚否则T正常提交。

  2. 先更新者获胜:通过锁机制保证第一个获得锁的事务提交其更新,之后试图更新的事务中止。

事务间可能冲突的操作通过数据项的不同版本的快照相互隔离,到真正要写入数据库时才进行冲突检测。因而这也是一种乐观并发控制。

MYSQL- InnoDB锁

InnoDB引擎使用了七种类型的锁,他们分别是:

  • 共享排他锁(Shared and Exclusive Locks)
  • 意向锁(Intention Locks)
  • 记录锁(Record Locks)
  • 间隙锁(Gap Locks)
  • Next-Key Locks
  • 插入意图锁(Insert Intention Locks)
  • 自增锁(AUTO-INC Locks)

共享排他锁(Shared and Exclusive Locks)

  • 如果一个事务对某一行数据加了S锁,另一个事务还可以对相应的行加S锁,但是不能对相应的行加X锁。
  • 如果一个事务对某一行数据加了X锁,另一个事务既不能对相应的行加S锁也不能加X锁。

意向锁(Intention Locks)

如果事务A申请了行锁(写锁),事务B申请了表锁(写锁),那么这两个事务就会发冲突。为了解决这类问题引入了意向锁。

  • 意向锁分为意向读锁(IS)和意向写锁(IX)
  • 意向锁是“表锁”,他并不会锁定表,只是显示某人正在锁定行,或者要锁定表中的行。当事务B申请表写锁时,发现该表已经有意向写锁,则会被阻塞。

这样也就解决了上边的问题了,意向锁是由InnoDB自行实现的,用户无法操作

Record Locks、Gap Locks、Next-Key Locks

  1. 记录锁(Record Locks):记录锁锁定索引中一条记录。
  2. 间隙锁(Gap Locks):间隙锁要么锁住索引记录中间的值,要么锁住第一个索引记录前面的值或者最后一个索引记录后面的值。
  3. Next-Key Locks:Next-Key锁是索引记录上的记录锁和在索引记录之前的间隙锁的组合。

三种类型锁的锁定范围不同,且逐渐扩大。我们来举一个例子来简要说明各种锁的锁定范围,假设表t中索引列有3、5、8、9四个数字值,根据官方文档的确定三种锁的锁定范围如下:

  • 记录锁的锁定范围是单独的索引记录,就是3、5、8、9这四行数据。
  • 间隙锁的锁定为行中间隙,用集合表示为(-∞,3)、(3,5)、(5,8)、(8,9)、(9,+∞)。
  • Next-Key锁是有索引记录锁加上索引记录锁之前的间隙锁组合而成,用集合的方式表示为(-∞,3]、(3,5]、(5,8]、(8,9]、(9,+∞)。

最后补充几点:

  • 间隙锁阻止其他事务对间隙数据的并发插入,这样可有有效的解决幻读问题(Phantom Problem)。正因为如此,并不是所有事务隔离级别都使用间隙锁,MySQL InnoDB引擎只有在Repeatable Read(默认)隔离级别才使用间隙锁。
  • 间隙锁的作用只是用来阻止其他事务在间隙中插入数据,他不会阻止其他事务拥有同样的的间隙锁。这就意味着,除了insert语句,允许其他SQL语句可以对同样的行加间隙锁而不会被阻塞。
  • 对于唯一索引的加锁行为,间隙锁就会失效,此时只有记录锁起作用。
  • 行锁实现依赖于索引,一旦某个加锁操作没有使用到索引,那么该锁就会退化为表锁。
  • 记录锁存在于包括主键索引在内的唯一索引中,锁定单条索引记录。