数据库事务

一、 事务

事务(Transaction),即满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。

ACID

  • A 原子性 Atomicity
    事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
    回滚可以用回滚日志来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。

  • C 一致性 Consistency
    数据库在事务执行前后都保持一致性状态。
    在一致性状态下,所有事务对一个数据的读取结果都是相同的。

  • I 隔离性 Isolation
    一个事务所做的修改在最终提交以前,对其它事务是不可见的。

  • D 持久性 Durability
    一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
    使用重做日志来保证持久性。

关系

原子性 + 隔离性 -> 一致性

原子性与隔离性共同在并发的情况下保证了一致性,即数据的正确性,最后正确的数据在数据库中持久化保存。

因此,一致性是核心。

一致性问题

串行的情况下,隔离性自然保证,保证了基本的原子性,就可以保证一致性。

并发的情况下,隔离性难以保证,由此引发了一系列不一致的问题:

  • 写问题

    • 更新丢失
      • 第一类:A 的撤销覆盖了 B 的修改,B 的更新丢失
      • 第二类:A 的修改覆盖了 B 的修改,B 的更新丢失
  • 读问题:

    • 读脏数据
      B 读取 A 已修改但未提交的数据,但随后 A 再次进行了修改(或回滚),B 读到了脏数据(中间数据,垃圾数据)。

    • 不可重复读
      A 修改/删除了数据,B 在 A 修改/删除数据前后两次读取,B 读取的内容不一致。
      与读脏数据的区别在于 A 是否已提交。

    • 幻影读
      A 新增了数据,B 在 A 新增数据前后两次读取,B 读取的内容不一致。
      与不可重复读的区别在于是同一条数据的修改,还是读到了不同的数据。

解决这些一致性问题,重点在于解决隔离性问题,因此设置了 隔离级别

二、 隔离级别

  1. 未提交读 READ_UNCOMMITTED:事务中的修改,即使没有提交,对其它事务也是可见的。

  2. 提交读 READ_COMMITTED:事务只能读取已经提交的事务所做的修改。

  3. 可重复读 REPEATABLE_READ:保证在同一个事务中多次读取同样数据的结果是一样的。

  4. 可串行化 SERIALIZABLE:强制事务串行执行。

四种级别,并发性依次下降,安全性依次上升。

对于 3 种读问题,4 种级别分别位于 4 个区间。即:

级别 读脏数据 不可重复读 幻影读
未提交读 允许 允许 允许
提交读 禁止 允许 允许
可重复读 禁止 禁止 允许
可串行化 禁止 禁止 禁止

隔离级别的具体实现是通过 来完成的。

三、 锁

读写锁

  • 读锁:共享锁,S 锁(Shared)
    加读锁后,其他事务只能进行读取,不能进行更新,即只能加读锁,不能加写锁。

  • 写锁:排他锁,X 锁(Exclusive)
    加写锁后,其他事务不能读取和更新,即不能加任何锁。

读写锁实现隔离级别

  1. 未提交读

    • 读:不加锁
    • 写:行级共享锁

    B 可以读到 A 已修改未提交的数据。因此会造成读脏数据问题。

  2. 提交读

    • 读:行级共享锁,读完该行立即释放
    • 写:行级排他锁,事务结束才能释放

    A 修改数据,会对该行加锁,直至事务结束,B 不会读到未提交的数据。因此可以解决脏读问题。
    A 和 B 可以同时读取某行,保证该行不会被修改。但读取结束后,数据可以被修改。因此会造成可重复读问题。

  3. 可重复读

    • 读:行级共享锁,事务结束才能释放
    • 写:行级排他锁,事务结束才能释放

    读锁写锁都是直到事务结束才能释放。因此可以解决不可重复读问题。
    但无法解决幻影读问题:

    A B
    1 select * from users where age < 20
    2 insert into users(age) values(18)
    3 select * from users where age < 20

    (1) A 对 1 检索出的 n 条数据加共享锁,B 无法修改或删除这 n 条数据。
    (2) B 新增 1 条数据。
    (3) A 检索出 n + 1 条数据。

    A 两次读取得到的数据条数并不一致,出现了幻影读的现象。

  4. 可串行化

    • 读:表级共享锁,事务结束才能释放
    • 写:表级排他锁,事务结束才能释放

    解决幻影读的问题。
    但是效率极为低下。

意向锁

如果想为一张表加表锁,需要检查表的每一行是否有冲突的行锁,扫描全表消耗很大,为了解决这个问题,引入意向锁。

意向锁是表级锁,是一种虚锁,仅代表有加锁的意图,同样分为 IS 和 IX。

  • 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
  • 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。

乐观锁

乐观锁指 乐观 地认为操作不会导致冲突,在操作数据前不加锁,在修改后,再去判断是否冲突。

数据库本身不提供乐观锁,需要程序员自己实现,通常可以利用 CAS 的思想。

  1. 为表添加版本字段,每次操作数据时版本号 + 1
  2. 取出数据时,记录版本号
  3. 计算数据后,再次取出版本号,判断是否一致
    • 一致:这段时间内,数据没有被修改过,将版本号 + 1 后放回数据
    • 不同:这段时间内,数据被修改过,重新取出数据,即执行 2

与乐观锁相对应,悲观锁 悲观 地认为此次操作会出现数据冲突,所以在进行操作前就加锁,保证操作的正确性。

悲观锁直接由数据库提供,如共享锁,排他锁,都是悲观锁的实现。

四、 多版本并发控制

隔离级别可以使用锁来实现,但效率不高。

多版本并发控制( MVCC )用于实现提交读和可重复读这两种隔离级别。

未提交读隔离级别总是读取最新的数据行,无需使用MVCC。
可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。

MVCC是通过在每行记录后面保存两个隐藏的列来实现的。

  • 行的创建时间
  • 行的过期时间(或删除时间)

这两列用系统版本号表示。每开始一个新的事务, 系统版本号 都会自动递增。
事务开始时刻的系统版本号会作为 事务版本号,用来和查询到的每行记录的版本号进行比较。

实现可重复读

SELECT

  • 只查找版本小于或等于当前事务版本的数据行:确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
  • 只查找删除版本大于当前事务版本或未定义删除版本的数据行:确保事务读取到的行,在事务开始之前未被删除。

INSERT

新插入的每一行保存当前系统版本号作为行版本号。

DELETE

删除的每一行保存当前系统版本号作为行删除标识。

UPDATE

  • 插入一行新记录,保存当前系统版本号作为行版本号。
  • 同时保存当前系统版本号到原来的行作为行删除标识。

相当于 INSERT + DELETE。




参考资料:
事务四大特征:原子性,一致性,隔离性和持久性(ACID)
CyC2018/CS-Notes

原文地址:https://www.cnblogs.com/JL916/p/12650421.html