1.4 PostgreSQL中的事务
从事务的形态上来划分,PostgreSQL支持隐式事务和显式事务。隐式事务是一个独立的SQL语句,执行结束之后自动提交。显式事务需要明确指定事务所需要的“标记”,这些“标记”将一组SQL语句组合到一起,形成一个事务块(Transaction Block)。一个事务块的开始使用BEGIN命令,事务块的结束使用COMMIT、END、ABORT、ROLLBACK等。
如果在事务块中发生了错误,由于事务需要满足原子性,那么事务块中后续的SQL语句就无法正常执行,不过同样需要明确指定结束标记才可以彻底结束事务。
事务块是借助有限状态机实现的,它包含很多状态。在事务运行过程中,事务块的状态是不断变化的,直到事务提交或者回滚为止。事务块的状态如表1-5所示。
表1-5 事务块的状态
PostgreSQL将事务处理的过程分成了3个层次。
• 上层:处理显式的事务块命令,例如BEGIN、COMMIT、ROLLBACK等,事务的上层实现包含如下函数。
o BeginTransactionBlock。
o EndTransactionBlock。
o UserAbortTransactionBlock。
o DefineSavepoint。
o RollbackToSavepoint。
o ReleaseSavepoint。
• 中层:无论是事务块命令,还是事务块中间的DML、DDL命令,对于事务来说,每一条都是一个查询,每个查询的执行都会借助中层的事务处理机制来完成。事务的中层实现包含如下函数。
o StartTransationCommand。
o CommitTransationCommand。
o AbortTransationCommand。
• 底层:真正的事务处理机制,负责维护事务的状态、事务资源的分配与回收等。事务的底层实现包含如下函数。
o StartTransaction。
o CommitTransaction。
o AbortTransaction。
o CleanupTransaction
o StartSubTransaction。
o CommitSubTransaction。
o AbortSubTransaction。
o CleanupSubTransaction。
事务块的状态是通过上层函数和中层函数同时控制的,而底层函数则主要控制事务的状态(事务块的状态和事务的状态是不同的)。事务的状态是底层事务真正的状态,如表1-6所示。
表1-6 事务的状态
事务系统是在上层函数和中层函数控制的事务块状态、底层函数控制的事务状态驱动下实现的,我们通过示例来了解一个事务的状态切换过程(示例参考了PostgreSQL文档)。
首先,PostgreSQL通过BEGIN命令开启一个事务块,将事务块的状态从TBLOCK_DEFAULT修改成TBLOCK_BEGIN。这个操作在BeginTransactionBlock函数中实现,而BeginTransactionBlock函数是一个上层函数,事务块的状态转换还要借助中层函数一起实现。BEGIN命令的函数调用关系如图1-2所示,在BeginTransactionBlock函数的两端还包裹着事务的中层函数。
图1-2 BEGIN命令的函数调用关系
BEGIN命令可以触发事务块状态的转换,如图1-3所示。
图1-3 BEGIN命令对应的事务块状态的转换
PostgreSQL事务块中的每个命令都是由中层函数(*Command系列函数)包裹的,从图1-3可以看出,在BEGIN命令执行之后,事务块进入了TBLOCK_INPROGRESS状态。它会一直把这种状态持续到事务结束,例如,针对示例中的“SELECT c1,c2 FROM T1”和“INSERT INTO t1 VALUES(1,1)”这两个SQL语句,它们都是在TBLOCK_INPROGRESS状态下执行的,如图1-4所示。
图1-4 TBLOCK_INPROGRESS状态下的事务执行流程
由于SELECT操作和INSERT操作都处于事务块中,因此它们的执行不会影响事务状态,如图1-5所示(需要注意的是,在执行SELECT命令和INSERT命令的同时,会调用一个Command CounterIncrement函数,这个函数可保证事务内的可见性,在后面的内容中会详细介绍)。
示例中最后执行的是COMMIT操作,它的含义是将事务提交。也就是说,将SELECT命令和INSERT命令所做的改变持久化到当前的数据库中。当然,如果你想要回滚事务,也可以通过ROLLBACK操作来终止事务,这样SELECT操作和INSERT操作所做的修改就会被回滚。
无论是COMMIT操作,还是ROLLBACK操作,它们都由上层函数处理,而且都会被中层的“*Command”函数包裹,如图1-6所示。
图1-5 TBLOCK_INPROGRESS状态和事务执行的关系
图1-6 事务COMMIT或ROLLBACK操作的函数调用关系
事务COMMIT命令的状态转换如图1-7所示。
图1-7 事务COMMIT命令的状态转换
事物ROLLBACK命令的状态转换如图1-8所示。
图1-8 事务ROLLBACK命令的状态转换
事务的执行并非一帆风顺。例如,下面的示例会违反唯一性约束,此时数据库会报错。同时事务块会进入TBLOCK_ABORT状态,该状态代表这个事务块已经发生过错误。由于执行事务块时会包含多个SQL语句,如果事务块中间的某个SQL语句出现了错误,那么其他SQL语句就可以根据TBLOCK_ABORT状态直接报错,这种状态会一直持续到事务块结束标记出现(COMMIT、ROLLBACK、END命令)。
事务块异常终止与显式执行ROLLBACK命令的状态转换如图1-9所示。
是否一旦发生报错,事务块就只能等待显式的事务终止命令呢?实际上也不尽然,虽然PostgreSQL没有显式地实现子事务,但是它可以通过SAVEPOINT来模拟子事务。通过这种方式,即使事务块内发生Error,也可以回滚到Error发生之前。
图1-9 事务块异常终止与显式执行ROLLBACK命令的状态转换
SAVEPOINT的出现又会带来一些新的事务块状态,这些事务块的状态和子事务相关,如表1-7所示。
表1-7 子事务对应的事务块状态
每个SAVEPOINT都是一个单独的子事务块,在PostgreSQL中用“栈”保存每个SAVEPOINT(子事务)的状态,因此,可以想象这些SAVEPOINT之间存在先后(父子)关系。当执行ROLLBACK TO SAVEPOINT恢复到某一个SAVEPOINT时,栈顶到这个目标SAVEPOINT之间的SAVEPOINT就会被丢弃。
SAVEPOINT使用的上层函数和中层函数与主事务相同,而它有自己的底层函数,如下。
• PushTransaction。
• PopTransaction。
• ReleaseSavepoint。
• RollbackToSavepoint。
下面通过示例来描述SAVEPOINT状态转换的过程。
执行“SAVEPOINT p6;”时的状态转换,其中最主要的是底层函数PushTransaction,它负责将p6这个SAVEPOINT入栈,并在中层函数CommitTransactionCommand中将p6对应事务块的状态修改为TBLOCK_SUBINPROGRESS,如图1-10所示。
图1-10 SAVEPOINT使用栈保存状态
从图1-10可以看出,每个SAVEPOINT都是以栈内一个节点的形式存在的,其中底层的是主事务节点,它的事务块状态是TBLOCK_INPROGRESS,其他层节点分别对应一个SAVEPOINT,在执行的过程中都处于TBLOCK_SUBINPROGRESS状态。
当前事务栈的栈顶是p6,在执行“RELEASE SAVEPOINT p5”后,p5被RELEASE,p6同时会被RELEASE。从图1-11中可以看出,p5和p6在ReleaseSavepoint这个底层函数中都被修改了事务块的状态,而且栈顶也指向了p4。
图1-11 RELEASE SAVEPOINT后的栈状态
如果执行“ROLLBACK TO SAVEPOINT p3”,则会产生一个回滚操作,事务栈的栈顶将变为p3,其中p4被Abort,而p3则会被RESTART,如图1-12所示,这里RollbackToSavepoint函数负责将p4的事务块状态修改为TBLOCK_SUBABORT_PENDING,将p3的事务块状态修改为TBLOCK_SUBRESTART。
当前事务栈的栈顶元素是p3,如果这时事务出错,假设执行了“SELECT 4/0”,则事务进入异常状态,会将栈顶元素(即p3)的事务块状态变为TBLOCK_SUBABORT。该状态既不代表SAVEPOINT p3彻底结束,也不代表它可以继续工作,它是否继续工作取决于下一步操作。
图1-12 ROLLBACK TO SAVEPOINT后的栈状态
如果下一步操作是“ROLLBACK TO SAVEPOINT p3”,那么p3的状态会先变为TBLOCK_SUBABORT_RESTART,然后恢复到TBLOCK_SUBINPROGRESS,如图1-13所示。
图1-13 事务出错且SAVEPOINT回滚后的栈状态
如果下一步操作是“ROLLBACK TO SAVEPOINT p2”,那么p3的状态会变为TBLOCK_SUBABORT_END,p3会被清除出事务栈,如图1-14所示。
图1-14 ROLLBACK TO SAVEPOINT后出栈示意图
隐式事务则不需要这样明显的标记,通常将一个单独执行的SQL语句看作独立的事务(PostgreSQL数据库是默认提交的)。在单独的SQL语句执行完毕且没有出现错误的情况下,事务将自动提交。
隐式事务和显式事务的区别就是它的事务块状态中没有TBLOCK_INPROGRESS状态,全程都处于TBLOCK_STARTED状态。
隐式事务通常会涉及中层函数和底层函数。以INSERT语句为例,隐式事务也会调用会中层的StartTransactionCommand函数,它会将事务块状态由TBLOCK_DEFAULT切换为TBLOCK_STARTED,然后正式执行SQL语句(而显式事务会由上层函数继续切换事务块状态),所以,PostgreSQL可以用事务块状态来区分当前SQL是隐式事务还是显式事务。