Spring的事务管理(精通Spring+4.x++企业应用开发实战 第十一章)

来源:互联网 发布:盐和避难所剧情 知乎 编辑:程序博客网 时间:2024/06/05 13:29

数据库事务基础知识

数据库事务是什么

事务要么整体生效,要么整体失效。在数据库上即多条SQL语句要么全执行成功,要么全执行失败

数据库事务必须同时满足4个特性:
原子性(Atomic),一致性(Consistency),隔离性(Isolation)和持久性(Durabiliy)

这里写图片描述
一致性是最终目标,其他特性都是为了达到这个目标而采取的措施。

数据库管理系统一般采用重执行日志来确保原子性,一致性和持久性。
重执行日志记录了数据库变化的每一个动作,数据库在一个事务中执行一部分操作后发生错误退出,数据库即可根据重执行日志撤销已经执行的操作。对于已经提交的事务,即使数据库奔溃,在重启数据库时也能根据日志对尚未持久化的数据进行相应的重执行操作

数据库管理系统采用数据库锁机制保证事务的隔离性。当多个事务视图对相同的数据进行操作时,只有持有锁的事务才能操作数据,直到前一个事务完成后,后面的事务才有机会对数据进行操作。

Oracle数据库使用了数据版本的机制,为回滚段为数据的每个变化都保存一个版本,使数据的更改不影响数据的读取

数据并发的问题

一个数据库可能拥有多个访问客户端,这些客户端都可用并发的方式访问数据库。数据库中的相同数据被多个事务访问,如果没有采取必要的隔离措施,就会导致各种并发问题,破坏数据的完整性。

这些问题可以归为5类,包括3类数据读问题(脏读,不可重复读和幻象读)及2类数据更新问题(第一类丢失更新和第二类丢失更新)

1.脏读(dirty read)
A事务读取B事务尚未提交的更改数据,并在这个数据的基础上进行操作。如果恰巧B事务回滚,那么A事务读到的数据根本是不被承认的
这里写图片描述
因为A事务读取了事务尚未提交的数据,因而造成了丢了500元。

Oracle数据库中,不会发生脏读。

2.不可重复读(unrepeatable read)
不可重复读是指A事务读取了B事务已经提交的更改数据。假设A在取款事务的过程中,B往该账户转账100元,A两个读取账户的余额发生不一致。

这里写图片描述
这里写图片描述
在同一事务中,T4时间点和T7时间点读取的账户存款余额不一致

3.幻象读(phantom read)
A事务读取B事务提交的新增数据,这时A事务将出现幻象读的问题,幻象读一般发生在计算统计数据的事务中

举例,假设银行系统在同一个事务中两次统计存款账户的总金额,在两次统计过程中,刚好新增了一个存款账户,并存入100,这时两次统计的总金额将不一致
这里写图片描述

幻象读是指读到了其他已经提交的新增数据,而不可重复读是指读到了已经提交事务的更改数据(更改或删除)

防止读到更改数据,只需对操作的数据添加行级锁,阻止操作中的数据发生变化
防止读到新增数据,需要添加表级锁——将整张表锁定,防止新增数据(Oracle使用多版本数据的方式实现)

4.第一类丢失更新
A事务撤销时,把已经提交的B事务的更新数据覆盖了。
这里写图片描述
这里写图片描述
A事务在撤销时,将B事务已经转入账户的金额抹去了

5.第二类丢失更新
A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失
这里写图片描述
由于转账事务覆盖了取款事务对存款余额所做的更新,导致银行损失了100.
如果转账事务先提交,用户账户损失100

数据库锁机制

数据库通过锁机制解决并发访问的问题。

按锁定的对象的不同,分为表锁定和行锁定。
表锁定对整张表进行锁定,行锁定对表中的特定行进行锁定。

从并发事务锁定的关系上看,可以分为共享锁定和独占锁定。
共享锁定会防止独占锁定,但允许其他的共享锁定,独占锁定既防止其他的独占锁定,也防止其他的共享锁定。

为了更改数据,数据库必须在更改的行上施加行独占锁定,INSERT,UPDATE,DELELTE和SELECT FOR UPDATE语句都会隐式采用必要的行锁定。

下面是Oracle数据库常用的5种锁定
这里写图片描述
这里写图片描述

事务隔离级别

直接使用锁管理是非常麻烦的,因此数据库为用户提供了自动锁机制。
只要用户指定会话的事务隔离级别,数据库就会分析事务中的SQL语句,然后自动为事务操作的数据资源添加合适的锁 。此外,数据库还会维护这些锁,当一个资源的锁数目太多时,自动进行锁升级以提高系统的运行性能,而这一过程对用户来说是透明的。

ANSI/ISO SQL 92标准定义了4个等级的事务隔离级别
这里写图片描述

事务的隔离级别和数据库并发性是对立的。一般来说,使用READ UNCOMMITED隔离级别的数据库拥有最高的并发性和吞吐量,而使用SERIALIZABLE隔离级别的数据库并发性最低。

JDBC对事务的支持

用户可以通过Connection#getMetaData()方法获取DatabaseMetaData对象,并通过该对象的supportsTransactions(),supportsTransactionIsolationLevel(int level)方法查看底层数据库的事务支持情况。

Connection默认情况下是自动提交的,即每条执行的SQL语句都对应一个事务。为了将多条SQL语句当成一个事务执行,必须先通过Connection#setAutoCommit(false)阻止Connection自动提交,并通过Connection#setTransactionIsolation()设置事务的隔离级别。

Connection中定义了对应SQL 92标准的4个事务隔离级别的常量。通过Connection#commit()提交事务,通过Connection#rollback()回滚事务。
这里写图片描述

JDBC 3.0引入了一个全新的保存点特性,Savepoint接口允许用户将事务分割为多个阶段,用户可以指定回滚到事务的特定保存点,而并非只能回滚到开始事务的点
这里写图片描述

在发生特定问题时,回滚到指定的保存点,而非回滚整个事务
这里写图片描述
用户可以通过DatabaseMetaData#supportsSavepoints()方法查看是否支持保存点功能

ThreadLocal(线程封闭)

ThreadLocal,是保存线程本地化对象的容器。当运行于多线程环境中的某个对象使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。
所以每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。从线程角度看,这个变量就像线程专有的本地变量

InheritableThreadLocal继承于ThreadLocal,它自动为子线程复制一份从父线程那里继承来的本地变量:在创建子线程时,子线程会接收所有可继承的线程本地变量的初始值。
当需要将本地线程变量自动传送给所有创建的子线程时,应使用InheritableThreadLocal

ThreadLocal的接口方法

只有4个方法
这里写图片描述
这里写图片描述

ThreadLocal支持泛型,该类的类名变成了ThreadLocal<T>。API方法分别调整为void set(T value),T get()和T initialValue()

ThreadLocal类中有一个Map,用于存储每个线程变量副本,Map中元素的键为线程对象,值为对应线程的变量副本。这样ThreadLocal就能为每个线程维护一份独立的变量副本

下面是一个简单的版本,帮助理解。

public class SimpleThreadLocal {    private Map valueMap= Collections.synchronizedMap(new HashMap());    public void set(Object newValue){        valueMap.put(Thread.currentThread(),newValue);   //键为线程,值为本线程的变量副本    }    public Object get(){        Thread currentThread=Thread.currentThread();        Object o=valueMap.get(currentThread);            //返回本线程对应的变量        if(o==null&&!valueMap.containsKey(currentThread)){  //如果在Map中不存在,则放到Map中保存            o=initialValue();            valueMap.put(currentThread,o);        }        return o;    }    public void remove(){        valueMap.remove(Thread.currentThread());    }    public Object initialValue(){        return null;    }}

ThreadLocal实例

public class SequenceNumber {    //通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值    public static ThreadLocal<Integer> seqNum=new ThreadLocal<Integer>(){        public Integer initialValue(){            return 0;        }    };    //获取下一个序列值    public int getNextNum(){        seqNum.set(seqNum.get()+1);        return seqNum.get();    }    private static class TestClient extends Thread{        private SequenceNumber sn;        public TestClient(SequenceNumber sn){            this.sn=sn;        }        public void run(){            for(int i=0;i<3;i++){      //每个线程打出3个序列值                System.out.println("thread["+Thread.currentThread().getName()+"] sn["+sn.getNextNum()+"]");            }        }    }    public static void main(String[] args) {        SequenceNumber sn=new SequenceNumber();        //3个线程共享sn,各自产生系列号        TestClient t1=new TestClient(sn);        TestClient t2=new TestClient(sn);        TestClient t3=new TestClient(sn);        t1.start();        t2.start();        t3.start();    }}

通过匿名内部类的方式定义ThreadLocal的子类,提供初始的变量值。
这里写图片描述

3个TestClient共享一个SequenceNumber实例,但并没有互相干扰,而是各自产生独立的序列号,这是因为通过ThreadLocal为每个线程提供了单独的副本。

与Thread同步机制的比较

ThreadLocal和线程同步机制都为了解决多线程中相同变量的访问冲突问题。

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。
而ThreadLocal为每个线程提供了一个独立的变量副本,从而隔离了多个线程对访问数据的冲突。因为每个线程都拥有自己的变量副本,也就没有必要对该变量进行同步。ThreadLocal提供了线程安全的对象封装,在编写多线程代码时,可以把不安全的变量封装到ThreadLocal中。

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式:访问串行化,对象共享化。
ThreadLocal采用了“以空间换时间”的方式:访问并行化,对象独享化。
同步机制仅提供一份变量,让不同的线程排队访问;ThreadLocal为每个线程都提供了一份变量,可以同时访问而互不影响。

Spring使用ThreadLocal解决线程安全问题

有状态会话bean :每个用户有自己特有的一个实例,在用户的生存期内,bean保持了用户的信息,即“有状态”;一旦用户灭亡(调用结束或实例结束),bean的生命期也告结束。即每个用户最初都会得到一个初始的bean。

无状态会话bean :bean一旦实例化就被加进会话池中,各个用户都可以共用。即使用户已经消亡,bean 的生命期也不一定结束,它可能依然存在于会话池中,供其他用户调用。由于没有特定的用户,那么也就不能保持某一用户的状态,所以叫无状态bean。但无状态会话bean 并非没有状态,如果它有自己的属性(变量),那么这些变量就会受到所有调用它的用户的影响,这是在实际应用中必须注意的。

其中bean作用域中的singleton是无状态的作用域,prototype是有状态的作用域…bean的整个生命周期负责:容器在初始化、配置、装饰或者是装配完一个prototype实例后,将它交给客户端,随后就对该prototype实例不闻不问了。不管何种作用域,容器都会调用所有对象的初始化生命周期回调方法。但对prototype而言,任何配置好的析构生命周期回调方法都将不会被调用。清除prototype作用域的对象并释放任何prototype
bean所持有的昂贵资源,都是客户端代码的职责。(让Spring容器释放被prototype作用域bean占用资源的一种可行方式是,通过使用bean的后置处理器,该处理器持有要被清除的bean的引用。)

一般情况下,只有无状态的Bean才可以在多线程环境下共享。绝大部分Bean都可以声明为singleton作用域,而singleton是无状态的作用域。
因为Spring对一些Bean中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也称为线程安全的“状态性对象”,因此,有状态的Bean就能以singleton的方式在多线程中正常工作。

在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程
这里写图片描述
用户可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。

下面实例体现Spring对有状态Bean的改造思路
非线程安全
这里写图片描述
conn是非线程安全的成员变量,因此addTopic()方法也是非线程安全的,必须在使用时创建一个新的TopicDao实例(非singleton)

下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造

@Repositorypublic class TopicDao {    //使用ThreadLocal保存Connection变量    private static ThreadLocal<Connection> connThreadLocal=new ThreadLocal<Connection>();    public static Connection getConnection(){        //如果connThreadLocal没有本线程对应的Connection,则创建一个新的Connection,并将其保存到线程本地变量中        if(connThreadLocal.get()!=null){            Connection conn= ConnectionManager.getConnection();            connThreadLocal.set(conn);            return conn;        }else{            //直接返回线程本地变量            return connThreadLocal.get();        }    }    public void addTopic(){        //从ThreadLocal中获取线程对应的Connection        Statement stat=getConnection().createStatement();    }}

不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否为空,如果为null,说明当前线程中还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前线程已拥有了Connection对象,直接使用。
这就保证了不同的线程使用自己独立的Connection,而不会使用其他线程的Connection。因此,整个TopicDao就可以做成singleton的Bean了。

这个例子将Connection的ThreadLocal直接放在DAO中只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其他DAO共用一个Connection。
要做到同一个事务多DAO共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。

Spring对事务管理的支持

Spring为事务管理提供了一致的编程模板,在高层次建立了统一的事务抽象。不管是选择Spring JDBC,Hibernate,JPA还是MyBaits,Spring都可以让用户用统一的编程模型进行事务管理

Spring事务管理提供了事务模板类TransactionTemplate。通过TransactionTemplate并配合使用事务回调TransactionCallback指定具体的持久化操作,就可以通过编程方式实现事务管理,而无须关注资源获取,复用,释放,事务同步和异常处理等操作。

Spring事务管理的亮点在于声明式事务管理。Spring允许通过声明方式,在IoC配置中指定事务的边界和事务属性,Spring自动在指定的事务边界上应用事务属性。

在单数据源的情况下,Spring直接使用底层的数据源管理事务,在面对多数据源的应用时,Spring才寻求Java EE应用服务器的支持,通过引用应用服务器中的JNDI(Java Naming and Directory Interface,Java命名和目录接口)资源完成JTA事务。

事务管理关键抽象

在Spring事务管理SPI(Service Provider Interface)的抽象层主要包括3个接口,分别是PlatformTransactionManager,TransactionDefinition和TransactionStatus,位于org.springframework.transaction包中。
这里写图片描述
Transaction用于描述事务的隔离级别,超时时间,是否为只读事务和事务传播规则等控制事务具体行为的事务属性,这些事务属性可以通过XML配置或注解描述提供,也可以通过手工编程的方式配置

PlatformTransactionManager根据TransactionDefinition提供的事务属性配置信息创建事务,并用TransactionStatus描述这个激活事务的状态

1.TransactionDefinition
TransactionDefinition定义了Spring兼容的事务属性,这些属性对事务管理控制的若干方面进行配置
这里写图片描述
这里写图片描述
Spring允许通过XML或注解元数据的方式为一个有事务要求的服务类配置事务属性。

2.TransactionStatus
TransactionStatus代表一个事务的具体运行状态。事务管理器可以通过该接口获取事务运行期的状态信息,也可以通过该接口间接地回滚事务,相比于在抛出异常时回滚事务的方式更具可控性。

该接口继承于SavepointManager接口,SavepointManager接口提供了嵌套事务的机制。

SavepointManager接口拥有以下几个方法
这里写图片描述
这3个方法在底层的资源不支持保存点时,将抛出NestedTransactionNotSupportedException异常。

TransactionStatus扩展了SavepointManager接口,提供了以下几个方法
这里写图片描述
这里写图片描述

3.PlatformTransactionManager
事务只能被提交或回滚(或回滚到某个保存点后提交),PlatformTransactionManager很好地描述了事务管理这个概念
这里写图片描述

这3个接口方法是SPI(Service Provider Interface)高层次的接口方法。这些访问没有与JNDI绑定在一起。

这里写图片描述

Spring的事务管理器实现类

Spring将事务管理委托给底层具体的持久化实现框架来完成,Spring为不同的持久化框架提供了PlatformTransactionManager接口的实现类
这里写图片描述
这里写图片描述

要实现事务管理,首先要在Spring中配置好相应的事务管理器,为事务管理器指定数据资源及一些其他事务管理控制属性。

下面来看一下几个常见的事务管理器的配置
1.Spring JDBC 和MyBatis
它们都基于数据源的Connection访问数据库,可以使用DataSourceTransactionManager,只要在Spring中进行以下配置就可以了

<!-- 扫描com.smart下的所有注解 -->    <context:component-scan base-package="com.smart"/>    <!-- 使用properties中的占位符-->    <context:property-placeholder            location="classpath:jdbc.properties"/>    <!-- 配置一个数据源-->    <bean id="dataSource"          class="org.apache.commons.dbcp.BasicDataSource"          destroy-method="close"          p:driverClassName="${jdbc.driverClassName}"          p:url="${jdbc.url}"          p:username="${jdbc.username}"          p:password="${jdbc.password}"/>    <!-- 基于数据源的事务管理器-->    <bean id="transactionManager"          class="org.springframework.jdbc.datasource.DataSourceTransactionManager"          p:datasource-ref="datasource"/>

DataSourceTransactionManager使用DataSource的Connection的commit(),rollback()等方法管理事务

2.JPA(Java Persistence API的简称,Java持久层API)
JPA通过javax.persistence.EntityTransaction管理JPA的事务,EntityTransaction对象可以通过javax.persistence.EntityManager#getTransaction()方法获得,而E你太太也Manager通过一个工厂类方法javax.persistence.EntityManagerFactory#createEntityManager()获取

底层中JPA依然通过JDBC的Connection的事务方法完成最终的控制。要配置一个JPA事务管理器,必须先提供一个DataSource,然后配置一个EntityManagerFactory,最后才配置一个JpaTransactionManager

3.Hibernate
这里写图片描述
大部分ORM框架都拥有自己事务管理的API,它们对DataSource和Connection进行了封装。
Hibernate使用org.hibernate.Session封装Connection,所以需要一个能够创建Session的SessionFactory

4.JTA(Java Transaction API)
如希望在Java EE容器里使用JTA,将通过JNDI和Spring的JtaTransactionManager获取一个容器管理的DataSource。它引用容器提供的全局事务管理,它不需要知道DataSource和其他特定的资源
这里写图片描述
这里写图片描述
使用jee命名空间从Java EE容器中返回JNDI管理的资源
①处的配置相当于
这里写图片描述
显然使用jee命名空间使配置更简洁。
jee命名空间还可以获取EJB本地或远程的无状态Session Bean

事务同步管理器

Spring将JDBC的Connection,Hibernate的Session等访问数据库的连接或会话对象统称为资源,这些资源在同一时刻是不能多线程共享的。
为了让DAO,Service类可以做到singleton,Spring 的事务管理器类TransactionSynchronizationManager使用ThreadLocal为不同事务线程提供了独立的资源副本,同时维护事务配置的属性和运行状态信息。

Spring为不同的持久化技术提供了一套从TransactionSynchronizationManager中获取对应线程绑定资源的工具类
这里写图片描述

这些工具类都提供了静态的方法,通过这些方法可以获取和当前线程绑定的资源,DataSourceUtils.getConnection(DataSource dataSource)方法可以从指定的数据源中获取和当前线程绑定的Connection,而Hibernate的SessionFactoryUtils.getSession(SessionFactory sessionFactory,boolean allowCreate)方法可以从指定的SessionFactory中获取和当前线程绑定的Session

这些工具类还可以将特定异常转换为Spring的DAO异常(上一章提过)

持久化是将程序数据在持久状态和瞬时状态间转换的机制。通俗的讲,就是瞬时数据(比如内存中的数据,是不能永久保存的)持久化为持久数据(比如持久化至数据库中,能够长久保存)。

Spring为不同的持久化技术提供了模板类,模板类在内部通过资源获取工具类间接访问TransactionSynchronizationManager中的线程绑定资源。
所以,如果DAO使用模板类进行持久化操作,这些DAO就可以配置成singleton,不使用模板类也可以直接通过资源获取工具类访问线程相关的资源

TransactionSynchronizationManager
这里写图片描述

TransactionSynchronizationManager将DAO,Service类中影响线程安全的所有“状态”统一抽取到该类中,并用ThreadLocal进行替换。

事务传播行为

Service接口方法可能在内部调用其他Service接口方法以共同完成一个完整的业务操作,因此会产生服务接口嵌套调用的情况,Spring通过事务传播行为控制当前的事务如何传播到被嵌套调用的目标服务接口方法中。

Spring在TransactionDefinition接口中规定了7种类型的事务传播行为,规定了事务方法和事务方法发生嵌套调用时事务如何进行传播

表11-4
这里写图片描述

spring支持编程式事务管理和声明式事务管理两种方式。

编程式的事务管理

实际中很少需要通过编程来进行事务管理,但Spring还是提供了TransactionTemplate以满足一些特殊的需要。

TransactionTemplate是线程安全中,可以在多个业务类中共享TransactionTemplate实例进行事务管理。它有两个主要的方法
这里写图片描述
TransactionCallback接口只有一个方法:Object doInTransaction(TransactionStatus status).

如果操作不会返回接口,则使用Transaction的子接口TransactionCallbackWithoutResult

@Servicepublic class ForumService1 {    public ForumDao forumDao;    public TransactionTemplate template;    @Autowired    public void setTemplate(TransactionTemplate template){        this.template=template;    }    @Autowired  //通过aop主动注入    public void setForumDao(ForumDao forumDao){        this.forumDao=forumDao;    }    public void addForum(final Forum forum){        template.execute(new TransactionCallbackWithoutResult() {            protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {                forumDao.addForum(forum);   //需要在事务环境中执行的代码            }        });    }}

Spring事务管理基于TransactionSynchronizationManager进行工作,如果在回调接口方法中需要显式访问底层数据连接,必须通过资源获取工具类得到线程绑定的线程连接。

使用XML配置声明事务

声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

   显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。     声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。

Spring的声明式事务管理,对代码的侵入性最小,可以让事务管理代码完全从业务代码中移除。

Spring的声明式事务管理是通过Spring AOP实现的,通过事务的声明性信息,Spring负责将事务管理增强逻辑动态织入业务方法的相应连接点中,这些逻辑包括获取线程绑定资源,开始事务,提交/回滚事务,进行异常转换和处理等工作。

Spring和EJB CMT声明事务的不同
这里写图片描述

回滚规则使用户能够指定什么样的异常导致自动回滚,什么样的异常不影响事务提交,这些规则可以在配置文件中通过声明的方式指定。
同时,可以通过调用TransactionStatus#setRollbackOnly()方法编程式地回滚当前事务。

一个将被实施事务增强的服务接口

BbtForum4个方法,希望addTopic()和updateForum()方法拥有写事务的能力,而其他两个只需拥有读事务的能力即可。

@Service@Transactional   //使用注解的声明式事务管理public class BbtForum {    public ForumDao forumDao;    public TopicDao topicDao;    public PostDao postDao;    public void addTopic(Topic topic) throws Exception{        topicDao.addTopic(topic);        postDao.addPost(topic.getPost());    }    @Transactional(readOnly = true)    public Forum getForum(int forumId){        return forumDao.getForum(forumId);    }    public void updateForum(Forum forum){        forumDao.updateForum(forum);    }    public int getForumNum(){        return forumDao.getForumNum();    }    @Autowired    public void setForumDao(ForumDao forumDao) {        this.forumDao = forumDao;    }    @Autowired    public void setPostDao(PostDao postDao) {        this.postDao = postDao;    }    @Autowired    public void setTopicDao(TopicDao topicDao) {        this.topicDao = topicDao;    }}

使用原始的TransactionProxyFactoryBean

TransactionProxyFactoryBean代理类实施声明式事务的方式已经不被推荐。

1.声明式事务配置
通过了解TransactionProxyFactoryBean理解Spring实施声明式事务的内部工作原理

  <!-- ①导入DAO和DataSource的配置文件 -->    <import resource="classpath:applicationContext-dao.xml" />    <!-- ②声明事务管理器-->    <bean id="txManager"          class="org.springframework.jdbc.datasource.DataSourceTransactionManager">        <property name="datasource" ref="datasource" />    </bean>    <!-- ③需要实施事务增强的目标业务Bean-->    <bean id="bbtForumTarget"          class="package com.smart.service.BbtForum"          p:forumDao-ref="forumDao"          p:topicDao-ref="topicDao"          p:postDao-ref="postDao" />    <!--④使用事务代理工厂类为目标业务Bean提供事务增强 -->    <bean id="bbtForum"          class="=org.springframework.transaction.interceptor.TransactionProxyFactoryBean"          p:transactionManager-ref="txManager"          p:target-ref="bbtForumTarget">          <!-- 事务属性配置-->          <property name="transactionAttributes">              <props>                  <prop key="addTopic">                      PROPAGATION_REQUIRED,+PessimisticLockingFailureException                  </prop>                  <!-- 只读事务-->                  <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>                  <!-- 可写事务-->                  <prop key="*">PROPAGATION_REQUIRED,-tion</prop>              </props>          </property>    </bean>

通过TransactionProxyFactoryBean对业务类进行代理,织入事务增强功能。
首先为该代理类指定事务管理器。通过target属性指定需要代理的目标Bean。最后为Bean的不同方法配置事务属性。
Spring允许通过键值配置业务方法的事务属性信息,键可以使用通配符,key=”get*”匹配所有一个get为前缀的方法

如果将TransactionProxyFactoryBean的optimize属性设置为true,即添加<property name=”optimize” value=”true”>,Spring自动通过CGLib动态代理为BbtForumImpl生成基于子类的代理。这时key=”*”不但匹配BbtForum接口的所有方法,也匹配BbtForumImpl中的其他方法,如setForumDao(),setTopicDao()等方法都将织入事务管理的增强

2.异常回滚/提交规则
<prop>内的值为事务属性信息,其配置格式如:
这里写图片描述

传播行为是唯一必须提供的配置项,当<prop>的值为空时也不会发生配置异常。可选的值参考表11-4

隔离级别是可选的,默认为ISOLATION_DEFAULT,表示使用数据库默认的隔离级别。其他可选项
这里写图片描述

如果希望将匹配方法设置为只读事务,可添加readOnly配置项

在事务运行过程中发生异常时,事务可以被声明为回滚会继续提交。默认情况下,当发生运行期异常时回滚;当发生检查型异常时,既不回滚也不提交,控制权交给外层调用。

可以通过配置显式指定回滚规则:指定带+或-的异常类名(或异常名匹配片段)。但抛出-异常时,将触发事务回滚;当抛出+异常时,即使这个异常是检查型异常,事务也会提交。抛出的异常或该异常的祖先类的类名匹配规则中指定了异常类名(或异常名片段),规则就生效,如下

这里写图片描述
只要业务方法运行时抛出的异常或其他父类异常包括”Exception”,事务都将回滚,例如SQLException和ParseException。

Spring默认的事务回滚规则是“运行期异常回滚,检查型异常不回滚”

用户还可以指定多个带+或-的事务回滚/提交机制
这里写图片描述

3.异常提交的示例
假设希望addTopic(Topic topic)在抛出PessimisticLockingFailureException异常时,依旧提交事务,配置如下
这里写图片描述
这里写图片描述
虽然在①和②之间抛出了异常,但Spring最终使①处的数据持久化操作生效,而②处的数据持久化操作将因为前面抛出了异常而无法执行

基于aop/tx命名空间的配置

TransactionProxyFactoryBean没有使用AOP,有明显的缺点。

Spring在基于Schema的配置中添加了一个tx命名空间,在配置文件中以明确结构化的方式定义事务属性,提高了配置事务属性的便利性。

下面通过tx和aop命名空间对上面的事务配置方式进行替换

<?xml version="1.0" encoding="UTF-8" ?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:p="http://www.springframework.org/schema/p"       xmlns:aop="http://www.springframework.org/schema/aop"       xmlns:tx="http://www.springframework.org/schema/tx"       xsi:schemaLocation="http://www.springframework.org/schema/beans       http://www.springframework.org/schema/beans/spring-beans-4.0.xsd      http://www.springframework.org/schema/aop      http://www.springframework.org/schema/aop/spring-aop-4.0.xsd      http://www.springframework.org/schema/tx      http://www.springframework.org/schema/tx/spring-tx-4.0.xsd">    <import resource="classpath:applicationContext-dao.xml"/>    <!-- ①事务管理器-->    <bean id="txManager"          class="org.springframework.jdbc.datasource.DataSourceTransactionManager"          p:dataSource-ref="dataSource"/>    <!-- ②使用强大的切点表达式语言定义目标方法-->    <aop:config>        <!-- ②-1 通过aop定义事务增强切面-->        <aop:pointcut id="serviceMethod" expression="execute(* com.smart.service.*Forum.*(..))"/>        <!-- ②-2 引用事务增强 -->        <aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice"/>    </aop:config>    <!-- ③事务增强-->    <tx:advice id="txAdvice" transaction-manager="txManager">        <!--③-1 事务属性定义 -->        <tx:attributes>            <tx:method name="get*" read-only="false"/>            <tx:method name="add*" rollback-for="Exception" />            <tx:method name="update*"/>        </tx:attributes>    </tx:advice></beans>

首先在配置文件中引入tx和aop命名空间的声明。

在aop命名空间中,通过切点表达式语言,将com.smart.service包下所有以Forum为后缀的类纳入了需要事务增强的范围,完成了事务切面的定义。

在<tx:advice>通过transaction-manager属性引用了①定义的事务管理器(默认查找名为transactionManager的事务管理器,如果命名为它,可以不指定该属性)

本来混杂在一起额事务属性,现在变得结构清晰了
这里写图片描述
这里写图片描述

使用注解配置声明式事务

使用@Transactional注解

因为注解本身就具有一组普适性的默认事务属性,往往只要在需要事务管理的业务类中添加一个@Transactional注解,就完成了业务类事务属性的配置

注解只提供元数据,本身并不能弯沉事务切面织入的功能,所以还需要在Spring配置文件中对标注的@Transactional注解的Bean进行加工处理,以织入事务管理切面

<?xml version="1.0" encoding="UTF-8" ?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:p="http://www.springframework.org/schema/p"       xmlns:tx="http://www.springframework.org/schema/tx"       xmlns:context="http://www.springframework.org/schema/context"       xsi:schemaLocation="http://www.springframework.org/schema/beans      http://www.springframework.org/schema/beans/spring-beans-4.0.xsd      http://www.springframework.org/schema/tx      http://www.springframework.org/schema/tx/spring-tx-4.0.xsd      http://www.springframework.org/schema/context      http://www.springframework.org/schema/context/spring-context.xsd">    <!-- 扫描包下的注解-->    <context:component-scan base-package="com.smart"/>    <import resource="classpath:applicationContext-dao.xml"/>    <bean id="txManager"          class="org.springframework.jdbc.datasource.DataSourceTransactionManager"          p:datasource-ref="datasource"/>    <!-- ①对标注@Transactional注解的Bean进行加工处理,以织入事务管理切面-->    <tx:annotation-driven transaction-manager="txManager"/></beans>

需要引入tx和context命名空间。

默认情况下,tx:annotation-driven会自动使用名为“transactionManager”的事务管理器。

<tx:annotation-driven>另外有两个属性
这里写图片描述

1.关于@Transactional的属性
拥有一组普适性很强的默认事务属性,往往可以使用这些默认的属性
这里写图片描述

Spring支持通过手工设定属性值覆盖默认值
这里写图片描述

2.在何处标注@Transactional注解
@Transactional注解可以被应用与接口定义和接口昂发,类定义和类的public方法上。

Spring建议在业务实现类上使用。当然可以在业务接口上使用@Transactional注解,但因为注解不能被继承,所以在业务接口中标注的@Transactional注解不会被业务实现类继承。
如果通过以下配置启用子类代理,那么业务类不会添加事务增强
这里写图片描述

因此,Spring建议在具体业务类上使用,这样不管proxy-target-class属性配置为什么,业务类都会启用事务机制

3.在方法处使用注解
方法处的注解会覆盖类定义处的注解,如果有些方法需要使用特殊的事务属性,可以在类注解的基础 上提供方法注解
这里写图片描述
这里写图片描述
方法注解提供了readOnly事务属性的设置,将覆盖类级注解中默认的readOnly=false设置

4.使用不同的事务管理器
如果希望在不同地方使用不同的事务管理器,可以
这里写图片描述
事务管理器在XML中分别定义
这里写图片描述

如果到处使用带标识的@Transactional注解,可以自定义一个绑定到特定事务管理器的注解,然后直接使用这个自定义的注解进行标识
这里写图片描述
使用自定义的注解
这里写图片描述

阅读全文
0 0