ddia-事务

transaction

事务是一个抽象层,允许应用程序假装某些并发问题和某些类型的硬件和软件故障不存在。各式各样的错误被简化为一种简单情况:事务中止(transaction abort),而应用需要的仅仅是重试。

如果没有事务处理,各种错误情况(进程崩溃,网络中断,停电,磁盘已满,意外并发等)意味着数据可能以各种方式变得不一致。例如,非规范化的数据可能很容易与源数据不同步。如果没有事务处理,就很难推断复杂的交互访问可能对数据库造成的影响。

本章深入讨论了并发控制的话题。我们讨论了几个广泛使用的隔离级别,特别是:

  • 读已提交,
  • 快照隔离(有时称为可重复读)
  • 可序列化

并通过研究竞争条件的各种例子,来描述这些隔离等级:

脏读

一个客户端读取到另一个客户端尚未提交的写入。读已提交或更强的隔离级别可以防止脏
读。
image

要求读锁的办法在实践中效果并不好。因为一个长时间运行的写入事务会迫使许多只读
事务等到这个慢写入事务完成。这会损失只读事务的响应时间,并且不利于可操作性.

大多数数据库使用如下方式防止脏读:对于写入的每个对象,数据库都
会记住旧的已提交值,和由当前持有写入锁的事务设置的新值。 当事务正在进行时,任何其
他读取对象的事务都会拿到旧值。 只有当新值提交后,事务才会切换到读取新值。

脏写

一个客户端覆盖写入了另一个客户端尚未提交的写入。几乎所有的事务实现都可以防止脏
写。

数据库通过使用行锁(row-level lock) 来防止脏写:当事务想要修改特定
对象(行或文档)时,它必须首先获得该对象的锁。然后必须持有该锁直到事务被提交或中
止。一次只有一个事务可持有任何给定对象的锁;如果另一个事务要写入同一个对象,则必
须等到第一个事务提交或中止后,才能获取该锁并继续。这种锁定是读已提交模式(或更强
的隔离级别)的数据库自动完成的。

读取偏差(不可重复读)

在同一个事务中,客户端在不同的时间点会看见数据库的不同状态。快照隔离经常用于解决
这个问题,它允许事务从一个特定时间点的一致性快照中读取数据。快照隔离通常使用多版
本并发控制(MVCC) 来实现。

image

image

更新丢失

两个客户端同时执行读取-修改-写入序列。其中一个写操作,在没有合并另一个写入变更情
况下,直接覆盖了另一个写操作的结果。所以导致数据丢失。快照隔离的一些实现可以自动
防止这种异常,而另一些实现则需要手动锁定( SELECT FOR UPDATE )。

使用提供的原子操作或显示锁定

写偏差

一个事务读取一些东西,根据它所看到的值作出决定,并将决定写入数据库。但是,写作的
时候,决定的前提不再是真实的。只有可序列化的隔离才能防止这种异常。

幻读

事务读取符合某些搜索条件的对象。另一个客户端进行写入,影响搜索结果。快照隔离可以
防止直接的幻像读取,但是写入歪斜环境中的幻影需要特殊处理,例如索引范围锁定。

弱隔离级别可以防止这些异常情况,但是让应用程序开发人员手动处理其他应用程序(例
如,使用显式锁定)。只有可序列化的隔离才能防范所有这些问题。

可序列化与两阶段锁

数十年来,两阶段锁定一直是实现可序列化的标准方式,但是许多应用出于性能问题的考虑避免使用它。

在2PL中,写入不仅会阻塞其他写入,也会阻塞读,反之亦然。快照隔离使得读不阻塞写,写
也不阻塞读(参阅“实现快照隔离”),这是2PL和快照隔离之间的关键区别。另一方面,因为
2PL提供了可序列化的性质,它可以防止早先讨论的所有竞争条件,包括丢失更新和写入偏
差。

实现两阶段锁

读与写的阻塞是通过为数据库中每个对象添加锁来实现的。锁可以处于共享模式(shared
mode)或独占模式(exclusive mode)。

  • 若事务要读取对象,则须先以共享模式获取锁。允许多个事务同时持有共享锁。但如果另一个事务已经在对象上持有排它锁,则这些事务必须等待

  • 若事务要写入一个对象,它必须首先以独占模式获取该锁。没有其他事务可以同时持有锁(无论是共享模式还是独占模式),所以如果对象上存在任何锁,该事务必须等待。

  • 如果事务先读取再写入对象,则它可能会将其共享锁升级为独占锁。升级锁的工作与直接获得排他锁相同。

  • 事务获得锁之后,必须继续持有锁直到事务结束(提交或中止)。这就是“两阶段”这个名字的来源:第一阶段(当事务正在执行时)获取锁,第二阶段(在事务结束时)释放所有的锁。

例子

image

事务 B 的 update 语句会被阻塞,直到事务 A 执行 commit 之后,事务 B 才能继续执行。

也就是说,在 InnoDB 事务中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。

-->