热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

begintransaction事务的作用_三问Spring事务:解决什么问题?如何解决?存在什么问题?...

文章来源:三问Spring事务:解决什么问题?如何解决?存在什么问题?原文作者:草捏子来源平台&
文章来源:三问Spring事务:解决什么问题?如何解决?存在什么问题?
原文作者:草捏子
来源平台:微信公众号

1. 解决什么问题

让我们先从事务说起,“什么是事务?我们为什么需要事务?”。事务是一组无法被分割的操作,要么所有操作全部成功,要么全部失败。我们在开发中需要通过事务将一些操作组成一个单元,来保证程序逻辑上的正确性,例如全部插入成功,或者回滚,一条都不插入。作为程序员的我们,对于事务管理,所需要做的便是进行事务的界定,即通过类似begin transactionend transaction的操作来界定事务的开始和结束。

下面是一个基本的JDBC事务管理代码:

// 开启数据库连接
Connection con = openConnection();
try {// 关闭自动提交con.setAutoCommit(false);// 业务处理// ... // 提交事务con.commit();
} catch (SQLException | MyException e) {// 捕获异常,回滚事务try {con.rollback();} catch (SQLException ex) {ex.printStackTrace();}
} finally {// 关闭连接try {con.setAutoCommit(true);con.close();} catch (SQLException e) {e.printStackTrace();}
}

直接使用JDBC进行事务管理的代码直观上来看,存在两个问题:

  1. 业务处理代码与事务管理代码混杂;
  2. 大量的异常处理代码(在catch中还要try-catch)。

而如果我们需要更换其他数据访问技术,例如Hibernate、MyBatis、JPA等,虽然事务管理的操作都类似,但API却不同,则需使用相应的API来改写。这也会引来第三个问题:

3.繁杂的事务管理API。

上文列出了三个待解决的问题,下面我们看Spring事务是如何解决。

2. 如何解决

2.1 繁杂的事务管理API

针对该问题,我们很容易可以想到,在众多事务管理的API上抽象一层。通过定义接口屏蔽具体实现,再使用策略模式来决定具体的API。下面我们看下Spring事务中定义的抽象接口。

在Spring事务中,核心接口是PlatformTransactionManager,也叫事务管理器,其定义如下:

public interface PlatformTransactionManager extends TransactionManager {// 获取事务(新的事务或者已经存在的事务)TransactionStatus getTransaction(@Nullable TransactionDefinition definition)throws TransactionException; // 提交事务void commit(TransactionStatus status) throws TransactionException;// 回滚事务void rollback(TransactionStatus status) throws TransactionException;
}

getTransaction通过入参TransactionDefinition来获得TransactionStatus,即通过定义的事务元信息来创建相应的事务对象。在TransactionDefinition中会包含事务的元信息:

  • PropagationBehavior:传播行为;
  • IsolationLevel:隔离级别;
  • Timeout:超时时间;
  • ReadOnly:是否只读。

根据TransactionDefinition获得的TransactionStatus中会封装事务对象,并提供了操作事务查看事务状态的方法,例如:

  • setRollbackOnly:标记事务为Rollback-only,以使其回滚;
  • isRollbackOnly:查看是否被标记为Rollback-only;
  • isCompleted:查看事务是否已完成(提交或回滚完成)。

还支持嵌套事务的相关方法:

  • createSavepoint:创建savepoint;
  • rollbackToSavepoint:回滚到指定savepoint;
  • releaseSavePoint:释放savepoint。

TransactionStatus事务对象可被传入到commit方法或rollback方法中,完成事务的提交或回滚。

下面我们通过一个具体实现来理解TransactionStatus的作用。以commit方法为例,如何通过TransactionStatus完成事务的提交。AbstractPlatformTransactionManagerPlatformTransactionManager接口的的实现,作为模板类,其commit实现如下:

public final void commit(TransactionStatus status) throws TransactionException {// 1.检查事务是否已完成if (status.isCompleted()) {throw new IllegalTransactionStateException("Transaction is already completed - do not call commit or rollback more than once per transaction");}// 2.检查事务是否需要回滚(局部事务回滚)DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status;if (defStatus.isLocalRollbackOnly()) {if (defStatus.isDebug()) {logger.debug("Transactional code has requested rollback");}processRollback(defStatus, false);return;}// 3.检查事务是否需要回滚(全局事务回滚)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;}// 4.提交事务processCommit(defStatus);
}

commit模板方法中定义了事务提交的基本逻辑,通过查看status的事务状态来决定抛出异常还是回滚,或是提交。其中的processRollbackprocessCommit方法也是模板方法,进一步定义了回滚、提交的逻辑。以processCommit方法为例,具体的提交操作将由抽象方法doCommit完成。

protected abstract void doCommit(DefaultTransactionStatus status) throws TransactionException;

doCommit的实现取决于具体的数据访问技术。我们看下JDBC相应的具体实现类DataSourceTransactionManager中的doCommit实现。

protected void doCommit(DefaultTransactionStatus status) {// 获取status中的事务对象 DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();// 通过事务对象获得数据库连接对象Connection con = txObject.getConnectionHolder().getConnection();if (status.isDebug()) {logger.debug("Committing JDBC transaction on Connection [" + con + "]");}try {// 执行commitcon.commit();}catch (SQLException ex) {throw new TransactionSystemException("Could not commit JDBC transaction", ex);}
}

commitprocessCommit方法中我们根据入参的TransactionStatus提供的事务状态来决定事务行为,而在doCommit中需要执行事务提交时将会通过TransactionStatus中的事务对象来获得数据库连接对象,再执行最后的commit操作。通过这个示例我们可以理解TransactionStatus所提供的事务状态和事务对象的作用。

下面是用Spring事务API改写后的事务管理代码:

// 获得事务管理器
PlatformTransactionManager txManager = getPlatformTransactionManager();
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// 指定事务元信息
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 获得事务
TransactionStatus status = txManager.getTransaction(def);
try {// 业务处理
}
catch (MyException ex) {// 捕获异常,回滚事务txManager.rollback(status);throw ex;
}
// 提交事务
txManager.commit(status);

无论是使用JDBC、Hibernate还是MyBatis,我们只需要传给txManager相应的具体实现就可以在多种数据访问技术中切换。

小结:Spring事务通过PlatformTransactionManagerTransactionDefinitionTransactionStatus接口统一事务管理API,并结合策略模式和模板方法决定具体实现。

Spring事务API代码还有个特点有没有发现,SQLException不见了。下面来看Spring事务是如何解决大量的异常处理代码。

2.2 大量的异常处理代码

为什么使用JDBC的代码中会需要写这么多的异常处理代码。这是因为Connection的每个方法都会抛出SQLException,而SQLException又是检查异常,这就强制我们在使用其方法时必须进行异常处理。那Spring事务是如何解决该问题的。我们看下doCommit方法:

protected void doCommit(DefaultTransactionStatus status) {DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction();Connection con = txObject.getConnectionHolder().getConnection();if (status.isDebug()) {logger.debug("Committing JDBC transaction on Connection [" + con + "]");}try {con.commit();}catch (SQLException ex) {// 异常转换throw new TransactionSystemException("Could not commit JDBC transaction", ex);}
}

Connectioncommit方法会抛出检查异常SQLException,在catch代码块中SQLException将被转换成TransactionSystemException抛出,而TransactionSystemException是一个非检查异常。通过将检查异常转换成非检查异常,让我们能够自行决定是否捕获异常,不强制进行异常处理。

Spring事务中几乎为数据库的所有错误都定义了相应的异常,统一了JDBC、Hibernate、MyBatis等不同异常API。这有助于我们在处理异常时使用统一的异常API接口,无需关心具体的数据访问技术。

小结:Spring事务通过异常转换避免强制异常处理。

2.3 业务处理代码与事务管理代码混杂

在2.1节中给出了使用Spring事务API的写法,即编程式事务管理,但仍未解决“业务处理代码与事务管理代码混杂”的问题。这时候就可以利用Spring AOP将事务管理代码这一横切关注点从代码中剥离出来,即声明式事务管理。以注解方式为例,通过为方法标注@Transaction注解,将为该方法提供事务管理。其原理如下图所示:

926ea2438a4544961ede606eb89ff637.png

Spring事务会为@Transaction标注的方法的类生成AOP增强的动态代理类对象,并且在调用目标方法的拦截链中加入TransactionInterceptor进行环绕增加,实现事务管理。

下面我们看下TransactionInterceptor中的具体实现,其invoke方法中将调用invokeWithinTransaction方法进行事务管理,如下所示:

protected Object invokeWithinTransaction(Method method, Class targetClass, final InvocationCallback invocation)throws Throwable {// 查询目标方法事务属性、确定事务管理器、构造连接点标识(用于确认事务名称)final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);final PlatformTransactionManager tm = determineTransactionManager(txAttr);final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {// 创建事务TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);Object retVal = null;try {// 通过回调执行目标方法retVal = invocation.proceedWithInvocation();}catch (Throwable ex) {// 目标方法执行抛出异常,根据异常类型执行事务提交或者回滚操作completeTransactionAfterThrowing(txInfo, ex);throw ex;}finally {// 清理当前线程事务信息cleanupTransactionInfo(txInfo);}// 目标方法执行成功,提交事务commitTransactionAfterReturning(txInfo);return retVal;} else {// 带回调的事务执行处理,一般用于编程式事务// ...}
}

在调用目标方法前后加入了创建事务、处理异常、提交事务等操作。这让我们不必编写事务管理代码,只需通过@Transaction的属性指定事务相关元信息。

小结:Spring事务通过AOP提供声明式事务将业务处理代码和事务管理代码分离。

3. 存在什么问题

Spring事务为了我们解决了第一节中列出的三个问题,但同时也会带来些新的问题。

3.1 非public方法失效

@Transactional只有标注在public级别的方法上才能生效,对于非public方法将不会生效。这是由于Spring AOP不支持对private、protect方法进行拦截。从原理上来说,动态代理是通过接口实现,所以自然不能支持private和protect方法的。而CGLIB是通过继承实现,其实是可以支持protect方法的拦截的,但Spring AOP中并不支持这样使用,笔者猜测做此限制是出于代理方法应是public的考虑,以及为了保持CGLIB和动态代理的一致。如果需要对protect或private方法拦截则建议使用AspectJ。

3.2 自调用失效

当通过在Bean的内部方法直接调用带有@Transactional的方法时,@Transactional将失效,例如:

public void saveAB(A a, B b)
{saveA(a);saveB(b);
}@Transactional
public void saveA(A a)
{dao.saveA(a);
}@Transactional
public void saveB(B b)
{dao.saveB(b);
}

在saveAB中调用saveA和saveB方法,两者的@Transactional都将失效。这是因为Spring事务的实现基于代理类,当在内部直接调用方法时,将不会经过代理对象,而是直接调用目标对象的方法,无法被TransactionInterceptor拦截处理。解决办法:

(1)ApplicationContextAware

通过ApplicationContextAware注入的上下文获得代理对象。

public void saveAB(A a, B b)
{Test self = (Test) applicationContext.getBean("Test");self.saveA(a);self.saveB(b);
}

(2)AopContext

通过AopContext获得代理对象。

public void saveAB(A a, B b)
{Test self = (Test)AopContext.currentProxy();self.saveA(a);self.saveB(b);
}

(3)@Autowired

通过@Autowired注解注入代理对象。

@Component
public class Test {@AutowiredTest self;public void saveAB(A a, B b){self.saveA(a);self.saveB(b);}// ...
}

(4)拆分

将saveA、saveB方法拆分到另一个类中。

public void saveAB(A a, B b)
{txOperate.saveA(a);txOperate.saveB(b);
}

上述两个问题都是由于Spring事务的实现方式的限制导致的问题。下面再看两个由于使用不当容易犯错的两个问题。

3.3 检查异常默认不回滚

在默认情况下,抛出非检查异常会触发回滚,而检查异常不会。

根据invokeWithinTransaction方法,我们可以知道异常处理逻辑在completeTransactionAfterThrowing方法中,其实现如下:

protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) {if (txInfo != null && txInfo.getTransactionStatus() != null) {if (logger.isTraceEnabled()) {logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() +"] after exception: " + ex);}if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) {try {// 异常类型为回滚异常,执行事务回滚txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus());}catch (TransactionSystemException ex2) {logger.error("Application exception overridden by rollback exception", ex);ex2.initApplicationException(ex);throw ex2;}catch (RuntimeException | Error ex2) {logger.error("Application exception overridden by rollback exception", ex);throw ex2;}}else {try {// 异常类型为非回滚异常,仍然执行事务提交txInfo.getTransactionManager().commit(txInfo.getTransactionStatus());}catch (TransactionSystemException ex2) {logger.error("Application exception overridden by commit exception", ex);ex2.initApplicationException(ex);throw ex2;}catch (RuntimeException | Error ex2) {logger.error("Application exception overridden by commit exception", ex);throw ex2;}}}
}

根据rollbackOn判断异常是否为回滚异常。只有RuntimeExceptionError的实例,即非检查异常,或者在@Transaction中通过rollbackFor属性指定的回滚异常类型,才会回滚事务。否则将继续提交事务。所以如果需要对非检查异常进行回滚,需要记得指定rollbackFor属性,不然将回滚失效。

3.4 catch异常无法回滚

在3.3节中我们说到只有抛出非检查异常或是rollbackFor中指定的异常才能触发回滚。如果我们把异常catch住,而且没抛出,则会导致无法触发回滚,这也是开发中常犯的错误。例如:

@Transactional
public void insert(List users) {try {JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);for (User user : users) {String insertUserSql = "insert into User (id, name) values (?,?)";jdbcTemplate.update(insertUserSql, new Object[] { user.getId(),user.getName() });}} catch (Exception e) {e.printStackTrace();}
}

这里由于catch住了所有Exception,并且没抛出。当插入发生异常时,将不会触发回滚。

但同时我们也可以利用这种机制,用try-catch包裹不用参与事务的数据操作,例如对于写入一些不重要的日志,我们可将其用try-catch包裹,避免抛出异常,则能避免写日志失败而影响事务的提交。



推荐阅读
  • 在springmvc框架中,前台ajax调用方法,对图片批量下载,如何弹出提示保存位置选框?Controller方法 ... [详细]
  • 如何实现织梦DedeCms全站伪静态
    本文介绍了如何通过修改织梦DedeCms源代码来实现全站伪静态,以提高管理和SEO效果。全站伪静态可以避免重复URL的问题,同时通过使用mod_rewrite伪静态模块和.htaccess正则表达式,可以更好地适应搜索引擎的需求。文章还提到了一些相关的技术和工具,如Ubuntu、qt编程、tomcat端口、爬虫、php request根目录等。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • SpringBoot uri统一权限管理的实现方法及步骤详解
    本文详细介绍了SpringBoot中实现uri统一权限管理的方法,包括表结构定义、自动统计URI并自动删除脏数据、程序启动加载等步骤。通过该方法可以提高系统的安全性,实现对系统任意接口的权限拦截验证。 ... [详细]
  • Android中高级面试必知必会,积累总结
    本文介绍了Android中高级面试的必知必会内容,并总结了相关经验。文章指出,如今的Android市场对开发人员的要求更高,需要更专业的人才。同时,文章还给出了针对Android岗位的职责和要求,并提供了简历突出的建议。 ... [详细]
  • 本文介绍了在序列化时如何对SnakeYaml应用格式化,包括通过设置类和DumpSettings来实现定制输出的方法。作者提供了一个示例,展示了期望的yaml生成格式,并解释了如何使用SnakeYaml的特定设置器来实现这个目标。对于正在使用SnakeYaml进行序列化的开发者来说,本文提供了一些有用的参考和指导。摘要长度为169字。 ... [详细]
  • 2018年人工智能大数据的爆发,学Java还是Python?
    本文介绍了2018年人工智能大数据的爆发以及学习Java和Python的相关知识。在人工智能和大数据时代,Java和Python这两门编程语言都很优秀且火爆。选择学习哪门语言要根据个人兴趣爱好来决定。Python是一门拥有简洁语法的高级编程语言,容易上手。其特色之一是强制使用空白符作为语句缩进,使得新手可以快速上手。目前,Python在人工智能领域有着广泛的应用。如果对Java、Python或大数据感兴趣,欢迎加入qq群458345782。 ... [详细]
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • 在重复造轮子的情况下用ProxyServlet反向代理来减少工作量
    像不少公司内部不同团队都会自己研发自己工具产品,当各个产品逐渐成熟,到达了一定的发展瓶颈,同时每个产品都有着自己的入口,用户 ... [详细]
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • 企业数据应用挑战及元数据管理的重要性
    本文主要介绍了企业在日常经营管理过程中面临的数据应用挑战,包括数据找不到、数据读不懂、数据不可信等问题。针对这些挑战,通过元数据管理可以实现数据的可见、可懂、可用,帮助业务快速获取所需数据。文章提出了“灵魂”三问——元数据是什么、有什么用、又该怎么管,强调了元数据管理在企业数据治理中的基础和前提作用。 ... [详细]
  • 本文介绍了前端人员必须知道的三个问题,即前端都做哪些事、前端都需要哪些技术,以及前端的发展阶段。初级阶段包括HTML、CSS、JavaScript和jQuery的基础知识。进阶阶段涵盖了面向对象编程、响应式设计、Ajax、HTML5等新兴技术。高级阶段包括架构基础、模块化开发、预编译和前沿规范等内容。此外,还介绍了一些后端服务,如Node.js。 ... [详细]
  • 本文介绍了Swing组件的用法,重点讲解了图标接口的定义和创建方法。图标接口用来将图标与各种组件相关联,可以是简单的绘画或使用磁盘上的GIF格式图像。文章详细介绍了图标接口的属性和绘制方法,并给出了一个菱形图标的实现示例。该示例可以配置图标的尺寸、颜色和填充状态。 ... [详细]
author-avatar
梦苓718
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有