Spring数据访问和事务

  • 1、模型
  • 2、解耦
  • 3、实现
    • 3.1 核心接口
    • 3.2 代码分析
      • 3.2.1 事务管理
      • 3.2.2 数据访问
  • 4、使用
    • 4.1 编程模式
    • 4.2 配置模式
      • 4.2.1 声明式配置方式
      • 4.2.2 注解式配置方式
  • 5、总结

1、模型

在一般的编程习惯中,Spring的数据访问和事务处理的层次结构归纳如下图所示:

                                                          图. 1

2、解耦

Spring事务作为一个独立的组件,其目的就是为了与数据访问组件进行分离,这也是Spring事务框架设计的原则。根据这一职责清晰的原则,Spring在设计时就对事务和数据访问进行了很好的职责划分,这个可以从spring-tx和spring-jdbc这两个包就可以看出来。

但是在实际的代码中,会遇到一个棘手的问题:事务和数据访问操作都需要同一个数据库连接资源,那么它们之间怎么传递呢?

这里涉及三个方面:一是线程安全,二是资源的唯一性,三是事务和数据访问的解耦。

                                                                                     图. 2

在图2中的1、2和3这三个地方都需要使用数据库连接,并且是同一个连接。Spring的做法是将该连接放在一个统一的地方,要使用该资源,都从这个地方获取,这样就解决了事务模块和数据访问模块之间的紧耦合。

解除耦合之后,对于不同的ORM技术,则需要提供不同的事务管理实现,如下图所示:

                                                                              图. 3

3、实现

3.1 核心接口

Spring事务框架的核心接口是:TransactionDefinition,TransactionStatus和PlatformTransactionManager。

TransactionDefinition用于定义事务属性,包括事务的隔离级别,事务的传播行为,事务的超时时间和是否为只读事务。对于隔离级别和传播行为这里就不深入了,可以网上找到很多资料。事务的超时时间就是一个事务需要在规定的时间里完成。只读事务表示在一个事务里不允许进行写操作。

TransactionStatus表示整个事务处理过程中的事务状态,通过事务状态可以进行事务的相应操作。

PlatformTransactionManager是对整个事务行为的抽象,定义了一个完整事务过程中的相关操作。对于不同的ORM技术,需要有不同的实现。

3.2 代码分析

下面简单分析一下事务和数据访问之间数据库连接资源是如何传递的,以JdbcTemplate为例,代码如下:

3.2.1 事务管理

                                                             图. 4

代码1:事务管理器首先获取事务对象(具体步骤请看代码2),然后根据事务对象判断是否已经存在事务,如果已经存在事务,则根据传播特性做进一步处理,这里就不介绍了;如果不存在事务,则开启一个新事务,开启新事务的具体内容见代码3。

 1 //************************************** 代码1: AbstractPlatformTransactionManager.java ****************************
 2 public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
 3    Object transaction = doGetTransaction();                                                   //获取事务
 4    ......
 5    if (isExistingTransaction(transaction)) {                                                  //如果已经存在事务,则根据传播特性再进一步处理
 6       // Existing transaction found -> check propagation behavior to find out how to behave.
 7       return handleExistingTransaction(definition, transaction, debugEnabled);
 8    }
 9  
10    // No existing transaction found -> check propagation behavior to find out how to proceed.
11    if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
12       throw new IllegalTransactionStateException(
13             "No existing transaction found for transaction marked with propagation 'mandatory'");
14    }
15    else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||  //如果不存在事务,则开启一个新事务
16          definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
17       definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
18       SuspendedResourcesHolder suspendedResources = suspend(null);
19        
20       try {
21          boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
22          DefaultTransactionStatus status = newTransactionStatus(
23                definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
24          doBegin(transaction, definition);                                                      //开启事务
25          prepareSynchronization(status, definition);
26          return status;
27       }
28       ......
29    }
30    ......
31 }
View Code

代码2:获取事务对象,事务对象中包含了一个数据库连接,这个数据库连接是从事务同步管理器中获取的。事务同步管理器管理了一个ThreadLocal变量,它用于存放当前线程使用的数据连接资源。

//***************************************** 代码2: DataSourceTransactionManager.java *****************************************
@Override
protected Object doGetTransaction() {
   DataSourceTransactionObject txObject = new DataSourceTransactionObject();
   txObject.setSavepointAllowed(isNestedTransactionAllowed());
   ConnectionHolder conHolder =
      (ConnectionHolder) TransactionSynchronizationManager.getResource(this.dataSource);  //通过事务同步管理器获取连接,从当前线程的ThreadLocal中获取
   txObject.setConnectionHolder(conHolder, false);
   return txObject;
}
View Code

代码3:开启事务,从事务对象中获取连接,如果连接不存在则从数据库连接池中获取新的连接,关闭自动提交,设定事务超时时间,最后将数据库连接存储在事务同步管理中,即绑定在当前线程上。

//***************************************** 代码3: DataSourceTransactionManager.java *****************************************
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
   DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
   Connection con = null;
 
   try {
      if (txObject.getConnectionHolder() == null ||                                  //如果事务对象里没有连接,则从数据源(连接池)中获取新的连接
            txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
         Connection newCon = this.dataSource.getConnection();
         if (logger.isDebugEnabled()) {
            logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction");
         }
         txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
      }
 
      txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
      con = txObject.getConnectionHolder().getConnection();
 
      Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
      txObject.setPreviousIsolationLevel(previousIsolationLevel);
 
      // Switch to manual commit if necessary. This is very expensive in some JDBC drivers,
      // so we don't want to do it unnecessarily (for example if we've explicitly
      // configured the connection pool to set it already).
      if (con.getAutoCommit()) {
         txObject.setMustRestoreAutoCommit(true);
         if (logger.isDebugEnabled()) {
            logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
         }
         con.setAutoCommit(false);
      }
      txObject.getConnectionHolder().setTransactionActive(true);
 
      int timeout = determineTimeout(definition);
      if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
         txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
      }
 
      // Bind the session holder to the thread.
      if (txObject.isNewConnectionHolder()) {
         TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder()); //将连接绑定到ThreadLocal中
      }
   }
 
   catch (Throwable ex) {
      DataSourceUtils.releaseConnection(con, this.dataSource);
      throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
   }
}
View Code

3.2.2 数据访问

                                                           图. 5

代码4:数据访问,先获取数据库连接:a、从事务同步管理器中获取连接,如果当前数据访问在一个事务中,那么一定可以获得一个连接;b、如果当前数据方面没有在一个事务中,那么将从数据库连接池中获取新的连接。

//***************************************** 代码4: JdbcTemplate.java *****************************************
public <T> T execute(StatementCallback<T> action) throws DataAccessException {
   Assert.notNull(action, "Callback object must not be null");
 
   Connection con = DataSourceUtils.getConnection(getDataSource());
   ......
}
  
//***************************************** DataSourceUtils.java *********************************************
public static Connection doGetConnection(DataSource dataSource) throws SQLException {
   Assert.notNull(dataSource, "No DataSource specified");
 
   ConnectionHolder conHolder = (ConnectionHolder) TransactionSynchronizationManager.getResource(dataSource); //先从ThreadLocal中获取连接
   if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
      conHolder.requested();
      if (!conHolder.hasConnection()) {
         logger.debug("Fetching resumed JDBC Connection from DataSource");
         conHolder.setConnection(dataSource.getConnection());
      }
      return conHolder.getConnection();
   }
   // Else we either got no holder or an empty thread-bound holder here.
 
   logger.debug("Fetching JDBC Connection from DataSource");
   Connection con = dataSource.getConnection();                                                       //如果ThreadLocal中没有连接,则从数据源(连接池)中获取新的连接
   ......
}
View Code

4、使用

对于Spring事务的使用最原始的是使用编程模式直接操作事务,这样可以控制事务的所有行为;另一个是我们都熟悉的配置模式。

4.1 编程模式

编程模式使用示例,以JdbcTemplate为例:

首先需要进行适当的配置,从这些配置中间我们可以得到清晰的脉络,从数据源到ORM,再到事务。从另一个角度可以看到,ORM和事务管理器的配置都依赖了数据源,这也说明它们之间在数据库连接上存在不寻常的关系,而这一点已经在上文中进行了讲解。

1. 配置数据源(包括数据库驱动和连接池)

<bean id="meilvDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${driver}"/>
<property name="jdbcUrl" value="${meilv_url}"/>
<property name="user" value="${meilv_username}"/>
<property name="password" value="${meilv_password}"/>
</bean>

2. 配置数据访问方式(ORM)

<!-- jdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="meilvDataSource"/>
</bean>

3. 配置事务管理器

<!-- 数据库的事务管理器配置 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="meilvDataSource"/>
</bean>

4. 编码

然后就是我们自己编写代码了:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext.xml"})
public class TransactionThreadTest {
 
    private static final ExecutorService executor = Executors.newFixedThreadPool(1);
 
    @Autowired
    private PlatformTransactionManager transactionManager;
 
    @Autowired
    private IUserDAO userDAO;
 
    @Test
    public void testTransactionThread() {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        //def.setReadOnly(true);
        Boolean flag = def.isReadOnly();
        TransactionStatus status = transactionManager.getTransaction(def);
 
        try {
 
            // 更新库存
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try {
                        syncDisneyPluStock();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            };
            executor.execute(runnable);
 
            Thread.sleep(1000);
 
            UserDO userDO = new UserDO();
            userDO.setName("lanlan");
            userDAO.insert(userDO);
 
        } catch (Exception e) {
            transactionManager.rollback(status);
        }
        transactionManager.commit(status);
    }
 
    public void syncDisneyPluStock() throws Exception {
        UserDO userDO = new UserDO();
        userDO.setName("dd");
        userDAO.insert(userDO);
        //throw new Exception();
    }
}
View Code

对于编程式事务管理,缺点是事务管理代码和业务逻辑代码相互混杂,并且所有代码都要自己写,异常也都要自己处理,这些都是很繁琐的事情。针对这个情况,Spring提供了TransactionTemplate的编程方式,这种模板方法设计模式统一处理了事务的流程,用户只需要关注自己的特定操作就可以了,具体通过实现相关的回调接口来完成。

顺便提一句:这种模式跟JDBCTemplate的编程方式是一样的,两者虽然在不同模块内,但是设计模式确是相同的。

4.2 配置模式

Spring的事务配置有多种方式,最常见的有XML中的声明式配置方式,还有注解式的配置方式。

4.2.1 声明式配置方式

Spring的事务最终还是通过使用AOP来实现的。声明式配置方式中,通过<aop:pointcut>定义切点,通过<tx:advice>定义通知,通过<aop:advisor>将这两者联系起来形成切面。

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="meilvDataSource"/>
</bean>
 
<!-- 使用tx/aop来配置 -->
<aop:config>
    <!-- 通过aop定义事务增强切面 -->
    <aop:pointcut id="serviceMethod" expression="execution(* com.test.aop.*.*(..))" />
    <!-- 引用事务增强 -->
    <aop:advisor pointcut-ref="serviceMethod" advice-ref="txAdvice" />
</aop:config>
 
<!--事务增强 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <!-- 事务属性定义 -->
    <tx:attributes>
        <tx:method name="action" read-only="false" />
        <tx:method name="work" rollback-for="RuntimeException" />
    </tx:attributes>
</tx:advice>
View Code

4.2.2 注解式配置方式

对于注解式配置方式,只需要在有事务的方法上增加事务注解就会生效。

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="meilvDataSource"/>
</bean>
  
<tx:annotation-driven transaction-manager="transactionManager"/>
View Code

在类或者方法上使用注解@Transactional就可以进行事务管理了。

5、总结

Spring的事务管理和数据访问模块职责相当清晰,认识这一设计原则对我们学习他们具有根本性的作用,本文试图理清两者之间的关系,而这所谓的“关系”就是数据库连接,这也是Spring事务和数据访问都需要依赖的基础。

从另一方面来看,Spring对这两者关系的处理很值得我们学习和借鉴,不管是多线程编程的线程安全性,还是模块之间的解耦。

posted on 2017-01-24 11:12  lanhz  阅读(1842)  评论(0编辑  收藏  举报