本章是第四章的延续,作者向读者展示了如何使用Spring事务管理来保证数据一致性。Spring对事务的管理有丰富的支持,程序控制的和声明式的都有。在本章中,我们会学习到如何把应用程序的代码放置在事务中,以确保在一切顺利时,所有的成果都被固定下来;一旦其中有一步出错,那么整个事情就像没有发生一样。
一、理解事务
首先我们应该弄清楚什么是事务,这样才能认识到事务的重要性。举一个小小的例子,大概每个人都有转账的经历。当我们从A帐户向B帐户转100元后,银行的系统会从A帐户上扣除100而在B帐户上加100,这是一般的正常现象。但是一旦系统出错了怎么办呢,这里我们假设可能会发生两种情况:(1)A帐户上少了100元,但是B帐户却没有多100元。(2)B帐户多了100元钱,但是A帐户上却没有被扣钱。这种错误一旦发生就等于出了大事,那么再假如一下,你要转账的是1亿呢?所以上面的两种情况分别是你和银行不愿意看到的,因为谁都不希望出错。那么有没有什么方法保证一旦A帐户上没有被扣钱而B帐户上也没有被加钱;或者A帐户扣了100元而B帐户准确无误的加上100元呢。也就是说要么转账顺利的成功进行,要么不转账呢?可以,这就是数据库事务机制所要起到的作用和做的事情。
下面给出事务的概念(在数据库中事务的概念在表述上略有差异,但是含义都是一样的):
所谓事务是用户定义的一个数据库操作序列。这些操作要么都做,要么都不做,是一个不可分割的工作单位。事务具有四个特性:
1. 原子性:一个事务中所有对数据库的操作是一个不可分割的操作序列。这些操作要么完整的被全部执行,要么一步也不做。是一个逻辑工作单位。
2. 一致性:一个事务独立执行的结果将保持一致性,即数据不会因为事务的执行而遭受破坏。
3. 隔离性:一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对其他并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
4. 持久性:一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其执行结果有任何影响。
结合前面的例子解释一下概念:在转账示例中A扣100元钱和B加100元钱这两步更新操作就是一个操作序列。这两步是一个整体要么全部成功,要么全部成功,绝对不能只成功一步而另一步失败。关于数据库事务的详细知识请参考有关的数据库原理书籍。
Spring框架引人注目的重要因素之一是它全面的事务支持。Spring支持“编程式事务”和“声明式事务”
二、在Spring中编写事务
在我们的代码中添加事务的一种方法就是使用Spring的TransactionTemplate类,用程序添加事务边界。就像其他Spring的模版类一样,TransactionTemplate利用一套回调机制,把我们的代码包装在一个TransactionTemplate里。看下面的代码片断:
publicvoid enrollStudentInCourse()
{
tran.execute(new TransactionCallback()
{
public Object doInTransaction(TransactionStatus ts){
try{
//do something....
}catch(Exception ex){
ts.setRollbackOnly();//出错进行事务回滚
}
returnnull;
}
}
);
}
下面是配置文件:
<bean id = "transactionTemplate" class = "org.springframework.transaction.support.TransactionTemplate">
<property name="transactionManager"><ref bean = "transactionTemplate"/></property>
</bean>
<bean id = "courseService" class = "MyImp">
<property name="transactionTemplate"><ref bean = "transactionTemplate"/></property>
</bean>
transactionTemplate对象有一个transactionManager属性。该属性为PlatformTransactionManager类型,它用来处理特定平台的事务管理的细节。也可以直接使用 org.springframework.transaction.PlatformTransactionManager
的实现来管理事务。只需通过bean引用简单地传入一个 PlatformTransactionManager
实现,然后使用 TransactionDefinition
和 TransactionStatus
对象,你就可以启动一个事务,提交或回滚。在上例的简单代码中我们展示的是“编程式事务”当你完全希望控制事务的边界时候,“编程式事务”是不错的。但是他有些侵入性。通常我们的事务需求并没有要求在事务的边界上进行如此精确的控制。我们可以使用“声明式事务”。
三、声明式事务
大多数Spring用户选择声明式事务管理。这是对应用代码影响最小的选择,因此也最符合非侵入式轻量级容器的理念。Spring的声明式事务管理是通过Spring AOP实现的,因为事务方面的代码与Spring绑定并以一种样板式风格使用,这样做是非常自然的,因为事务时系统级的,凌驾于应用的主要功能之上的,你可以把Spring的事务想象成是一个切面(方面)包裹着一个方法。不过尽管如此,你一般并不需要理解AOP概念就可以有效地使用Spirng的声明式事务管理。
在Spring2.0之前要在Spring中使用声明式事务,得用TransactionProxyFactoryBean。这是一个接口,他有点类似在第三章中的ProxyFactoryBean,只不过它的目的是将方法包装在事务的上下文中。Spring2.0及以后的版本中声明式事务的配置与之前的版本有相当大的不同。主要差异在于不再需要配置TransactionProxyFactoryBean
了。Spring2.0之前的旧版本风格的配置仍然是有效的;你可以简单地认为新的<tx:tags/>
替你定义了TransactionProxyFactoryBean
。和TransactionProxyFactoryBean配合的还有两个,一个是PlatformTransactionManager另一个是TransactionAttributeSource。
1.理解事务属性:
在Spring里,事务属性是对事务策略如何应用到方法的描述。这个描述包括下列一些参数:传播行为、隔离级别、只读提示、事务超时间隔。
(1)传播行为
传播行为定义了关于客户端和被调用方法的事务边界。Spring定义了7种截然不同的传播行为。(在书中165页有详细列表)Spring的传播行为分别对应了EJB容器管理的事务(CMT)中所有的传播规则。传播规则回答了一个问题,就是新的事务是否要被启动或是被挂起,或者方法是否要在事务环境中运行。
(2)隔离级别
在一个典型的应用中,多个事务并发运行,经常会操作同一个数据来完成任务。并发会导致以下问题:
l 脏读——脏读发生在一个事务读取了被另一个事务改写但还未提交的数据时。
l 不可重复读——不可重复读发生在一个事务执行相同的查询两次或两次以上,但每一次查询结果不同时。
l 幻读——幻读和不可重复读相似。
在理想状态下,事务要完全相互隔离,以防止这些问题发生。(这些内容在数据库原理里面都有讲解,大家可以参考数据库原理的相关书)然而完全隔离会影响性能,因为隔离经常牵扯到锁定在数据库中的记录。侵占性的锁定会阻碍并发,要求事务相互等待来完成工作,这样会引起“事务死锁”。
应该认识到完全隔离会影响性能,并且不是所有应用都需要完全隔离,有时候应用需要在事务隔离上有些弹性。因此也有好几个隔离级别,见书166页下面的表格。
(3)只读
如果一个事务只对后段数据库执行读操作,数据库就可能利用事务只读的特性,使用某些优化措施。通过声明一个事务为只读,你就给了后端数据库一个机会,来应用那些它认为合适的优化措施。
(4)事务超时
在事务中你可能选择设置的另一个属性就是超时。假设你的事务在预料之外长时间运行,因为事务可能涉及对后端数据库的锁定,长时间运行的事务会不必要的占用数据库资源。与其等他结束,不如声明一个事务,在特定秒数后自动回滚。
四、通过方法名声明事务
NameMatchTransactionAttributeSource,这个事务属性源让你在方法名的基础上声明事务属性。看下面的例子:
<bean id = "transactionAttuibuteSource" class = "org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource">
<property name="properties">
<props>
<prop key="enrollStudentInCourse">
PROPAGATION_REQUIRES_NEW
</prop>
</props>
</property>
</bean>
被命名为transactionAttuibuteSource的bean 将被置入到TransactionProxyFactoryBean的transactionAttuibuteSource属性中。
NameMatchTransactionAttributeSource的properties属性把方法名映射到事务属性描述器上。事务属性描述器有以下几种形式:
l PROPAGATION——传播行为
l ISOLATION——隔离级别(可选)
l Readonly——是只读事务吗?(可选)
l -Exeptions——回滚规则(可选)
l +Exception——同上
在上面的例子中,只指定了传播行为。但是,事务的许多其他参数都可以在事务属性描述器中定义。
指定事务隔离级别
<bean id = "transactionAttuibuteSource" class = "org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource">
<property name="properties">
<props>
<prop key="enrollStudentInCourse">
PROPAGATION_REQUIRES_NEW, ISOLATION_REPEATABLE_READ
</prop>
</props>
</property>
</bean>
使用只读事务
<bean id = "transactionAttuibuteSource" class = "org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource">
<property name="properties">
<props>
<prop key="enrollStudentInCourse">
PROPAGATION_REQUIRES_NEW, ISOLATION_REPEATABLE_READ, readOnly
</prop>
</props>
</property>
</bean>
指定回滚规则
当事务运行过程中抛出异常时,事务可以被声明为回滚或者不回滚。默认情况下,发生运行时间异常时,事务将被回滚;发生受查异常(checked exception)时,不回滚。
<bean id = "transactionAttuibuteSource" class = "org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource">
<property name="properties">
<props>
<prop key="enrollStudentInCourse">
PROPAGATION_REQUIRES_NEW, ISOLATION_REPEATABLE_READ, -CourseException
</prop>
</props>
</property>
</bean>
CourseException用一个负号(-)标记。异常可以用负号(-)或者正号(+)标记,当负号异常抛出时,将触发回滚;相反的,正号异常表示事务仍可提交,即使这个异常抛出。你甚至可以把运行时间异常标记为正号,以防止回滚。
使用通配符匹配
<bean id = "transactionAttuibuteSource" class = "org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource">
<property name="properties">
<props>
<prop key="get*">
PROPAGATION_SUPPORTS
</prop>
</props>
</property>
</bean>
五、用元数据声明事务
就元数据而言,他们不直接改变你的代码行为。反之,他们给应用的支撑平台提供提示和建议,指导平台如何才能把附加行为运用到应用中。用元数据来表现事务属性是最自然不过的了。正如你所看到的,事物属性不直接改变方法的执行,但当一个方法被TransactionProxyFactoryBean代理时,它将被包裹在一个事务中。
1.用元数据来书写事务属性
为了让TransactionProxyFactoryBean能够从元数据检索事务属性,需要将他的transactionAttributeSource设置成AttributesTransactionAttributeSource,如下所示:
<bean id = "transactionAttributeSource" class = "org.springframework.transaction.interceptor.AttributesTransactionAttributeSource">
<constructor-arg>
<ref bean = "attributesImpl"/>
</constructor-arg>
</bean>
在这个事务属性源的构造参数中置入了一个名为attributesImpl的对象的引用。这个attributesImpl对象将被事务属性源使用,和底层的元数据实现配合。
2.用Commons Attributes声明事务
Jakarta Commons Attributes是最早在Java中实现元数据的方法之一。Commons Attributes的好处就在于不需要跳到Java 5才能使用元数据。这样如果你仍然部署在一个比较老的Java版本,并且想用元数据声明事务的话,唯一的选择就是使用Commons Attributes。然而,现在Java 6已经快出来了。Java 5已经可以说得到普遍应用了,尤其是在我们公司以及我个人都已经迁移到Java 5平台上了。所以,运用Commons Attributes声明事务这里就不多摘录了。
六、修剪事务声明
Spring提供两种方法抗击繁复的XML:
l Bena继承
l AOP自动代理
1.从父TransactionProxyFactoryBean继承
简化事务和服务对象声明的一种方法是使用Spring对父bean的支持。使用<bean>标签的parent属性,就能够指定一个bean成为其他bean的孩子,继承父bean的属性。
使用bean继承来缩减包含多重TransactionProxyFactoryBean声明的XML,开始于在上下文定义中添加一个TransactionProxyFactoryBean的abstract声明:
<bean id = "abstractTxDefinition" class = "org.springframework.transaction.interceptor.TransactionProxyFactoryBean" lazy-init="true">
<property name="transactionManager">
<ref bean="transactionManager"/>
</property>
<property name="transactionAttributeSource">
<ref bean="attributeSource"/>
</property>
</bean>
这个声明类似于前面的courseService声明,除了这两件事情:
l 首先,它的lazy-init属性是设置为true的。应用上下文通常在启动的时候实例化所有singleton bean。既然我们的应用仅仅使用abstractTxDefinition的子bean,而不会直接使用abstractTxDefinition,那么我们就不希望容器去初始化一个我们从来不用的bean。Lazy-init属性告诉容器不要创建bean,除非我们请求。实际上lazy-init就是把这个bean变成抽象的。
l Traget属性奇怪的消失了。
下面创建子bean:
<bean id = "courseService" parent = "abstractTxDefinition">
<property name="target">
<bean class = "myImpl">
</property>
</bean>
Parent属性表示这个bean将从abstractTxDefinition bean中继承他的定义。这个bean所要添加的一件事情就是绑定一个target属性的值。
到目前为止,这个技术并没有让我们节省更多的XML,但是考虑到你将要做的事情是让另一个bean事务化,你只需要加另一个abstractTxDefinition的子bean就可以了。
<bean id = "studentService" parent = "abstractTxDefinition">
<property name="target">
<bean class = "myImpl">
</property>
</bean>
但是,请注意你并不用再完整的定义另外一个TransactionProxyFactoryBean。现在想象一下,假如你的应用有几十个服务bean需要被事务化。当我们有大量需要被事务化的bean时,bean的继承就真正得到回报了。
2.自动代理事务
在Spring中事务是基于AOP的,同样可以用自动代理来除掉TransactionProxyFactoryBean的重复实例。
首先,就像要做任何自动通知(auto-advising)一样,你需要声明一个bean,成为DefaultAopProxyFactory的实例:
<bean id = "autoproxy" class = "org.springframework.aop.framework.DefaultAopProxyFactory">
....
</bean>
DefaultAopProxyFactory将在应用上下文中遍历advisor,自动用它来代理匹配advisor的pointcut的所有bean。对于事务,应该使用的advisor是TransactionAttributeSourceAdvisor:
<bean id="transactionAdvisor" class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor">
<constructor-arg>
<ref bean = "transactionInterceptor"/>
</constructor-arg>
</bean>
TransactionAttributeSourceAdvisor是一个羽翼丰满的AOP advisor,和任何advisor一样,它由一个pointcut和一个拦截器组成。这个pointcut是一个static的pointcut。他根据事务属性源来确定一个方法是否和一些事务属性关联。如果一个方法有事务属性,那么这个方法将被代理并包含在一个事务中。
关于拦截器,它通过一个构造参数置入到TransactionAttributeSourceAdvisor。他由TransactionInterceptor类实现并像下面那样被置入到应用中:
<bean id = "transactionInterceptor" class = "org.springframework.transaction.interceptor.TransactionInterceptor">
<property name="transactionManager">
<ref bean = "transactionManager"/>
</property>
<property name="transactionAttributeSource">
<ref bean = "transactionAttributeSource"/>
</property>
</bean>
TransactionInterceptor有两个用来完成工作的合作者。他使用PlatformTransactionMananager,并将他置入到transactionManager属性,是事物和他背后的事务实现相配合。同时,他使用被置入到transactionAttributeSource属性的事务实现源来确定它将拦截的应用刀这个方法的事务策略。事实上,当在使用TransactionProxyFactoryBean时早已定义好了transactionManager和transactionAttributeSource bean——它们也将很好地为事务拦截器工作。
最后一件要做的事情就是删除所有TransactionProxyFactoryBean的实例,并把服务bean重命名回它们原来正确的名称。
为自动代理选择一个属性源
当使用自动代理事务时,事务属性源是一个方法是否需要代理的关键。这个事务可能会提示你选择不同的事务属性源。
当使用自动代理时,对于事务属性源的一个更好的选择是MethodMapTransactionAttributeSorrce。这个事务属性源类似于NameMatchTransactionAttributeSource,但他让你指定需要事务化的完整类名称和方法名。
当用自动代理时,事务属性源的一个更好的选择是TransactionAttributeSourceAdvisor。回忆一下,AttributeTransactionAttributeSourceAdvisor把事务属性从元数据中取出来,而这些元数据是直接放在要事务化的方法的代码中的。这就意味着,如果你使用AttributeTransactionAttributeSourceAdvisor作为属性源,并同时使用自动代理的话,使方法事务化,只不过是添加合适的元数据到方法里而已。
七、小结
事务是企业应用开发的重要组成部分,他使软件更加可靠。它们确保一种要么全有要么全无的行为,防止数据不一致而导致的不可预测的错误发生。它们同时也支持并发,防止并发应用线程在操作同一数据时互相影响。
关于Spring事务的详细内容,在Spring2.0文档中做了详细的讲解。并且在Spring2.0中有了许多改动。
posted on 2007-10-23 21:33
谭明 阅读(1745)
评论(0) 编辑 收藏 所属分类:
Spring