事务

数据库事务通常包含了一个序列的对数据库的读写操作。包含有以下两个目的:

为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。 当事务被提交给了数据库管理系统,需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中。 如果事务中有的操作没有成功完成,则事务中的所有操作都需要回滚,回到事务执行前的状态。 同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。

特点

从业务角度出发,对数据库的一组操作要求保持 4 个特征:原子性、一致性、隔离性、持久性。

为了更好地理解 ACID,可以看 ACID 专文。

隔离级别

并发事务带来的问题

  1. 更新丢失: 当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题。 最后的更新覆盖了由其他事务所做的更新,例如,两个编辑人员制作了同一文档的电子副本。 每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。 最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。 如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题。

  2. 脏读: 一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态。 另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些脏数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。

  3. 不可重复读: 一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或被删除了。

  4. 幻读: 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据。

幻读和不可重复读的区别: 不可重复读的重点是修改,在同一事务中,同样的条件,第一次读的数据和第二次读的数据不一样,因为中间有其他事务提交了修改。 幻读的重点在于新增或者删除,在同一事务中,同样的条件,第一次和第二次读出来的记录数不一样,因为中间有其他事务提交了插入、删除。

解决办法
  • 更新丢失通常是应该完全避免的。 但防止更新丢失,并不能单靠数据库事务控制器来解决,需要应用程序对要更新的数据加必要的锁来解决,防止更新丢失应该是应用的责任。

  • 脏读、不可重复读和幻读都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。 要么加锁,在读取数据前对其加锁,阻止其他事务对数据进行修改。 要么使用 MVCC,也称为多版本数据库,不用加任何锁。 它会通过一定机制生成一个数据请求时间点的一致性数据快照,并用这个快照来提供一定级别的一致性读取。 从用户的角度来看,好象是数据库可以提供同一数据的多个版本。

SQL 标准定义了四类隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的。 低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

第一级别:未提交读

  • 所有事务都可以看到其他未提交事务的执行结果
  • 本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少
  • 该级别引发的问题是脏读,读取到了未提交的数据

第二级别:已提交读

  • 这是大多数数据库系统的默认隔离级别,但不是 MariaDB、MySQL 默认的
  • 它满足了隔离的简单定义,一个事务只能看见已经提交事务所做的改变
  • 这种隔离级别出现的问题是不可重复读,意味着在同一个事务中执行相同的 select 语句时可能看到不一样的结果。 导致这种情况的原因可能有:
    • 有一个交叉的事务有新的 commit,导致了数据的改变;
    • 一个数据库被多个实例操作时,同一事务的其他实例在该实例处理其间可能会有新的 commit

第三级别:可重复读

  • 这是 MariaDB、MySQL 默认的事务隔离级别
  • 它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行
  • 此级别可能出现的幻读,当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当再读取该范围的数据行时,会出现幻读
  • InnoDB 通过 MVCC 机制、间隙锁解决幻读问题
MVCC

大多数据库事务型引擎实现都不是简单的行级锁。 基于提升并发性考虑,一般都同时实现了 MVCC,包括 Oracle、PostgreSQL,不过实现各不相同。

MVCC 的实现是通过保存数据在某一个时间点快照来实现的。也就是不管实现时间多长,每个事物看到的数据都是一致的。 分为乐观并发控制和悲观并发控制。

InnoDB 的 MVCC 通过在每行记录后面保存两个隐藏的列来实现。 这两个列一个保存了行的创建时间,一个保存行的过期时间。 当然存储的并不是真实的时间而是系统版本号。 每开始一个新的事务,系统版本号都会自动新增。 事务开始时刻的系统版本号会作为事务的版本号,用来查询到每行记录的版本号进行比较。

第三隔离级别下 MVCC 如何工作:

  • SELECT 只查找版本早于当前事务版本的数据行,这样可以确保事务读取的行要么是在开始事务之前已经存在要么是事务自身插入或者修改过的。 行的删除版本号要么未定义,要么大于当前事务版本号,这样可以确保事务读取到的行在事务开始之前未被删除。 只有符合上述两个条件的才会被查询出来。

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

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

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

保存这两个版本号,使大多数操作都不用加锁。使数据操作简单,性能很好,并且能保证只会读取到复合要求的行。 不足之处是每行记录都需要额外的存储空间,需要做更多的行检查工作和一些额外的维护工作。

MVCC 只在第二三两种隔离级别下工作。 可以认为MVCC是行级锁一个变种,但是他很多情况下避免了加锁操作,开销更低。 虽然不同数据库的实现机制有所不同,但大都实现了非阻塞的读操作,读不用加锁,且能避免出现不可重复读和幻读。 写操作也只锁定必要的行,写必须加锁,否则不同事务并发写会导致数据不一致。

第四级别:可串行读

  • 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题
  • 它在每个读的数据行上加上共享锁
  • 在这个级别,可能导致大量的超时现象和锁竞争

隔离级别

隔离级别脏读丢失更新不可重复读幻读
未提交读✔️✔️✔️✔️
已提交读✔️✔️
可重复读✔️
可串行读

各数据库并不一定完全实现了四个隔离级别。

Oracle 只提供 Read committed 和 Serializable 两个标准隔离级别,另外还提供自己定义的 Read only 隔离级别。

SQL Server 除支持这些级别外,还支持一个叫做快照的隔离级别,但严格来说它是一个用 MVCC 实现的 Serializable 隔离级别。

MariaDB、MySQL 都支持,但在具体实现时,有一些特点,比如在一些隔离级别下是采用 MVCC 一致性读,但某些情况下又不是。

Mysql可以通过执行 set transaction isolation level 命令来设置隔离级别,新的隔离级别会在下一个事务开始的时候生效。 例如:set session transaction isolation level read committed;

事务日志

  • 使用事务日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中。 不用每次都将修改的数据本身持久到磁盘。

  • 事务日志采用的是追加的方式,因此写日志的操作是磁盘上一小块区域内的顺序 I/O ,而不像随机I/O需要在磁盘的多个地方移动磁头。 采用事务日志的方式相对来说要快得多。

  • 事务日志持久以后,内存中被修改的数据在后台可以慢慢刷回到磁盘。

  • 如果数据的修改已经记录到事务日志并持久化,但数据本身没有写回到磁盘,此时系统崩溃,存储引擎在重启时能够自动恢复这一部分修改的数据。

目前来说,大多数存储引擎都是这样实现的,通常称之为预写式日志,修改数据需要写两次磁盘。

实现原理

事务的实现是基于数据库的存储引擎,不同的存储引擎对事务的支持程度不一样。

InnoDB 是 MariaDB 默认的存储引擎,默认的隔离级别是可重复读,并且在可重复读的隔离级别下更进一步,加入了 MVCC 和 间隙锁。

因此 InnoDB 的可重复读隔离级别其实实现了串行化级别的效果,而且保留了比较好的并发性能。

事务的隔离性是通过锁实现,而事务的原子性、一致性和持久性则是通过事务日志实现。说到事务日志就得提一下 redo 和 undo。

  • redo log

事务日志通过重做 (redo) 日志和日志缓冲 (InnoDB Log Buffer) 实现。 事务开启时,事务中的操作都会先写入存储引擎的日志缓冲中,在事务提交之前,这些缓冲的日志都需要提前刷新到磁盘上持久化。 这就是预写日志。

当事务提交之后,在 Buffer Pool 中映射的数据文件才会慢慢刷新到磁盘。 此时如果数据库崩溃或者宕机,那么当系统重启进行恢复时,就可以根据 redo log 中记录的日志,把数据库恢复到崩溃前的一个状态。 未完成的事务可以继续提交也可以选择回滚,这基于恢复的策略而定。

  • undo log undo log 主要为事务的回滚服务。 在事务执行的过程中,除了记录 redo log,还会记录一定量的 undo log。 undo log 记录了数据在每个操作前的状态,如果事务执行过程中需要回滚,就可以根据 undo log 进行回滚操作。 单个事务的回滚,只会回滚当前事务做的操作,并不会影响到其他的事务做的操作。

  • 开始事务

  • 记录 A=1 到 undo log
  • update A = 3
  • 记录 A=3 到 redo log
  • 记录 B=2 到 undo log
  • update B = 4
  • 记录B = 4 到redo log
  • 将 redo log 刷新到磁盘
  • 提交

在 1 - 8 的任意一步系统宕机,事务未提交,该事务就不会对磁盘上的数据做任何影响。 在 8 - 9 系统宕机,恢复之后可以选择回滚,也可以选择继续完成事务提交,因为此时 redo log 已经持久化。 若在 9 之后系统宕机,内存映射中变更的数据还来不及刷回磁盘,那么系统恢复之后,可以根据 redo log 把数据刷回磁盘。

所以,redo log 其实保障的是事务的持久性和一致性,而 undo log 则保障了事务的原子性。

事务使用

  • START TRANSACTIONBEGIN 开始事务
  • COMMIT 提交
  • ROLLBACK 回滚
  • CHAIN 在事务提交或者回滚之后会立即启动一个新事务,并且和刚才的事务具有相同的隔离级别
  • RELEASE 在事务提交或者回滚之后会断开和客户端的连接。
  • SET AUTOCOMMIT 可以修改当前连接的提交方式,如果设置了 SET AUTOCOMMIT=0,则需要通过明确的命令进行提交或者回滚

注意点:

  • 在锁表期间用 start transaction 命令开始一个新事务,会造成一个隐含的 unlock tables 被执行
  • 在同一个事务中,最好不使用不同存储引擎的表,否则 ROLLBACK 时需要对非事务类型的表进行特别的处理,因为 COMMIT、ROLLBACK 只能对事务类型的表进行提交和回滚
  • 所有的 DDL 语句是不能回滚的,并且部分的 DDL 语句会造成隐式的提交
  • 在事务中可以通过定义 SAVEPOINT 指定回滚事务的一个部分,但是不能指定提交事务的一个部分。 对于复杂的应用,可以定义多个不同的 SAVEPOINT,满足不同的条件时,回滚不同的 SAVEPOINT。 需要注意的是,如果定义了相同名字的 SAVEPOINT,则后面定义的 SAVEPOINT 会覆盖之前的定义。 对于不再需要使用的 SAVEPOINT,可以通过 RELEASE SAVEPOINT 删除,删除后不能再执行 ROLLBACK TO SAVEPOINT。
  • 默认采用自动提交模式,可以通过设置 autocommit 来启用或禁用自动提交模式
powered by Gitbook该文件修订时间: 2020-04-10 10:05:54

results matching ""

    No results matching ""

    results matching ""

      No results matching ""