为了实现一个正确的事务,你知道数据库有多努力吗?| SQL全面教程九:事务(5)彻底了解隔离级别与事务的实现| 8月更文挑战

770 阅读12分钟

隔离级别

隔离级别可结合下一部分并发事务可能产生的数据一致性问题一起了解。相信会更有帮助。

事务的(4种)隔离级别

多个事务并行执行时,就有可能出现脏读、不可重复读,幻读等问题,这些都是由于事务同时执行时产生的数据一致性问题,是事物的隔离性需要解决的。

不同的隔离级别对应着不同的并行执行时的一致性问题,同时也影响并发度和数据库的性能等。隔离级别越高,隔离性越好,通俗说是隔离的越严实,并发度就会越低,效率就越低,性能也越差。

因此,实际数据库使用要综合各方面,使用恰当的隔离级别。

SQL标准的事务隔离级别有4种:

读未提交-read uncommitted

  • 读未提交(read uncommitted):一个事务还没提交时,它做的变更就能被别的事务看到。

通常很少有数据库使用这个隔离级别,有的数据库即使设置改级别也不会生效,如PostgreSQL。因为它会产生脏读、不可重复读,幻读、丢失更新等所有可能的并发问题。

读提交-read committed

  • 读提交(read committed),或读已提交:一个事务提交之后,它做的变更才会被其他事务看到。

除MySQL/MariaDB之外,所有主流数据的默认级别基本都是RC-读提交。

可重复读-repeatable read

  • 可重复读(repeatable read):一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。可重复读隔离级别下,未提交变更对其他事务也是不可见的。

MySQL/MariaDB的InnoDB引擎默认的隔离级别。但RR会产生幻读现象,因此MySQL引入了间隙锁(Gap)解决该问题。但降低了并发度,且易发生死锁。

串行化-serializable

  • 串行化(serializable),或可串行化:事务对数据的操作相对于其他事务是完全不可见的(即完全隔离的),等同于事务一个接一个串行执行的效果。

可串行化是最高的隔离级别。可串行化通常基于锁实现,对同一行记录"写"会加写锁,"读"会加读锁,select中使用范围时,还会加范围锁(range-locks),当出现读写等锁冲突的时候,后访问的事务必须等之前的事务结束释放锁之后,才能继续执行。

隔离级别小总结

隔离级别的实现通常通过加锁(lock)的方式实现,加锁会丢失一定的并发度,高隔离级别会增加系统的锁定开销,同时死锁的可能性也会增加。使用低级别的隔离时,开发人员需要仔细确认避免产生难以察觉的错误。

高隔离级别包含低隔离级别,因此允许在一个高的隔离级别下运行请求的隔离级别的事务。

Oracle数据库不支持READ UNCOMMITTEDREPEATABLE READ隔离级别。

READ UNCOMMITTED也称为浏览访问(browse access); READ COMMITTED称为游标稳定(cursor stability); REPEATABLE READ可能发生幻读。 SERIALIZABLE称为隔离。

SQL和SQL2标准的默认事务隔离级别为SERIALIZABLE

隔离级别描述了正在更新(或读取)的数据对其他事务可见的程度。(The isolation level describes the degree to which the data being updated is visible to other transactions)

关于快照隔离

Snapshot快照隔离级别,是由RDBMS自己提供和实现的隔离级别,说它是隔离级别,更多的应该说是隔离方法或方案。它的实现是形成一个当前数据的快照,在这个快照的基础上进行读取、修改操作。多版本并发控制中,多会用到这种方案,减少锁产生的阻塞问题。

PostgreSQL中RR(可重复读)隔离级别的实现,就是使用的数据快照形式;SQL Server开启READ_COMMITTED_SNAPSHOT,会在读提交隔离级别下使用快照,即使用行版本维护事务的并发访问,减少阻塞;SQL Server中可以专门设置和使用Snapshot隔离级别。

在快照隔离中,事务中任何语句读取的数据将是事务开始时存在的数据的事务一致版本。当前事务开始后其他事务所做的数据修改对当前事务中执行的语句是不可见的。

SNAPSHOT事务在读取数据时不请求锁定。SNAPSHOT事务读取数据不会阻止其他事务写入数据。写入数据的事务不会阻止SNAPSHOT事务读取数据。但是,SNAPSHOT事务写入数据会阻止其他(SNAPSHOT)事务写入相同数据。快照隔离更不容易发生冲突,能提供事务执行的并发度。

快照隔离级别,和实行快照方案的其他隔离级别还是有很大的不同。主要快照隔离遵循的是在事务开始结束的整个过程中,都是使用事务开始时版本的数据,从而不会产生脏读、丢失更改、不可重复读和幻读等问题;而通过快照方案实现的其他隔离级别,还是会产生对应的并发问题。

但是,无论什么隔离级别,在多个事务修改写入同一个数据时,都会发生冲突,而产生执行阻塞或中断。

比如,快照隔离级别下(SQL Server),两个事务修改同一数据会发生如下报错,中止执行。

Snapshot isolation transaction aborted due to update conflict. You cannot use snapshot isolation to access table 'dbo.StudentMarks' directly or indirectly in database 'AdventureWorks2016' to update, delete, or insert the row that has been modified or deleted by another transaction. Retry the transaction or change the isolation level for the update/delete statement.

SQL Server中的快照隔离级别:

快照隔离级别是从SQL Server2005开始引入的。

在快照隔离中,每个事务的更新行版本都在TempDB中维护。一旦事务开始,就会忽略表中插入或更新的所有新行、删除的记录。

事务的实现机制

事务的实现相对比较复杂,它是基于多种机制和方案共同保障而做到了。

比如,通过回滚机制实现事务的原子性;借助锁机制和MVCC(多版本并发控制)保证事务的隔离性和并发性;基于WAL、Checkpoint、Crash Recovery、重做或回滚等机制实现事务的持久性;通过约束一致性(唯一索引、外键、check、NOT NULL等完整性约束)和数据一致性(由原子性、隔离性、持久性等机制保证,尤其是持久性保证数据的一致不丢失),实现数据库系统的稳定状态。

回滚机制

回滚机制,就是在事务发生错误的情况下,数据库可以恢复到事务开始时的状态的能力。它是保证原子性的核心,也是事务并发调度的重要属性。

锁Locking

锁(加锁或封锁)机制,是实现事务的并发控制与隔离性的最常用的方案。

锁机制就是事务在对某个数据对象(如表、记录等)操作之前,先向系统发出请求,对其加锁;加锁后事务就对该数据对象有了一定的控制,在释放它的锁之前,其它的事务不能更新此数据对象。

加锁后具有什么样的控制由锁的类型决定。比如排它锁(Exclusive Locks,简记为X锁)、共享锁(Share Locks,简记为S锁)、读意向锁、写意向锁、间隙锁等,此外依据锁的范围,还可以分为表级锁、行级锁。它们综合作用,保证多个事务并发操作数据时,事物间各自的隔离和互不干扰。

锁使得对数据记录的访问以互斥的方式进行,即当一个事务访问某个数据记录时,其他任何事务都不能修改该数据记录。这是保证隔离性的方法之一(互斥)。

后续会详细介绍锁相关内容

MVCC

多版本并发控制(Multi-Version Concurrency Control,简称“MVCC”)是一种并发控制的方法,用于实现数据库的并发访问。与封锁机制的目标类似,但是比加锁有着更高的并发度、可以减少事务阻塞等。任何情况下MVCC都不会阻塞读操作,只有在多个写操作更新相同行记录时,才会有阻塞事务的冲突

MVCC协议确保每个事务都只看到与该事务启动时的快照视图相一致的版本数据;每个事务只看到数据的一个快照,该快照只包含那些在事务启动时已经提交的数据。这个快照并不等于数据的当前状态。

MVCC的关键是要高效地维护每一行记录的不同版本,每个版本表示事务开始时,所有已经提交的数据的最新结果。

MVCC实现的也是事务的隔离和并发控制。它的目的是,让读操作不阻塞写操作,写操作不阻塞读操作,提高数据库访问的并发度。数据读取的是行记录的最新版本。写操作会创建独有的隔离的行记录副本,用以更新。

后续会详细介绍MVCC相关内容

MVCC只有同时写同一个数据(或同一行记录)时才会产生冲突阻塞。而标准的两阶段封锁协议(加锁和释放锁在事务的开始和结束,分两个阶段执行),读操作和写操作都可能被阻塞,因为每个数据库对象只有一个版本,读操作和写操作在访问任何数据前都需要获得相应的锁

WAL

WAL(Write-Ahead Logging)是一种实现事务日志的标准方法。WAL的核心机制是,在对数据进行任何变更之前,先确保对数据库的所有修改操作都已经记录到事务日志中,从而保证事务的更新操作不会丢失。即先写日志到磁盘的机制,数据可以后续再刷新到磁盘。

WAL的目的在于保证日志的完整和正确,从而完成事务的持久性,支持数据库的灾难恢复(Crash Recovery),防止数据丢失,而且先写日志将数据的随机写改为日志的顺序写,这样也提高了数据库的处理性能。

WAL日志未必记录的是事务,也有的会直接记录数据(便于直接进行数据恢复等),比如MySQL的redolog。不同RDBMS、不同版本都可能会有所差异。

Checkpoint

Checkpoint是事务日志中的检查点。

Checkpoint机制的基本过程是这样的。当产生检查点时,到该检查点为止的所有的内存数据页都会被刷新写入到硬盘中,并在WAL事务日志中写入该检查点记录。也就是,检查点机制保证检查点之前的所有数据都已经写入到了数据库文件中。

数据库如果发生故障,恢复过程中会从最后的检查点开始,重做这个检查点之后的事务日志,即重做日志,使数据库恢复到一致性状态。

从异常恢复的角度来说,检查点之前的WAL日志是不需要的,这样就可以循环使用日志文件;不同的数据库有不同的处理机制,比如MySQL的binlog则不会循环覆盖,而是归档便于数据库的同步或重现恢复(当然binlog中没有检查点的概念,而是redolog中使用checkpoint)。

Checkpoint机制的作用在于:保证数据库的一致性,将缓冲区的脏数据写入硬盘,使内存和硬盘中的数据一致;使WAL日志尽快失效,便于重用和节省占用的硬盘空间;缩短数据库崩溃的恢复时间,如果不进行Checkpoint,WAL日志就一直不会失效,数据库系统在重启过程中需要重做的WAL事务日志就会非常庞大,导致恢复时间过长。

Checkpoint表示日志可以清除的位置,表示数据已经写入到数据库文件的位置,表示Crash Recovery异常恢复时重做日志开始的位置。

Crash Recovery

Crash Recovery也叫crash-safe。crash-safe的能力就是做到,即使数据库异常重启,已经提交的记录都不会丢失。

数据库系统发生异常终止的原因有很多,通常都是由于数据库服务进程被强行终止。可能的情况有:内存不足时被OOM Killer终止(Out Of Memory Killer)、操作系统崩溃、服务器停机、服务器重启、服务器发生硬件故障等。

Crash Recovery机制保证当发生数据库系统(服务进行)异常终止时,数据库能够从灾难中恢复到某个一致性状态。故障恢复/灾难恢复/异常恢复,就是要做到,只要硬盘上的数据和日志没有丢失,数据库就不会丢失数据,这是数据库系统的设计目标。

不丢失数据的含义如下:

  • 数据库实例或服务还能再次启动。
  • 数据库重启后,已经提交的事务还在。
  • 当数据库处于一致性状态时,不会出现数据错乱的情况。

Crash Recovery通常需要多个文件和操作共同配合才能实现。比如MySQL中可能会用到redolog、undolog、binlog、数据文件等;PostgreSQL中可能会用到WAL日志文件、控制文件(Checkpoint等信息)、事务状态文件、数据文件等。

数据库完整性

数据库完整性约束是保证事务一致性的重要方式。使得对数据库的修改和操作满足一定的约束条件,保证数据库状态的稳定。

在介绍数据库完整性中会详细说明相关内容。