引入spring事务管理

1.1 为什么用spring事务管理?

在没有spring事务管理器之前,java开发者通常有两种事务管理器可供选择:本地事务和全局事务。本地事务针对的是某一个具体资源(例如JDBC),使用起来也比较方便,但当程序需要处理多个事务资源时,本地事务则无法胜任了。全局事务解决了这个问题,一般都会采用JTA(JAVA transaction API)来实现全局事务。但全局事务的限制也很多,如必须使用JNDI数据源、需要支持XA协议的数据库、依赖于容器、性能低下、只支持单应用多数据源,不支持多应用多数据源等。
针对不同环境、不同事务策略,spring提供了一个事务管理器的抽象层,开发者通过这个抽象事务管理器进行管理事务,之后配置一个具体的事务实现层,就可以实现代码写一次,可以适用于所有的具体事务管理器API。

1.2 理解spring抽象事务管理器

spring抽象事务管理器采用的是策略模式,理解策略模式的关键点就是看它的策略。org.springframework.transaction.PlatformTransactionManager接口定义了相关方法

public interface PlatformTransactionManager {
    TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
    void commit(TransactionStatus status) throws TransactionException;
    void rollback(TransactionStatus status) throws TransactionException;
}

PlatformTransactionManager提供了3个方法,getTransaction根据TransactionDefinition中定义的相关信息创建或者返回一个已经存在的事务。返回的TransactionStatus代表着一个事务。commit和rollback分别表示提交事务和回滚事务。TransactionDefinition细节如下:

public interface TransactionDefinition {
    int getPropagationBehavior();
    int getIsolationLevel();
    int getTimeout();
    boolean isReadOnly();
    String getName();
}

getPropagationBehavior获取事务的传播特性,可选值如下:

传播特性 解释
PROPAGATION_REQUIRED 0 支持当前事务环境,如果没有则创建一个
PROPAGATION_SUPPORTS 1 支持当前事务,没有不创建事务
PROPAGATION_MANDATORY 2 支持当前事务,没有则报错
PROPAGATION_REQUIRES_NEW 3 挂起当前事务,新建一个事务
PROPAGATION_NOT_SUPPORTED 4 如果存在事务则挂起
PROPAGATION_NEVER 5 如果存在事务则报错
PROPAGATION_NESTED 6 开启一个嵌套事务,很多数据库都不支持

事务的七种传播特性中,需要注意的一个点:用了事务管理器之后连接将由事务管理器管理了,例如用了PROPAGATION_NOT_SUPPORTED之后,第一次操作会获取一个新的connection,但之后的操作都是基于已获取的connection,如果用的是AbstractRoutingDataSource,在NOT_SUPPORTED传播特性的方法内切换数据源是无效的,因为选择数据源是在getConnection的时候确定的。

-- 切换数据源后,只有获取连接才会调用determineTargetDataSource使切换后的数据源生效
public Connection getConnection(String username, String password) throws SQLException {
  return determineTargetDataSource().getConnection(username, password);
}

getIsolationLevel获取事务的隔离级别,可选值如下:

隔离级别 解释
ISOLATION_DEFAULT -1 使用数据源提供的隔离级别
ISOLATION_READ_UNCOMMITTED 1 允许一个事务读取另一个事务还没提交的数据
ISOLATION_READ_COMMITTED 2 禁止脏读
ISOLATION_REPEATABLE_READ 4 禁止不可重读
ISOLATION_SERIALIZABLE 8 禁止幻读

一般数据库的默认级别都会设置成READ_COMMITTED,即禁止脏读。不可重读的意思是当一个事务内两次查询同一条数据,在第二次查询数据之前有个事务更改了这条数据并提交了事务,则第二次查询的数据是更改之后的数据,与第一次查询的不一致,即为不可重复读。设置成REPEATABLE_READ之后,更新那条数据的事务会等待查询那条数据的事务提交后才可以提交。幻读的意思是当 一个事务两次查询一个范围的数据, 在第二次查询之前有个事务插入了一条符合查询条件的事务,则会造成两次查询结果不一致。设置成禁止幻读之后,插入符合第一个查询条件的事务必须等查询事务提交之后,才可以被提交。可以看到,禁止不可重读和禁止幻读会对性能带来极大影响,一般数据库默认隔离级别都设置成禁止脏读。
getTimeout方法获取事务超时值,超过这个时间之后将会被回滚。isReadOnly标明这个事务是不是只读事务,如果有写操作会抛出异常。getName返回事务名字,便于在开发事务管理后台的时候区分事务,默认会设置成类全路径.方法名。
TransactionStatus像其它事务接口一样,提供了一些简单方式来控制事务执行和查询事务状态,详细如下:

public interface TransactionStatus extends SavepointManager, Flushable {
    boolean isNewTransaction();
    boolean hasSavepoint();
    void setRollbackOnly();
    boolean isRollbackOnly();
    void flush();
    boolean isCompleted();
}
1.3 声明spring事务管理器

无论使用声明式事务还是编程式事务,都需要指定具体的事务管理器实现类,一般我们采用依赖注入来指定具体的事务管理器。通常根据我们项目工作的环境来选择具体的事务管理器。
使用JDBC数据源的步骤:

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="${jdbc.driverClassName}" />
    <property name="url" value="${jdbc.url}" />
    <property name="username" value="${jdbc.username}" />
    <property name="password" value="${jdbc.password}" />
</bean>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

使用JTA数据源的步骤:

<?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:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/jee
        http://www.springframework.org/schema/jee/spring-jee.xsd">

    <jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>
    <bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />
</beans>

使用hibernate数据源

<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="mappingResources">
        <list>
            <value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
        </list>
    </property>
    <property name="hibernateProperties">
        <value>
            hibernate.dialect=${hibernate.dialect}
        </value>
    </property>
</bean>

<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory"/>
</bean>
1.4 使用spring事务管理器

声明式事务需要与AOP相结合才可以使用,AOP会给要进行事务管理的方法生成代理,在TransactionInterceptor的invoke方法中,选择具体的PlatformTransactionManager 。可以在xml中配置事务管理器要代理的类:

<?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: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.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    <aop:config>
        <aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.*.*(..))"/>
        <aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
    </aop:config>
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <tx:attributes>
            <!-- all methods starting with 'get' are read-only -->
            <tx:method name="get*" read-only="true"/>
            <!-- other methods use the default transaction settings -->
            <tx:method name="*"/>
        </tx:attributes>
    </tx:advice>
</beans>

XML配置的一般是业务方法的默认事务行为。通常对于特殊方法,会采用@Transactional注解来配置事务的属性,需要使用tx:annotation-driven来启用事务注解支持。

<?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: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.xsd
        http://www.springframework.org/schema/tx
        http://www.springframework.org/schema/tx/spring-tx.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">
    <!-- enable the configuration of transactional behavior based on annotations -->
    <tx:annotation-driven transaction-manager="txManager"/><!-- a PlatformTransactionManager is still required --> 

    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
</beans>
属性名 默认值 解析
transaction-manager transactionManager 事务管理器的名字
mode proxy 代理模式,还可以是aspectj
proxy-target-class false 仅在mode="proxy"时生效,即默认使用jdk接口代理,true的话则使用类代理
order Ordered.LOWEST_PRECEDENCE 事务委托器的优先级

默认的代理模式是运行时织入,所以如果你用注解的方法不是public的,或者你不是通过代理对象而是直接方法内部调用,及时你用了@Transaction注解,也是不会生效的。Transaction注解有个比较重要的属性value,当有多个事务管理器时,可以通过设置这个属性选择具体的事务管理器

public class TransactionalService {
    @Transactional("order")
    public void setSomething(String name) { ... }

    @Transactional("account")
    public void doSomething() { ... }
}

<tx:annotation-driven/>
<bean id="transactionManager1" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        ...
    <qualifier value="order"/>
</bean>

<bean id="transactionManager2" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        ...
    <qualifier value="account"/>
</bean>
1.5 手动获取connection连接

如果某些特殊场景下,你需要用connection直接执行些sql,可以采取下面的方法。如果当前是在一个事务环境,则会返回对应的connection,否则会为你创建一个connection。针对JPA和Hibernate使用的是EntityManagerFactoryUtils和SessionFactoryUtils两个类。

Connection conn = DataSourceUtils.getConnection(dataSource);
1.6 编码方式使用事务管理器

如果你的应用只有几个方法需要用到事务,为了避免使用代理带来了性能损耗,可以采用编码方式来管理事务

DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);
try {
    // execute your business logic here
}
catch (MyException ex) {
    txManager.rollback(status);
    throw ex;
}
txManager.commit(status);
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343