spring的事务
Chapter 1. Spring中的事务控制(Transacion Management with Spring)
Table of Contents
事务管理(Transaction Management)是一个很深的研究方向,而本章最终目的是为了阐述spring的事务管理抽象的理念以及相关内容,所以,你大可放心,我不会为你准备一本类似于“砖头”的书籍, 但为了能够在整个讲解的过程中始终有一个平滑的过渡,有关事务(Transaction)的一些基本概念还是有必要简单介绍一下的。
为了说明“什么是事务”,我觉得先从事务所针对的目的说起,会比较容易切入。
对于一个软件系统来说,需要相应的数据资源(比如,数据库,文件系统等)来保存系统状态,在对系统状态所依托的数据资源进行访问的时候,为了保证系统始终处于一个“正确”的状态[1], 我们就必须对这些访问操作进行一些必要的限定,以此来保证系统状态的完整性。
事务就是以可控的方式对数据资源进行访问的一组操作,为了保证事务执行前后数据资源所承载的系统状态始终处于“正确”状态,事务本身持有四个限定属性, 即原子性(Atomicity),一致性(Consistency),隔离性(Isolation)以及持久性(Durability),也就是常说的事务的ACID属性:
- 事务的原子性(Atomicity)
-
原子性要求事务所包含的全部操作是一个不可分割的整体,这些操作要么全部提交成功,要么只要其中一个操作失败,就全部“成仁”(“一颗老鼠屎搅坏一锅汤”好像形容这种情况比较贴切哦)。 如果把整个事务的操作比做“钢七连”,那我们的口号就得从“不抛弃,不放弃”改成“要么不抛弃,要么就全部放弃”了。
- 事务的一致性(Consistency)
-
一致性要求事务所包含的操作不能违反数据资源的一致性检查,数据资源在事务执行之前处于一个数据的一致性状态,那么,事务执行之后也需要依然保持数 据间的一致性状态。 对于一个证券系统来说,如果顾客银行账户和证券账户资金总和为10万的话(银行账户初始8万,证券账户初始2万),从银行账户的8万转账5万到证券账户的 事务操作结束之后, 银行账户会剩余3万,证券账户为7万,两个账户的总和依然是10万,如果事务操作结束后,整个数据状态不是这个样子,那么就说系统处于不一致状态,而使用 事务其中一个目的就是为了避免这种不一致性状态的产生。
- 事务的隔离性(Isolation)[2]
-
事务的隔离性主要规定了各个事务之间相互影响的程度。隔离性概念主要面向对数据资源的并发访问(Concurrency),并兼顾影响事务的一致性。当两个事务或者更多事务同时访问同一数据资源的时候, 不同的隔离级别决定了各个事务对该数据资源访问的不同行为。
不出意外的话,我们可以为事务指定四种类型的隔离级别,隔离程度按照从弱到强分别为“Read Uncommitted”,“Read Committed”,“Repeatable Read”和“Serializable”:
-
Read Uncommitted. 最低的隔离级别,Read Uncommitted最直接的效果就是一个事务可以读取另一个事务并未提交的更新结果。
Read Uncommitted是以较低的隔离度来寻求较高的性能,其本身无法避免以下几个问题:
-
脏读(Dirty Read). 如果一个事务中对数据进行了更新,但事务还没有提交,另一个事务可以“看到”该事务没有提交的更新结果,这样造成的问题就是,如果第一个事务回滚,那么,第二个事务在此之前所“看到”的数据就是一笔脏数据。
-
不可重复读取(Non-Repeatable Read). 不可重复读取是指同一个事务在整个事务过程中对同一笔数据进行读取,每次读取结果都不同。如果事务1在事务2的更新操作之前读取一次数据,在事务2的更新 操作之后再读取同一笔数据一次,两次结果是不同的,所以,Read Uncommitted也无法避免不可重复读取的问题。
-
幻读(Phantom Read)[3]. 幻读是指同样一笔查询在整个事务过程中多次执行后,查询所得的结果集是不一样的。幻读针对的是多笔记录。在Read Uncommitted隔离级别下, 不管事务2的插入操作是否提交,事务1在插入操作之前和之后执行相同的查询,取得的结果集是不同的,所以,Read Uncommitted同样无法避免幻读的问题。
-
-
Read Committed. Read Committed通常是大部分数据库采用的默认隔离级别,它在Read Uncommitted隔离级别基础上所做的限定更进一步, 在该隔离级别下,一个事务的更新操作结果只有在该事务提交之后,另一个事务才可能读取到同一笔数据更新后的结果。 所以,Read Committed可以避免Read Uncommitted隔离级别下存在的脏读问题, 但,无法避免不可重复读取和幻读的问题。
-
Repeatable Read. Repeatable Read隔离级别可以保证在整个事务的过程中,对同一笔数据的读取结果是相同的,不管其他事务是否同时在对同一笔数据进行更新,也不管其他事务对同一笔数 据的更新提交与否。 Repeatable Read隔离级别避免了脏读和不可重复读取的问题,但无法避免幻读。
-
Serializable. 最为严格的隔离级别,所有的事务操作都必须依次顺序执行,可以避免其他隔离级别遇到的所有问题,是最为安全的隔离级别, 但同时也是性能最差的隔离级别,因为所有的事务在该隔离级别下都需要依次顺序执行,所以,并发度下降,吞吐量上不去,性能自然就下来了。 因为该隔离级别极大的影响系统性能,所以,很少场景会使用它。通常情况下,我们会使用其他隔离级别加上相应的并发锁的机制来控制对数据的访问,这样既保证 了系统性能不会损失太大,也能够一定程度上保证数据的一致性。
不同的隔离级别设置会对系统的并发性以及数据一致性造成不同的影响,总的来说,隔离级别与系统并发性成反比,与数据一致性成正比。 也就是说,事务隔离度越高,系统并发性越差,进而造成系统性能就越差,不过,隔离度的增高,却可以更好地保证数据的一致性。隔离程度与并发性和一致性的关 系如下图:
在具体的实践过程中,我们需要根据系统的具体情况来调整隔离度以保证系统性能与数据一致性之间有一个良好的平衡,但总的来说,保证数据的一致性的考虑应该优于对系统性能的考虑。 -
- 事务的持久性(Durability)
-
事务的持久性是指一旦整个事务操作成功提交完成,对数据所做的变更将被记载并不可逆转,多少有点儿“生米煮成熟饭” 的意思。 即使发生某些系统灾难或者什么天灾人祸之类的事情,之前事务所做的变更也可以找回并恢复。纵使海枯石烂,我(数据库等资源管理系统)对你(事务)的真心永 不变! 通常情况下,数据库等数据资源管理系统会通过冗余存储或者多数据网络备份等方式来保证事务的持久性。
在一个典型的事务处理场景中,有以下几个参与者:
- Resource Manager(RM)
-
ResourceManager简称RM,它负责存储并管理系统数据资源的状态,比如数据库服务器,JMS消息服务器等都是相应的Resource Manager。
- Transaction Processing Monitor(TP Monitor)
-
Transaction Processing Monitor简称TPM或者TP Monitor,它的职责是在分布式事务场景中协调包含多个RM的事务处理。TP Monitor通常对应特定的软件中间件(Middleware), 随着软件开发技术的进步,TP Monitor的实现也由原来基于过程式的设计与实现转向面向对象的更趋模块化的设计和实现。J2EE规范[4]中的应用服务器(Application Server)通常担当的就是TP Monitor的角色。
- Transaction Manager(TM)
-
Transaction Manager简称为TM,它可以认为是TP Monitor中的核心模块,直接负责多RM之间的事务处理的协调工作,并且提供事务界定(Transaction Demarcation)[5], 事务上下文传播(transaction context propagation)[6]等功能接口。
- Application
-
以独立形式存在的或者运行于容器中的应用程序,可以认为是事务边界的触发点。
-
全局事务(Global Transaction). 如果整个事务处理过程中有多个Resource Manager的参与,那么就需要引入TP Monitor来协调多个RM之间的事务处理,TP Monitor将采用“两阶段提交(2 Phase Commit)”协议来保证整个事务的ACID属性, 这种场景下的事务我们就称其为全局事务(Global Transaction)或者分布式事务(Distributed Transaction)。全局事务中各个参与者之间的关系如下图所示:
所有应用程序提交的事务请求需要通过TP Monitor的调配之后,直接由TM统一协调,TM将使用“两阶段提交(two-phase commit)”协议来协调处理多RM之间的事务处理。 针对“两阶段提交”的描述,最经典的比喻就是西方婚礼的过程,婚礼的牧师或者主持是TM,他会首先询问两位新人(两个RM),是否愿意娶对方为妻(嫁给对方), 如果双方的反馈都是“I do”的时候,牧师将宣布二者结为夫妻(即整个事务提交成功)。如果双方任何一方有疑议,那不好意思,婚礼无法继续进行,整个事务提交失败,双方都要回滚(rollback)到之前的单身状态。 -
局部事务(Local Transaction). 如果当前事务只有一个Resource Manager参与其中的话,我们就可以称当前事务为局部事务(Local Transaction)。 比如,你在当前事务中只对一个数据库进行更新,或者只向一个消息队列中发送消息的情况,都属于局部事务。
因为局部事务只包含一个Resource manager,所以,也就没有必要引入相应的TP Monitor来帮助协调管理多个Resource Manager之间的事务, 应用程序可以直接与RM打交道。通常情况下,相应的RM都有内置的事务支持,所以,在局部事务中,我们更倾向于直接使用RM的内置事务支持, 这样不仅可以极大的减少事务处理的复杂度,也避免了引入TP Monitor来协调多个Resource Manager之间事务的性能负担。
Caution
局部事务与全局事务的主要区分在于“事务”中牵扯多少RM,而不是“系统”中实际有多少RM,这是需要我们注意的地方。 即使你系统中存在多个数据库(即RM),只要你当前事务只更新一个数据库的数据,那当前事务就依然应该算作局部事务,而不是全局事务(虽然这种情况下,你也可以启用全局事务)。
实际上,针对单一事务资源的事务管理,你可以在局部事务中直接使用RM内置的事务支持来进行,你也可以引入TP Monitor在分布式事务场景中进行,通常情况下, 各TP Monitor在实现的时候会检测参与事务的RM数目,如果只有单一的RM参与,TP Monitor会做一定的优化,避免采用“两阶段提交”协议的负担, 但即使如此,针对单一事务资源参与的事务,直接采用局部事务中RM内置的事务支持,无论是从复杂度,还是从效率上来看,都要更胜一筹。
到此为止,我们所阐述的都是概念层面的东西,要真正的在系统开发中使用事务,我们需要相应的产品和API支持。 为了让事务“走进现实”,不同的组织或者不同的技术平台会有各自不同的API设计和实现, 但既然Spring(不是Spring .Net)归属于Java一族,其他的平台和组织的解决方案暂且放于一边,我们专门来看Java平台的事务解决方案如何?
对于应用程序的开发人员来说,更多时候,我们是通过相应产品提供的API接口来访问事务资源,考虑如何在应用的业务逻辑中界定事务边界,而对于各提 供商如何在产品中实现事务支持, 则通常不是我们需要关心的问题。所以,以下内容将更多的围绕着各事务处理场景下我们可以通过哪些产品提供的事务处理接口或者标准的事务处理接口来进行事务 控制为主线进行。 当然,期间我们也可能提及相应场景下比较受欢迎的几款事务处理的产品实现。
下面我们将按照从局部事务场景到全局事务场景的顺序来看一下,各场景中Java平台为我们都准备了哪些可用的事务处理API。
在Java的局部事务场景中,系统中事务管理的具体处理方式会随着所使用的数据访问技术的不同而各异,我们不是使用专用的事务API来管理事务, 而是通过当前使用的数据访问技术所提供的基于“connection” [7] 的API来管理事务。
-
数据库资源的局部事务管理. 要在对数据库的访问过程中进行事务管理,每一种数据访问技术都提供了特定于它自身的事务管理API,比如JDBC是Java平台访问关系数据库最基础的 API,如果直接使用JDBC进行数据访问的话,我们可以将数据库连接(java.sql.Connection)的自动提交(AutoCommit)功 能设置为false,改为手动提交来控制整个事务的提交或者回滚:
Connection connection = null; boolean rollback = false; try { connection = dataSource.getConnection(); connection.setAutoCommit(false); // do data access with JDBC connection.commit(); } catch(SQLException e) { e.printStackTrace(); // don‘t do this rollback = true; } finally { if(connection != null) { if(rollback) { try { connection.rollback(); } catch (SQLException e) { e.printStackTrace(); // don‘t do this } } else { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); // don‘t do this } } } }
而如果我们使用Hibernate进行数据访问,我们就得使用Hibernate的Session进行数据访问期间的事务管理[8]:Session session = null; Transaction transaction = null; try { session = sessionFactory.openSession(); transaction = session.beginTransaction(); // do data access with Hibernate session.flush(); transaction.commit(); } catch(HibernateException e) { transaction.rollback(); } finally { session.close(); }
同样的,如果我们使用JDO,TopLink甚至JPA进行数据访问的话,这些数据访问技术也都在他们数据访问API之上提供了相应的事务管理支持。 -
消息服务资源的局部事务管理. 在使用JMS进行消息处理的过程中,我们可以通过JMS的javax.jms.Session来控制整个处理过程的事务:
boolean rollback = false; Connection con = null; Session session = null; try { con = cf.createConnection(); session = con.createSession(true, Session.AUTO_ACKNOWLEDGE); // process Messages with JMS API session.commit(); } catch (JMSException e) { e.printStackTrace();// don‘t do this rollback = true; } finally { if(con != null) { if(rollback) { try { session.rollback(); } catch (JMSException e1) { e1.printStackTrace(); // don‘t do this } } else { try { con.close(); } catch (JMSException e) { e.printStackTrace(); // don‘t do this } } } }
我们在通过javax.jms.Connection的createSession方法创建javax.jms.Session的时候, 将该方法的第一个参数指定为true要求创建一个事务型的javax.jms.Session实例,然后就可以根据情况提交和回滚(rollback)事 务了。
Java平台上的分布式事务管理主要是通过Java Transaction API(JTA)或者Java Connector Architecture(JCA)提供支持的。
JTA是Sun提出的标准化分布式事务访问的Java接口规范。不过,JTA规范定义的只是一套Java接口定义,具体的实现留给了相应的提供商去 实现,各JavaEE应用服务器需要提供对JTA的支持, 另外,除了可以使用绑定到各JavaEE应用服务器的JTA实现之外,Java平台上也存在几个独立的并且比较成熟的JTA实现产品,这包括:
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。