构成单一逻辑工作单元的操作集合称作事务(transaction)。即使有故障,数据库系统也必须保证事务的正确执行——要么执行整个事务,要么属于该事务的操作一个也不执行。此外,数据库系统必须以一种能避免引入不一致性的方式来管理事务的并发执行。
事务是访问并可能更新各种数据项的一个程序执行单元(unit)。事务通常由高级数据操纵语言(如SQL)或编程语言(如Java)通过JDBC或ODBC嵌入式数据库访问书写的用户程序的执行所引起。事务用形如begin transaction和end transaction语句(或函数调用)来界定。事务由begin transcation与end transcation之间执行的全体操作组成。
因为事务是不可分割的,所以要么执行其全部内容,要么就根本不执行。因此,如果一个事务开始执行,但由于某些原因失败,则事务对数据库造成的任何可能的修改都要撤销。无论事务本身是否失败(如,事务中的某个操作执行异常),或者操作系统崩溃,或者计算机本身停止运行,这项要求都要成立。这种“全或无”的特性称为“原子性”(atomicity)。
此外,由于事务是一个单一的单元,它的操作不能看起来是被其他不属于该事务的数据库操作分隔开的。即使单条SQL语句也会涉及许多分开的数据库访问,并且一个事务可能会由多条SQL语句构成。因此数据库系统必须采取特殊处理确保事务正常执行而不被来自并发执行的数据库语句所干扰。这种特性称为隔离性(isolation)。
即使系统能保证一个事务的正确执行,但如果此后系统崩溃,也应保证系统能够记住该事务。因此,即使系统崩溃后,事务的操作也必须是持久的。这种特性称为持久性(durability)。
因为上述三个特性,事务就成了构造与数据库的交互的一种理想方式。事务必须保证数据库的一致性——如果一个事务作为原子从一个一致的数据库状态开始独立地运行,则事务结束时数据库也必须再次是一致的。这种一致性要求超出数据完整性约束(如主码约束、参照完整性、check约束等)。相反,事务会被期望的更多,以保证无法用SQL构建数据完整性的场景的一致性约束。这种场景是编写事务的程序员的职责。这种特性称为一致性(consistency)。
数据库需要维护事务的以下四个性质:
1.原子性(Atomicity)事务是一个原子操作,由一系列动作组成。事务的原子性确保这一系列动作要么全部完成,要么完全不起作用。
2.一致性(Consistency)隔离执行事务时(在没有其他事务并发的情况下),保持数据库的一致的数据库状态。
3.隔离性(Isolation)并发事务执行之间无影响,在一个事务内部的操作对其他事务是不产生影响,这需要事务隔离级别来指定隔离性。每个事务都感觉不到系统中其他事务在并发地执行。
4.持久性(Durability)一旦事务完成,数据库的改变必须是永久的,即使出现系统故障。
在简单模型中,数据项只包含一个单一的数据值。每个数据项由一个名字所标识(如一个字符,X、Y、Z等)。事务运用以下两个操作访问数据。
read(X):从数据库把数据项X传送到执行read操作的事务的主存缓冲区的一个也称为X的变量中。
write(X):从执行write事务的主存缓冲区的变量X中把数据项X传回数据库中。
注意,在实际数据库系统中,write操作不一定立即更新磁盘上的数据;write操作的结果可以临时存储在某处,以后再写到磁盘上。目前,我们假设write操作立即更新数据库。
事务并非总能成功地执行完成。这种事务称为中止(abort)事务。如果要确保原子性,中止事务必须对数据库的状态不造成影响。因此,中止事务对数据库所做过的任何改变必须撤销。一旦中止事务造成的变更被撤销,我们就说事务已回滚(rolled back)。恢复机制负责管理事务中止。典型的方法是维护一个日志(log)。每个事务对数据库的修改都会记录到日志中。我们先记录执行修改的事务标识符、修改的数据项标识符以及数据项的旧值(修改前的)和新值(修改后的)。然后数据库才会修改。(为什么先写日志,再写数据库?–保证修改可恢复)
成功完成执行的事务称为已提交(committed)。一个对数据库进行过更新的已提交事务使数据库进入一个新的状态,即使出现系统故障,这个状态也可以保持。
一旦事务已提交,则不能通过中止来撤销其造成的影响。撤销已提交事务所造成影响的唯一方法是执行一个补偿事务(compensating transcation)。书写和执行一个补偿事务的责任由用户执行,不是通过数据库系统来处理。
为更准确地定义一个事务完成的含义,建立了一个简单的抽象事务模型。事务必须处于以下状态之一。
活动的(active):初始状态,事务执行时处于这个状态。
部分提交的(partially committed):最后一条语句执行后。
失败的(failed):发现正常的执行不能继续后。
中止的(aborted) :事务回滚并且数据库已恢复到事务开始执行前的状态后。
提交的(committed):成功完成后。
事务从活动状态开始。当事务完成它的最后一条语句后就进入到部分提交状态。此刻,事务已经完成执行,但由于实际输出可能仍临时驻留在主存中,因此一个硬件故障可能阻止成功完成,于是事务仍有可能不得不中止。
接着数据库系统向磁盘上写入足够的信息,确保即使出现故障时,事务所做的更新也能在系统重启后重新创建。当最后一条这样的信息写完后,事务就进入提交状态。
系统判定事务不能继续正常执行后(如,硬件错误或逻辑错误),事务就进入失败状态。这种事务必须回滚。这样,事务就进入中止状态。此刻,系统有两种选择。
它可以重启(restart)事务,但仅当引起事务中止的硬件或不是由事务的内部逻辑锁产生的软件错误时。重启的事务被看成是一个新事务。
它可以杀死(kill)事务,这样做通常是由于事务的内部逻辑造成的错误,只有重写应用程序才能改正,或者由于输入错误,或所需数据在数据库中没有找到。
事务处理系统通常允许多个事务并发执行。在数据库中使用并发执行的动机在本质上与操作系统中使用多道程序(multiprogramming)的动机是一样的。
当多个事务并发地执行时,可能违背隔离性,这导致即使每个事务都正确执行,数据库的一致性也可能被破坏。数据库系统必须控制事务之间的交互,以防止它们破坏数据库的一致性。数据库系统通过称为并发控制机制(concurrency-control scheme)的一系列机制保证这一点。
当数据库系统并发地执行多个事务时,相应的调度不必是串行的。在多个事务的情形下,所有事务共享CPU时间。 在并发执行中,通过保证所执行的任何调度的效果都与没有并发执行的调度效果一样,我们可以确保数据库的一致性。
在实际应用中,数据库中的数据是要被多个用户共同访问的,在多个用户同时操作相同的数据时,可能就会出现一些事务并发的问题:
1.脏读(Dirty Read)。一个事务读取到另一个事务未提交的数据。
2.不可重复读(Non-repeatable Read)。一个事务对同一行数据重复读取两次,但得到的结果不同。如在两次读取操作之间,还有其他的事务更新该行数据。
3.虚读/幻读(Phantom Read)。一个事务执行两次查询,但第二次查询的结果包含了第一次查询中未出现的数据。不可重复读针对的是值的不同,幻读针对的是数据条数的不同。
4.丢失更新(Lost Update)。丢失更新可分为两类,分别是第一类丢失更新和第二类丢失更新。第一类丢失更新是指两个事务同时操作同一个数据时,当第一个事务撤销时,把已经提交的第二个事务的更新数据覆盖了,第二个事务就造成了数据丢失。第二类丢失更新是指当两个事务同时操作同一个数据时,第一个事务将修改结果成功提交后,对第二个事务已经提交的修改结果进行了覆盖,对第二个事务造成了数据丢失。简言之,丢失更新是指一个事务中的更新被另一个事务中的更新覆盖,并根据丢失更新的事务细分为第一类丢失更新和第二类丢失更新。
为了避免上述事务并发问题的出现,在标准的 SQL 规范中定义了四种事务隔离级别,不同的隔离级别对事务的处理有所不同:
1.Serializable(可串行化)
提供严格的事务隔离。它要求事务序列化执行,事务只能一个接一个地执行,不能并发执行。此隔离级别可有效防止脏读、不可重复读、幻读。但这个级别可能导致大量的超时现象和锁竞争,在实际应用中很少使用。
2.Repeatable Read(可重复读)
一个事务在执行过程中,可以访问其他事务成功提交的新插入的数据,但不可以访问成功修改的数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。此隔离级别可有效防止不可重复读和脏读。
3.Read Committed (已提交读)
一个事务在执行过程中,既可以访问其他事务成功提交的新插入的数据,又可以访问成功修改的数据。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。此隔离级别可有效防止脏读。
4.Read Uncommitted (未提交读)
一个事务在执行过程中,既可以访问其他事务未提交的新插入的数据,又可以访问未提交的修改数据。如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据。此隔离级别可防止丢失更新。
以上所有隔离性级别都不允许脏写(Dirty Write)。
一般来说,事务的隔离级别越高,越能保证数据库的完整性和一致性,但相对来说,隔离级别越高,对并发性能的影响也越大。因此,通常将数据库的默认隔离级别设置为已提交读Committed Read),它既能防止脏读,又能有较好的并发性能。虽然这种隔离级别会导致不可重复读、幻读和第二类丢失更新这些并发问题,但可通过在应用程序中采用悲观锁或乐观锁加以控制。
常见的关系型数据库的默认事务隔离级别采用的是READ_COMMITED,例如PostgreSQL、ORACLE、SQL Server和DB2。但是使用InnoDB引擎的MySQL数据库默认事务隔离级别是REPEATABLE_READ。
可以使用多种并发控制机制(Concurrency-Control Sheme)来保证,即使在有多个事务并发执行时,不管操作系统在事务之间如何分时共享资源(如CPU时间),都只产生可接受的调度。
并发控制机制的目的是获得高度的并发性,同时保证所产生的调度是冲突可串行化或视图可串行化的、可恢复的,并且无级联的。
一个事务可以封锁其访问的数据项,而不用封锁整个数据库。在这种策略下,事务必须在足够常的时间内持有锁来保证可串行化,但这一周期又要足够短,不会过度影响性能。一个简单且广泛用来确保可串行化的方案是两阶段封锁协议。简单来说,两阶段封锁要求一个事务由两个阶段,一个阶段只获得锁但不释放锁,第二个阶段只释放锁但不获得锁。
如果我们有两种锁,则封锁的结果将进一步得到改进:共享的和排他的。共享锁用于事务读的数据项,而排他锁用于事务写的数据项。这两种锁模式以及两阶段封锁协议在保证可串行化的前提下允许数据的并发读。
另一类实现隔离性的技术为每个事务分配一个时间戳(timestamp),通常是当它开始的时候。对于每个数据项,系统维护两个时间戳。数据项的读时间戳记录读该数据项的事务的最大(即最近的)时间戳。数据项的写时间戳记录写入该数据项当前值的事务的时间戳。时间戳用来确保在访问冲突情况下,事务按照事务时间戳的顺序来访问数据项。当不可能访问时,违例事务将会中止,并且分配一个新的时间戳重新开始。
通过维护数据项的多个版本,一个事务允许读取一个旧版本的数据项,而不是被另一个未提交或者在串行化序列中应该排在后面的事务写入的新版本的数据项。有许多版本并发控制技术。其中快照隔离(snapshot isolation)技术是应用广泛的一种技术。
在快照隔离中,每个事务开始时有其自身的数据库版本或快照。它从这个私有版本中读取数据,因此和其他事务所做的更新隔离开。如果事务更新数据库,更新只出现在其私有版本中,而不是实际的数据库本身中。当事务提交时,和更新有关的信息将保存,使得更新被写入“真正的”数据库。
当一个事务 T T T进入部分提交状态后,只有在没有其他并发事务已经修改该事务想要更新的数据项的情况下,事务进入提交状态。而不能提交的事务则中止。
快照隔离可以保证读数据的尝试永远无需等待(不像封锁那样)。只读事务不会中止,只有修改数据的事务有微小的中止风险。
快照隔离时,如果多个事务更新相同的数据项,会带来数据不一致问题(无法看到彼此的更新)。
原创不易,如果本文对您有帮助,欢迎关注我,谢谢 ~_~