Spring事务管理---上

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring事务管理---上相关的知识,希望对你有一定的参考价值。

Spring事务管理---上


本系列文章:

Spring事务王国概览


本文涉及到的AOP组件,例如ProxyFactory等,如果不清楚的建议先去翻看我之前的AOP系列文章


编程式事务管理


回顾一下上面这张图:

  • 通过Spring进行编程式事务管理有两种方式,要么直接使用PlatformTransactionManager,要么使用更方便的TransactionTemplate。

对于编程式事务而言,更推荐使用TransactionTemplate进行编程式事务管理。


使用PlatformTransactionManager进行编程式事务管理

PlatformTransactionManager接口定义了事务界定的基本操作,我们可以直接使用它来进行编程式事务管理。

public class TransactionMain 
    public static void main(String[] args) throws ClassNotFoundException, SQLException 
        test();
    

    private static void test() 
        DataSource dataSource = getDS();
        JdbcTransactionManager jtm = new JdbcTransactionManager(dataSource);
        //JdbcTransactionManager根据TransactionDefinition信息来进行一些连接属性的设置
        //包括隔离级别和传播行为等
        DefaultTransactionDefinition transactionDef = new DefaultTransactionDefinition();
        //开启一个新事务---此时autocommit已经被设置为了false,并且当前没有事务,这里创建的是一个新事务
        TransactionStatus ts = jtm.getTransaction(transactionDef);
        //进行业务逻辑操作
        try 
            update(dataSource);
            jtm.commit(ts);
        catch (Exception e)
            jtm.rollback(ts);
            System.out.println("发生异常,我已回滚");
        
    

    private static void update(DataSource dataSource) throws Exception 
        JdbcTemplate jt = new JdbcTemplate();
        jt.setDataSource(dataSource);
        jt.update("UPDATE Department SET Dname=\\"大忽悠\\" WHERE id=6");
        throw new Exception("我是来捣乱的");
    

只要为TransactionManager提供合适的PlatformTransactionManager实现,然后结合TransactionDefinition开启事务,并结合TransactionStatus来回滚或者提交事务,就可以完成针对当前对象的整个事务管理。

PlatformTransactionManager虽然帮助我们用抽象事务操作屏蔽了不同事务管理的API差异,但是如果在程序中大量使用PlatformTransactionManager来进行事务管理,那还是会有很多重复代码。

这个时候就可以借鉴一下Spring的JDBCTemplate的设计思想,使用模板方法模式加callBack相互结合的方式,对直接使用PlatformTransactionManager进行事务管理的代码封装,这就有了更加方便的编程式事务管理方式,即使用TransactionTemplate的编程式事务管理。


使用TransactionTemplate进行编程式事务管理

TransactionTemplate负责对PlatformTransactionManager的固定事务操作和异常处理进行模板化封装。开发人员更多地关注与通过相应的Callback接口提供具体的事务界定内容即可。

Spring为TransactionTemplate提供了两个回调接口,分别为TransactionCallback和TransactionCallbackWithoutResult,二者的区别在于是否需要返回执行结果。

使用TransactionTemplate进行事务管理的代码如下:

public class TransactionMain 
    public static void main(String[] args) throws ClassNotFoundException, SQLException 
        test();
    

    private static void test() 
        DataSource dataSource = getDS();
        JdbcTransactionManager jtm = new JdbcTransactionManager(dataSource);
        TransactionTemplate tt = new TransactionTemplate(jtm);
        tt.execute((transactionStatus)->
            JdbcTemplate jt = new JdbcTemplate();
            jt.setDataSource(dataSource);
            int update = jt.update("UPDATE Department SET Dname=\\"大忽悠\\" WHERE id=6");
            return update;
        );
    

    private static void update(DataSource dataSource) 
        JdbcTemplate jt = new JdbcTemplate();
        jt.setDataSource(dataSource);
        jt.update("UPDATE Department SET Dname=\\"大忽悠\\" WHERE id=6");
    

可以稍微看看他的excute模板方法是如何实现的:

	public <T> T execute(TransactionCallback<T> action) throws TransactionException 
		Assert.state(this.transactionManager != null, "No PlatformTransactionManager set");

		if (this.transactionManager instanceof CallbackPreferringPlatformTransactionManager) 
			return ((CallbackPreferringPlatformTransactionManager) this.transactionManager).execute(this, action);
		
		else 
			TransactionStatus status = this.transactionManager.getTransaction(this);
			T result;
			try 
			//执行业务代码
				result = action.doInTransaction(status);
			
			//出现异常就回滚
			catch (RuntimeException | Error ex) 
				// Transactional code threw application exception -> rollback
				rollbackOnException(status, ex);
				throw ex;
			
			catch (Throwable ex) 
				// Transactional code threw unexpected exception -> rollback
				rollbackOnException(status, ex);
				throw new UndeclaredThrowableException(ex, "TransactionCallback threw undeclared checked exception");
			
			//没出现异常就提交,然后返回结果即可
			this.transactionManager.commit(status);
			return result;
		
	

我们可以使用CallBack接口公开的TransactionStatus将事务标记为rollbackonly。

TransactionTemplate在commit的时候,会先检测rollbackonly是否被设置了,如果被设置了,可以改为回滚事务。

	public final void commit(TransactionStatus status) throws TransactionException 
		if (status.isCompleted()) 
			throw new IllegalTransactionStateException(
					"Transaction is already completed - do not call commit or rollback more than once per transaction");
		

		DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;
		if (defStatus.isLocalRollbackOnly()) 
			if (defStatus.isDebug()) 
				logger.debug("Transactional code has requested rollback");
			
			processRollback(defStatus, false);
			return;
		

		if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) 
			if (defStatus.isDebug()) 
				logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");
			
			processRollback(defStatus, true);
			return;
		

		processCommit(defStatus);
	

对于事务操作中可能抛出的checked exception异常,如果想回滚事务,又不想以unchecked exception的形式向上层传播的话,我们可以通过拿到TransactionStatus,燃烧设置rollbacknoly字段来达到一箭双雕的目的。


编程创建基于SavePoint的嵌套事务

TransactionStatus不仅可以在事务处理期间通过setRollbackOnly()方法来干预事务的状态。

如果需要,作为savepointManager,它也可以帮助我们使用Savepoint机制来创建嵌套事务。

先复习一下mysql中savepoint的用法吧:

SET autocommit=0;
START TRANSACTION;
DELETE FROM test1 WHERE  NAME='大忽悠';
SAVEPOINT a;#设置保存点
DELETE FROM test1 WHERE  NAME='小朋友';
ROLLBACK TO a;#回滚到保存点

SAVEPOINT的好处在于可以选择回滚一部分事务,举个例子: 如果我们要进行转账,从一个账号中取出前,如果向主账户转账失败,那么就将钱转入备用账户,这个例子,可以用sql语句写成下面这样:

SET autocommit=0;
START TRANSACTION;
UPDATE account SET money=money-100 WHERE name="取款账户";
SAVEPOINT a;#设置保存点
UPDATE account SET money=money+100 WHERE name="主账户";
ROLLBACK TO a;#回滚到保存点
#如果向临时账户转账失败了,那么回滚整个事务
UPDATE account SET money=money+100 WHERE name="临时账户";
#转账成功
commit;
#向临时账户转账都失败的话
rollback;

下面用代码演示一下savepoint的使用:

public class TransactionMain 
    private static DataSource dataSource = getDS();
    private static TransactionTemplate tt = new TransactionTemplate(new JdbcTransactionManager(dataSource));
    private static JdbcTemplate jt = new JdbcTemplate(dataSource);

    public static void main(String[] args) throws ClassNotFoundException, SQLException 
        test();
    

    private static void test() 
        tt.execute((transactionStatus)->
            try 
                withDraw();
                Object savepoint = transactionStatus.createSavepoint();
                try
                    saveMoney("主账户");
                    System.out.println(1/0);
                catch (Exception e)
                    transactionStatus.rollbackToSavepoint(savepoint);
                    saveMoney("备用账户");
                finally 
                    transactionStatus.releaseSavepoint(savepoint);
                
            catch (Exception e)
                transactionStatus.setRollbackOnly();
            
            return null;
        );
    

    private static void withDraw() 
        jt.update("UPDATE account SET money=money-100 WHERE `name`='取款账户'");
    

    private static void saveMoney(String name) 
        jt.update("UPDATE account SET money=money+100 WHERE `name`= '"+name+"'");
    


如果转账期间抛出的是unchecked exception,最外层的捕捉是没有必要的,因为TransactionTempalte将自动回滚事务。

注意: 不同的传播行为加上多次尝试去创建事务,可能会导致取款和存款的操作不在同一个事务中,这样就违反了事务的ACID属性。

一般情况下借助TransactionStatus的Savepoint来实现嵌套事务并非唯一方式,

更推荐使用结合PROPAGATION_NESTED传播行为的声明式事务管理方式。


声明式事务管理

编程式事务的缺点在于事务代码与业务代码相互耦合,如果要解除这种耦合关系关系,就必须将事务代码与业务代码拆分开来。


其实我们完成可以将事务代码抽象成一个模板方法,然后将业务方法放置在指定中间位置处执行即可,这里可以将业务方法以回调接口形式公开出来,就像TransactionTemplate一样。

也可以通过代理的思想,被业务对象进行动态代理,而将这部分事务模板diam放在拦截器中去执行。

显然,后者更加解耦,因为可以让程序员完全与事务代码说拜拜,因此这种方法也被称为声明式事务,因为我们还是需要声明一下当前事务方法需要什么事务支持,例如:设置一下隔离级别,超时时间,传播行为等。


模拟声明式事务

模拟之前先思考下面几个问题:

  • 针对每个对象业务方法的拦截,需要知道该方法是否需要事务支持,如果需要,针对该事务的TransactionDefintion相关信息又从哪里获得?
  • 如果调用方法过程中抛出异常,如果对这些异常进行处理,哪些异常抛出需要回滚,哪些不需要呢?

我们需要提供某种方式来记载业务方法与对应的事务信息之间的映射关系,拦截器只要查询这种映射关系,就可以知道要不要为当前业务方法创建事务。

如果要创建事务,则以业务方法作为标记到映射关系中查找创建事务需要的信息,然后创建事务。

一般这种映射关系都是在配置文件中或通过注解提供的,这种映射关系,一般被叫做驱动事务的元数据(Metadata)。

至于事务执行过程中对哪些事务需要回滚进行判断,则可以通过:


也就是TransactionDefintion的子类TransactionAttribute,TransactionAttribute中新增了rollbackOn方法,可以通过这个属性的设置告诉拦截器出现什么异常时应该回滚。


上面铺垫了那么多,下面实战一下:

  • 业务对象准备
public class TestDao 
    private final JdbcTemplate jt;

    public TestDao(JdbcTemplate jt) 
        this.jt = jt;
    

    public void update() throws Exception 
        jt.update("UPDATE Department SET Dname=\\"大忽悠\\" WHERE id=6");
        throw new RuntimeException("我是来捣乱的");
    


public class TestService 
    private final TestDao testDao;

    public TestService(TestDao testDao) 
        this.testDao = testDao;
    

    @TestTransactional
    public void update() throws Exception 
        testDao.update();
    

  • 拦截器准备
@Component("ti")
@RequiredArgsConstructor
public class TransactionInterceptor implements MethodInterceptor 
    private final PlatformTransactionManager tm;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable 
        Method method = invocation.getMethod();
        TransactionDefinition td = getTransactionDefintionFormMethod(method);
        TransactionStatus ts = tm.getTransaction(td);
        Object res=null;
        try
            res=invocation.proceed();
        catch (Throwable t)
            if(needRollbackOn(t,td))
                tm.rollback(ts);
            else
                tm.commit(ts);
            
            throw t;
        
        tm.commit(ts);
        return res;
    


    private boolean needRollbackOn(Throwable t, TransactionDefinition td) 
       return ((TransactionAttribute)td).rollbackOn(t);
    

    /**
     * 默认回滚unchecked ex
     */
    private TransactionDefinition getTransactionDefintionFormMethod(Method method) 
      return new DefaultTransactionAttribute();
    

  • 配置类
@Configuration
public class TestConfig implements BeanFactoryAware 
    private BeanFactory beanFactory;
    @Bean
    public DataSource dataSource()
       return getDS();
    

    @Bean
    public JdbcTemplate jdbcTemplate()
        return new JdbcTemplate(dataSource());
    

    @Bean
    public PlatformTransactionManager tm()
       return new JdbcTransactionManager(dataSource());
    

    @Bean
    public TestDao testDao()
        return new TestDao(jdbcTemplate());
    

    @Bean
    public TestService testService()
        return new TestService(testDao());
    
    
    //生成代理对象
    @Bean("ts")
    public TestService serviceProxy()
        ProxyFactoryBean proxyFactoryBean=new ProxyFactoryBean();
        proxyFactoryBean.setTarget(testService());
        //ProxyFactoryBean需要和IOC搭配使用,因此需要给他设置一个IOC容器
        //这里给出的是拦截器在IOC中的beanName
        proxyFactoryBean.setInterceptorNames("ti");
        proxyFactoryBean.setBeanFactory(beanFactory);
        return (TestService) proxyFactoryBean.getObject();
    


    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException 
               this.beanFactory=beanFactory;
    


  • 测试
@SpringBootApplication
public class TransactionMain 
    public static void main(String[] args) throws ClassNotFoundException, SQLException 
        ConfigurableApplicationContext app = SpringApplication.run(TransactionMain.class, args);
        TestService ts = (TestService) app.getBean("ts");
        try 
            ts.update();
         catch (Exception e) 
            e.printStackTrace();
        
    

这边经过测试是会进行回滚的,大家可以自行尝试


小结

上面只是一个简单的模拟,实际的拦截器设计会比上面复杂一些。

Spring提供了用户声明事务管理的一切设施(org.springframework.transaction.interceptor.TransactionInterceptor),对于我们来说,所要做的只是决定使用XML元数据驱动,还是使用注解元数据驱动的声明式事务管理。


XML元数据驱动的声明式事务

Spring允许我们在IOC容器的配置文件中直接指定事务相关的元数据,我们可以使用以下四种方式在IOC容器配置文件中指定事务需要的元数据。

  • ProxyFactory或者ProxyFactoryBean+TransactionInterceptor
  • 使用"一站式"的TransactionProxyFactoryBean
  • 使用BeanNameAutoProxyCreator
  • 使用Spring 2.x的声明事务配置方式。

当然,这里我采用的是配置类的方式,就不用XML形式,本质都是一样,只不过解析器不同罢了。


ProxyFactory或者ProxyFactoryBean+TransactionInterceptor

ProxyF

以上是关于Spring事务管理---上的主要内容,如果未能解决你的问题,请参考以下文章

spring 事务管理

Spring Cloud Stream 手动偏移管理

Spring事务扩展篇

Spring事务管理注意小事项

Spring——事务注解@Transactional建议收藏

Spring AOP-事务管理

(c)2006-2024 SYSTEM All Rights Reserved IT常识