Spring程序员我爱编程

Spring的事务管理

2018-05-21  本文已影响348人  小螺钉12138

1.数据库事务基础知识

1.1.何为数据库事务

数据库事务的4个特性

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

和Java程序采用对象锁机制进行线程同步类似,数据库管理系统采用数据库锁机制保证事务的隔离性。当多个事务试图对相同的数据进行操作时,只有持有锁的事务才能操作数据,直到前一个事务完成后,后面的事务才有机会对数据进行操作。Oracle数据库还使用了数据版本的机制,在回滚段位数据的每个变化都保存一个版本,使数据的更改不影响数据的读取。

1.2.数据的并发问题

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

两类丢失事件

1.3.数据库锁机制

数据库通过锁机制解决并发访问的问题,虽然不同的数据库在实现细节上存在差别,但原理基本上是一样的。按锁定的对象不一样,一般可以分为表锁定和行锁定。前者对整张表锁定,后者对表中特定行进行锁定。从并发事务锁定的关系上看,可以分为共享锁和独占锁。共享锁定会防止独占锁定,但允许其他的共享锁定。而独占锁定既防止其他的独占锁定,也防止其他的共享锁定。为了更改数据,数据库必须在进行更改的行上施加独占锁定,select、update、delete和select for update 语句都会隐式采用必要的行锁定。oracle数据库常用的5种锁定

1.4.事务隔离级别

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

隔离级别 脏读 不可重复读 幻象读 第一类丢失更新 第二类丢失更新
READ UNCOMMITED 允许 允许 允许 不允许 允许
READ COMMITED 不允许 允许 允许 不允许 允许
REPEATABLE READ 不允许 不允许 允许 不允许 不允许
SERIALIZABLE 不允许 不允许 不允许 不允许 不允许
1.5.JDBC对事务的支持

并不是所有的数据库都支持事务,即使支持事务的数据库也并非支持所有的事务隔离级别。用户可以根据Connection.getMetaData()方法获取DatabaseMetaData对象,并通过该对象的supportsTransactions()、supportsTransactionIsolationLevel(int level)
方法查看底层数据库的事务支持情况

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

2.ThreadLocal基础知识

2.1.ThreadLocal是什么

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

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

2.2.ThreadLocal的接口方法
2.3.一个ThreadLocal实例
public class SequenceNumber {
    //通过匿名内部类覆盖ThreadLocal的initialValue()方法,指定初始值
    private static ThreadLocal<Integer> seqNum=new ThreadLocal<Integer>(){
        public Integer initialValue(){
            return 0;
        }
    };

    //获取下一个序列值
    public int getNextNum(){
        seqNum.set(seqNum.get()+1);
        return seqNum.get();
    }

    public static void main(String[] args) {
        SequenceNumber sn =new SequenceNumber();
        TestClient t1=new TestClient(sn);
        TestClient t2=new TestClient(sn);
        TestClient t3=new TestClient(sn);
        t1.start();
        t2.start();
        t3.start();
    }

    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++){//每个线程输出三个序列值
                System.out.println("thread["+Thread.currentThread().getName()+"] sn["+sn.getNextNum()+"]");
            }
        }
    }
}

//输出内容
thread[Thread-0] sn[1]
thread[Thread-0] sn[2]
thread[Thread-0] sn[3]
thread[Thread-1] sn[1]
thread[Thread-1] sn[2]
thread[Thread-1] sn[3]
thread[Thread-2] sn[1]
thread[Thread-2] sn[2]
thread[Thread-2] sn[3]

考查输出结果信息,发现每个线程所产生的序号虽然都是共享同一个SequenceNumber实例,但它们并没有相互干扰,而是各自产生独立的序列号,这是因为通过ThreadLocal为每个线程提供了单独的副本

2.4.与Thread同步机制的比较

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序缜密地分析什么时候对变量进行读/写、什么时候需要锁定某个对象、什么时候释放对象锁等繁杂的问题,程序设计和编写难度较大

而ThreadLocal从另一个角度来解决多线程的并发访问。ThreadLocal为每个线程提供了一个独立的变量副本,从而隔离了多个线程对访问数据的冲突。因为每个线程都拥有自己的变量副本,因而也就没有必要对该变量进行同步,ThreadLocal提供了线程安全的对象封装,在编写多线程时,可以把不安全的变量封装进ThreadLocal

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

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

web应用一般分为控制层,业务层和持久层,在不同测层次编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求返回响应所经过的所有程序调用都属于同一个线程,这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定。

3.Spring对事务管理的支持

Spring为事务管理提供了一致的编程模板,在高层次建立了统一的事务抽象,像Spring DAO为不同的持久化类提供了模板类一样,Spring也提供了事务模板类TransactionTemplate。通过TransactionTemplate并配合使用事务回调TransactionCallback指定具体的持久化操作,就可以通过编程方式实现事务管理,而无须关注资源获取、复用、释放、事务同步和异常处理等操作。

3.1.事务管理关键抽象

在事务管理SPI的抽象层主要包括3个接口,分别是PlatformTransactionManager、TransactionDefinition和TransactionStatus

3.2.Spring的事务管理器实现类

Spring将事务管理委托给底层具体的持久化实现框架来完成。因此,Spring为不同的持久化框架提供了PlatformTransactionManager接口的实现类。

<!--配置一个dataSource的数据源-->
<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="txManager"
          class="org.springframework.jdbc.datasource.DataSourceTransactionManager"
          p:dataSource-ref="dataSource"/><!--引用数据源-->
<bean id="entityManagerFactory"
          class="org.springframework.orm.jpa.datasource.LocalContainerEntityManagerFactoryBean"
          p:dataSource-ref="dataSource"/><!--指定一个数据源-->
          
<bean id="transactionManager"
          class="org.springframework.orm.jpa.JpaTransactionManager"
          p:entityManagerFactory-ref="entityManagerFactory"/><!--指定实体管理器-->

<bean id="sessionFactory"
          class="org.springframework.orm.hibernate4.LocalSessionFactoryBean"
          p:dataSource-ref="dataSource"<!--指定一个数据源-->
          p:mappingResource="classpath:bbtFoum.hbm.xml"><!--指定Hibernate的配置文件-->
        <property name="annotatedClasses"><!--Hibernate其它属性-->
            <list>
                <value>com.smart.User</value>
            </list>
        </property>
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
                <prop key="hibernate.show_sql">true</prop>
            </props>
        </property>
    </bean>
    
<bean id="transactionManager"
          class="org.springframework.orm.hibernate4.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"/><!--注入会话工厂-->
<jee:jndi-lookup id="accountDs" jndi-name="java:comp/env/jdbc/account"/>
<jee:jndi-lookup id="orderDs" jndi-name="java:comp/env/jdbc/order"/>
<!--通过jee命名空间获取Java EE应用服务器容器中的数据源-->

<!--指定JTA事务管理器-->
<bean id="transactionManager"
    class="org.springframework.transaction.jta.JtaTransactionManager"
</bean>
3.3.事务同步管理器

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

3.4.事务传播行为

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

事务传播行为类型 说明
PROPAGATION_REQUIRED 如果当前没有事务,则新建一个事务;如果已经存在一个事务,则加入到这个事务中。这是最常见的选择
PROPAGATION_SUPPORTS 支持当前事务,如果没有当前事务,则以非事务方式执行
PROPAGATION_MANDATORY 使用当前事务,如果当前没有事务,则抛出异常
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,则把当前事务挂起
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,则把当前事务挂起
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作

4、编程式的事务管理

Spring还是为编程式事务管理提供了模板类TransactionTemplate,以满足一些特殊场合的需要。TransactionTemplate有两个主要方法:

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

由于Spring事务管理基于TransactionSynchronizationManager进行工作,所以,如果在回调接口方法中需要显示访问数据库底层数据连接,则必须通过资源获取工具类得到线程绑定的数据连接。这是Spring事务管理的底层协议,不容违反。如果FourmDao是基于Spring提供模板类构建的,由于模板类已经在内部使用了资源获取工具类获取数据连接,所以用户就不必关心底层数据库连接的获取问题。

5、使用XML配置声明式事务

大多数Spring用户选择声明式事务管理的功能,这种方式对代码的侵入性最小,可以让事务管理代码完全从业务代码中移除,非常符合非侵入式轻量级容器的理念。

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

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

BbtForum拥有4个方法,我们希望addTopic()和updateForum()方法拥有写事务的能力,而其他两个方法只需要拥有读事务的能力就可以了,

@Service
@Transactional
public class BbtForum{

    public ForumDao forumDao;
    public TopicDao topicDao;
    public PostDao  postDao;
    public void addTopic(Topic topic){
        topicDao.addTopic(topic);
        postDao.addPost(topic.getPost());
    }
    
    public Forum getForum(int forumId){
        return forumDao.getForum(forumId);
    }
    
    public void updateForum(Forum forum){
        forumDao.updateForum(forum);
    }
    
    public int getForumNum(){
        return forumDao.getForumNum();
    }
}

BbtForum是一个POJO,只是简单的使用持久层的多个DAO类,通过它们的协作实现业务功能。在这里,我们看不到任何事务操作的代码,所以如果直接使用BbtForum,那么这些方法都将以无事务的方式运行。现在,我们的任务是通过Spring声明式事务配置让这些业务方法拥有适合的事务功能。

5.2.使用原始的TransactionProxyFactoryBean
<!--1、引入DAO和DataSource的配置文件-->
  <import resource="classpath:applicationContext-dao.xml"/>
    
      <!--2、声明事务管理-->
    <bean id="txManager" class="org.springframe.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataDource"/>
    </bean>
    
    <!--3、需要实施事务增强的目标业务Bean-->
    <bean id="bbtForumTarget" class="com.example.SequenceNumber"
                              p:forumDao-ref="forumDao"
                              p:topicDao-ref="topicDao"
                              p:postDao-ref="postDao"                          
    />
    
    <!--4、使用事务代理工厂类为目标业务Bean提供事务增强-->
    <bean id="bbtForum" class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"
            p:transactionManager-ref="txManager"//4、1指定事务管理器
            p:target-ref="bbtForumTarget">//4、2指定目标业务
        <property name="transactionAttributes">//4、3事务属性
            <props>
                <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
                //4、4只读事务
                <prop key="*">PROPAGATION_REQUIRED</prop>
                //4、5可写事务
            </props>
        </property>
    </bean>

按照约定的习惯,需要事务增强的业务类一般将id取名为xxxTarget,如3处所示,这可以在字面上表示该Bean是要被代理的目标Bean

通过TransactionProxyFactoryBean对业务类进行代理,织入事务增强功能,如4处所示,首先需要为该代理类指定事务管理器,这些事务管理器实现了PlatformTransactionManager接口,其次,通过target属性指定需要代理的目标Bean;最后,为业务Bean的不同方法配置事务属性。Spring允许通过键值配置业务方法的事务属性信息,键可以使用通配符,如get代表目标类中所有以get为前缀的方法,它匹配BbtForum的getForum(int forumId)和getForumNum()方法;而key=""匹配BbtForum接口的所有方法

PROPAGATION(传播行为),ISOLATION(隔离级别(可选)),readOnly(是否为只读事务(可选)),-Exception(发生这些异常时回滚事务(可选)),+Exception(发生这些异常时照样提交事务(可选))

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

    <prop key="add*">PROPAGATION_REQUIRED,-Exception</prop>

只要业务方法运行时抛出的异常或其父类异常的类名包括“Exception”,事务都回滚,以下异常都符合这条规则:SQLException、ParseException。正因为Spring采用名称字符串包含的比较方式,所以用户甚至可以将回滚规则设置为“-tion”。

因为Spring默认的事务回滚规则为“运行期异常回滚,检查型异常不回滚”,所以带负号的异常设置仅对检查型异常有意义,此外,用户可以指定多个带正号或负号的事务回滚/提交规则

<prop key="add*">
    PROPAGATION_REQUIRED,-XxxException,-YyyException
</prop>
5.3.基于aop/tx命名空间的配置
 <!--引入DAO和DataSource的配置文件-->
  <import resource="classpath:applicationContext-dao.xml"/>

      <!--声明事务管理-->
    <bean id="txManager" class="org.springframe.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataDource"/>
    </bean>

    <!--使用强大的切点表达式语言轻松定义目标方法-->
    <aop:config>
        <!--2.1.通过aop定义事务增强切面-->
        <aop:pointcut id="serviceMethod" expression="execution(* com.example.SequenceNumber.*(..))"/>
        <!--2.2.引用事务增强-->
        <aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice"/>
    </aop:config>

    <!--3.事务增强-->
    <tx:advice id="txAdvice" transaction-manager="txManager">
        <!--3.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>

6.使用主机配置声明式事务

除了基于XML的事务配置,Spring还提供了基于注解的事务配置,即通过@Transactional对需要事务增强的Bean接口、实现类或方法进行标注;在容器中配置基于注解的事务增强驱动,即可启用基于注解的声明式事务,笔者在实际项目中一般采用这种配置

6.1.使用@Transactional注解

使用@Transactional对基于aop/tx的命名空间的事务配置进行改造

@Service
@Transactional
public class BbtForum{

    public ForumDao forumDao;
    public TopicDao topicDao;
    public PostDao  postDao;
    public void addTopic(Topic topic){
        topicDao.addTopic(topic);
        postDao.addPost(topic.getPost());
    }
    
    public Forum getForum(int forumId){
        return forumDao.getForum(forumId);
    }
    
    public void updateForum(Forum forum){
        forumDao.updateForum(forum);
    }
    
    public int getForumNum(){
        return forumDao.getForumNum();
    }
}

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

<!--对标注@Transactional注解的Bean进行加工处理,以织入事务管理切面-->
<tx:annotation-driven transaction-manager="txManager>/

<tx:annotation-driven>还有两个属性

@Service
public class MultiForumService{
    //1、使用名为topic的事务管理器
    @Transactional("topic")
    public void addTopic(Topic topic){
        ...
    }
    
    //2、使用名为forum的事务管理器
    @Transactional("forum")
    public void updateForum(Forum forum){
        ...
    }
}

<bean id="forumTxManager" class="org.springframe.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="forumDataSource">
        <qualifier value="forum"/>
</bean>

<bean id="topicTxManager" class="org.springframe.jdbc.datasource.DataSourceTransactionManager"
p:dataSource-ref="topicDataSource">
        <qualifier value="topic"/>
</bean>

<tx:annotation-driven>

在1处,为事务管理器指定了一个数据源,每个事务管理器都可以绑定一个独立的数据源,在2处,指定了一个可被@Transactional注解引用的事务管理器标识,如果到处都使用代标识的注解,可以自定义一个绑定到特定事务管理器的注解,然后直接使用这个自定义的注解(感觉没什么意义,都是使用注解)

@Target(ElementType.METHOD,ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Transactional("forum")//绑定到forum的事务管理器中
public @interface ForumTransaction{
    
}

7.集成特定的应用服务器

一般来说,Spring的事务抽象与应用服务器是无关的,不过,如果用户希望事务管理器使用特定的UserTransaction和TransactionManager对象(可以被自动探测),以获取更多的事务控制功能(如事务挂起等),则可以采用Spring为集成这些应用服务器所提供的适配器

7.1.BEA WebLogic
<bean id="txManager" class="org.springframework.transaction.jta.WebLogicJtaTransactionManager"/>
7.2.WebSphere
<bean id="txManager" class="org.springframework.transaction.jta.WebSphereUowTransactionManager">
上一篇下一篇

猜你喜欢

热点阅读