`
alfred.w
  • 浏览: 92605 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

事务方法嵌套调用的迷茫

 
阅读更多
事务方法嵌套调用的迷茫
Spring 事务一个被讹传很广说法是:一个事务方法不应该调用另一个事务方法,否则将产生两个事务。结果造成开发人员在设计事务方法时束手束脚,生怕一不小心就踩到地雷。
其实这种是不认识 Spring 事务传播机制而造成的误解,Spring 对事务控制的支持统一在 TransactionDefinition 类中描述,该类有以下几个重要的接口方法:
int getPropagationBehavior():事务的传播行为
int getIsolationLevel():事务的隔离级别
int getTimeout():事务的过期时间
boolean isReadOnly():事务的读写特性。
很明显,除了事务的传播行为外,事务的其它特性 Spring 是借助底层资源的功能来完成的,Spring 无非只充当个代理的角色。但是事务的传播行为却是 Spring 凭借自身的框架提供的功能,是 Spring 提供给开发者最珍贵的礼物,讹传的说法玷污了 Spring 事务框架最美丽的光环。
所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。Spring 支持 7 种事务传播行为:
PROPAGATION_REQUIRED 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。
PROPAGATION_SUPPORTS 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY 使用当前的事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与 PROPAGATION_REQUIRED 类似的操作。
Spring 默认的事务传播行为是 PROPAGATION_REQUIRED,它适合于绝大多数的情况。假设 ServiveX#methodX() 都工作在事务环境下(即都被 Spring 事务增强了),假设程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这 3 个服务类的 3 个方法通过 Spring 的事务传播机制都工作在同一个事务中。
下面,我们来看一下实例,UserService#logon() 方法内部调用了 UserService#updateLastLogonTime() 和 ScoreService#addScore() 方法,这两个类都继承于 BaseService。它们之间的类结构说明如下:

图 1. UserService 和 ScoreService

具体的代码如下所示:

清单 9 UserService.java

@Service("userService")
public class UserService extends BaseService {
    @Autowired
    private JdbcTemplate jdbcTemplate;
    @Autowired
    private ScoreService scoreService;

    public void logon(String userName) {
        updateLastLogonTime(userName);
        scoreService.addScore(userName, 20);
    }

    public void updateLastLogonTime(String userName) {
        String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
        jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
    }
}

UserService 中注入了 ScoreService 的 Bean,ScoreService 的代码如下所示:

清单 10 ScoreService.java

@Service("scoreUserService")
public class ScoreService extends BaseService{
    @Autowired
    private JdbcTemplate jdbcTemplate;
    public void addScore(String userName, int toAdd) {
        String sql = "UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?";
        jdbcTemplate.update(sql, toAdd, userName);
    }
}

通过 Spring 的事务配置为 ScoreService 及 UserService 中所有公有方法都添加事务增强,让这些方法都工作于事务环境下。下面是关键的配置代码:

清单 11 事务增强配置

<!-- 添加Spring事务增强 -->
<aop:config proxy-target-class="true">
    <aop:pointcut id="serviceJdbcMethod"
        <!-- 所有继承于BaseService类的子孙类的public方法都进行事务增强-->
        expression="within(user.nestcall.BaseService+)"/>
    <aop:advisor pointcut-ref="serviceJdbcMethod"
        advice-ref="jdbcAdvice" order="0"/>
</aop:config>
<tx:advice id="jdbcAdvice" transaction-manager="jdbcManager">
    <tx:attributes>
        <tx:method name="*"/>
    </tx:attributes>
</tx:advice>

将日志级别设置为 DEBUG,启动 Spring 容器并执行 UserService#logon() 的方法,仔细观察如下的输出日志:

清单 12 执行日志

16:25:04,765 DEBUG (AbstractPlatformTransactionManager.java:365) -
    Creating new transaction with name [user.nestcall.UserService.logon]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT  ①为UserService#logon方法启动一个事务

16:25:04,765 DEBUG (DataSourceTransactionManager.java:205) -
    Acquired Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]
for JDBC transaction

logon method...

updateLastLogonTime... ②直接执行updateLastLogonTime方法

16:25:04,781 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update

16:25:04,781 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement
    [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]

16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows

16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:470) - Participating
    in existing transaction   ③ScoreService#addScore方法加入到UserService#logon的事务中

addScore...

16:25:04,828 DEBUG (JdbcTemplate.java:785) - Executing prepared SQL update

16:25:04,828 DEBUG (JdbcTemplate.java:569) - Executing prepared SQL statement
    [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]

16:25:04,828 DEBUG (JdbcTemplate.java:794) - SQL update affected 0 rows

16:25:04,828 DEBUG (AbstractPlatformTransactionManager.java:752) -
    Initiating transaction commit

④提交事务

16:25:04,828 DEBUG (DataSourceTransactionManager.java:265) - Committing JDBC transaction
    on Connection [org.apache.commons.dbcp.PoolableConnection@32bd65]

16:25:04,828 DEBUG (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
    [org.apache.commons.dbcp.PoolableConnection@32bd65] after transaction

16:25:04,828 DEBUG (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource

从上面的输入日志中,可以清楚地看到 Spring 为 UserService#logon() 方法启动了一个新的事务,而 UserSerive#updateLastLogonTime() 和 UserService#logon() 是在相同的类中,没有观察到有事务传播行为的发生,其代码块好像“直接合并”到 UserService#logon() 中。接着,当执行到 ScoreService#addScore() 方法时,我们就观察到了发生了事务传播的行为:Participating in existing transaction,这说明 ScoreService#addScore() 添加到 UserService#logon() 的事务上下文中,两者共享同一个事务。所以最终的结果是 UserService 的 logon(), updateLastLogonTime() 以及 ScoreService 的 addScore 都工作于同一事务中。
回页首
多线程的困惑
由于 Spring 的事务管理器是通过线程相关的 ThreadLocal 来保存数据访问基础设施,再结合 IOC 和 AOP 实现高级声明式事务的功能,所以 Spring 的事务天然地和线程有着千丝万缕的联系。
我们知道 Web 容器本身就是多线程的,Web 容器为一个 Http 请求创建一个独立的线程,所以由此请求所牵涉到的 Spring 容器中的 Bean 也是运行于多线程的环境下。在绝大多数情况下,Spring 的 Bean 都是单实例的(singleton),单实例 Bean 的最大的好处是线程无关性,不存在多线程并发访问的问题,也即是线程安全的。
一个类能够以单实例的方式运行的前提是“无状态”:即一个类不能拥有状态化的成员变量。我们知道,在传统的编程中,DAO 必须执有一个 Connection,而 Connection 即是状态化的对象。所以传统的 DAO 不能做成单实例的,每次要用时都必须 new 一个新的实例。传统的 Service 由于将有状态的 DAO 作为成员变量,所以传统的 Service 本身也是有状态的。
但是在 Spring 中,DAO 和 Service 都以单实例的方式存在。Spring 是通过 ThreadLocal 将有状态的变量(如 Connection 等)本地线程化,达到另一个层面上的“线程无关”,从而实现线程安全。Spring 不遗余力地将状态化的对象无状态化,就是要达到单实例化 Bean 的目的。
由于 Spring 已经通过 ThreadLocal 的设施将 Bean 无状态化,所以 Spring 中单实例 Bean 对线程安全问题拥有了一种天生的免疫能力。不但单实例的 Service 可以成功运行于多线程环境中,Service 本身还可以自由地启动独立线程以执行其它的 Service。下面,通过一个实例对此进行描述:

清单 13 UserService.java 在事务方法中启动独立线程运行另一个事务方法

@Service("userService")
public class UserService extends BaseService {
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Autowired
    private ScoreService scoreService;
    //① 在logon方法体中启动一个独立的线程,在该独立的线程中执行ScoreService#addScore()方法
    public void logon(String userName) {
        System.out.println("logon method...");
        updateLastLogonTime(userName);
        Thread myThread = new MyThread(this.scoreService,userName,20);
        myThread.start();
    }

    public void updateLastLogonTime(String userName) {
        System.out.println("updateLastLogonTime...");
        String sql = "UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?";
        jdbcTemplate.update(sql, System.currentTimeMillis(), userName);
    }
    //② 封装ScoreService#addScore()的线程
    private class MyThread extends Thread{
        private ScoreService scoreService;
        private String userName;
        private int toAdd;
        private MyThread(ScoreService scoreService,String userName,int toAdd) {
            this.scoreService = scoreService;
            this.userName = userName;
            this.toAdd = toAdd;
        }
        public void run() {
            scoreService.addScore(userName,toAdd);
        }
    }
}

将日志级别设置为 DEBUG,执行 UserService#logon() 方法,观察以下输出的日志:

清单 14 执行日志

[main] (AbstractPlatformTransactionManager.java:365) - Creating new transaction with name
    [user.multithread.UserService.logon]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT ①

[main] (DataSourceTransactionManager.java:205) - Acquired Connection
    [org.apache.commons.dbcp.PoolableConnection@1353249] for JDBC transaction

logon method...

updateLastLogonTime...

[main] (JdbcTemplate.java:785) - Executing prepared SQL update

[main] (JdbcTemplate.java:569) - Executing prepared SQL statement
    [UPDATE t_user u SET u.last_logon_time = ? WHERE user_name =?]

[main] (JdbcTemplate.java:794) - SQL update affected 0 rows

[main] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit

[Thread-2](AbstractPlatformTransactionManager.java:365) -
    Creating new transaction with name [user.multithread.ScoreService.addScore]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT ②

[main] (DataSourceTransactionManager.java:265) - Committing JDBC transaction
    on Connection [org.apache.commons.dbcp.PoolableConnection@1353249] ③

[main] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
    [org.apache.commons.dbcp.PoolableConnection@1353249] after transaction

[main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource

[Thread-2] (DataSourceTransactionManager.java:205) - Acquired Connection
    [org.apache.commons.dbcp.PoolableConnection@10dc656] for JDBC transaction

addScore...

[main] (JdbcTemplate.java:416) - Executing SQL statement
    [DELETE FROM t_user WHERE user_name='tom']

[main] (DataSourceUtils.java:112) - Fetching JDBC Connection from DataSource

[Thread-2] (JdbcTemplate.java:785) - Executing prepared SQL update

[Thread-2] (JdbcTemplate.java:569) - Executing prepared SQL statement
    [UPDATE t_user u SET u.score = u.score + ? WHERE user_name =?]

[main] (DataSourceUtils.java:312) - Returning JDBC Connection to DataSource

[Thread-2] (JdbcTemplate.java:794) - SQL update affected 0 rows

[Thread-2] (AbstractPlatformTransactionManager.java:752) - Initiating transaction commit

[Thread-2] (DataSourceTransactionManager.java:265) - Committing JDBC transaction
    on Connection [org.apache.commons.dbcp.PoolableConnection@10dc656] ④

[Thread-2] (DataSourceTransactionManager.java:323) - Releasing JDBC Connection
    [org.apache.commons.dbcp.PoolableConnection@10dc656] after transaction

在 ① 处,在主线程(main)执行的 UserService#logon() 方法的事务启动,在 ③ 处,其对应的事务提交,而在子线程(Thread-2)执行的 ScoreService#addScore() 方法的事务在 ② 处启动,在 ④ 处对应的事务提交。
所以,我们可以得出这样的结论:在 相同线程中进行相互嵌套调用的事务方法工作于相同的事务中。如果这些相互嵌套调用的方法工作在不同的线程中,不同线程下的事务方法工作在独立的事务中。
回页首
小结
Spring 声明式事务是 Spring 最核心,最常用的功能。由于 Spring 通过 IOC 和 AOP 的功能非常透明地实现了声明式事务的功能,一般的开发者基本上无须了解 Spring 声明式事务的内部细节,仅需要懂得如何配置就可以了。
但是在实际应用开发过程中,Spring 的这种透明的高阶封装在带来便利的同时,也给我们带来了迷惑。就像通过流言传播的消息,最终听众已经不清楚事情的真相了,而这对于应用开发来说是很危险的。本系列文章通过剖析实际应用中给开发者造成迷惑的各种难点,通过分析 Spring 事务管理的内部运作机制将真相还原出来。
在本文中,我们通过剖析了解到以下的真相:
在没有事务管理的情况下,DAO 照样可以顺利进行数据操作;
将应用分成 Web,Service 及 DAO 层只是一种参考的开发模式,并非是事务管理工作的前提条件;
Spring 通过事务传播机制可以很好地应对事务方法嵌套调用的情况,开发者无须为了事务管理而刻意改变服务方法的设计;
由于单实例的对象不存在线程安全问题,所以进行事务管理增强的 Bean 可以很好地工作在多线程环境下。
在 下一篇 文章中,笔者将继续分析 Spring 事务管理的以下难点:
混合使用多种数据访问技术(如 Spring JDBC + Hibernate)的事务管理问题;
进行 Spring AOP 增强的 Bean 存在哪些特殊的情况。
分享到:
评论

相关推荐

    Java程序设计基础:调用方法-方法的嵌套调用.pptx

    在定义方法时,一个方法内不能再定义另一个方法,即不能嵌套定义,但是在调用一个方法的过程中,还可以调用另一个方法,这是方法的嵌套调用。 方法的嵌套调用 假设main方法中调用a方法,a 方法中调用b方法,具体流程...

    嵌套调用求阶乘和_C语言_嵌套调用_

    2. **递归方法(含嵌套调用)**:在这种方法中,我们使用函数自身来调用自身,直到达到基本情况(通常是n=1)。这种方法虽然直观,但可能会导致栈溢出,尤其是对于大数的阶乘。以下是一个使用嵌套调用的阶乘函数示例...

    Dll调用与嵌套调用

    Dll调用与嵌套调用 一.Win32动态链接库  1.制作的步骤:  (1)新建WIN32 Dynamic-link Library工程,工程名为MyDll,选择A simple DLL project类型。  (2)MyDll.h的内容如下: 以下是引用片段: extern "C" _...

    Oracle数据完整性嵌套事务调用分析研究

    Oracle 数据完整性嵌套事务调用分析研究 Oracle 数据库中,数据完整性是指数据的正确性、完整性和一致性。为了保护数据的完整性,我们可以使用多种方法,例如数据表的主键约束、外键约束、触发器等等。在处理数据...

    WINCE dll 嵌套调用

    WINCE 下 dll 嵌套调用 a.exe里面调用b.dll 然后再 b.dll调用c.dll 不过指针传递还未成功 谁成功了告诉下我 谢谢

    实验三: 数据库的嵌套调用

    数据库的嵌套调用,让初学者很好的学习数据库的使用和掌握一些调用方法

    C语言函数的嵌套调用和递归调用PPT课件.pptx

    C语言函数的嵌套调用和递归调用 本文主要介绍C语言函数的嵌套调用和递归调用,包括函数的递归调用、变量的作用域和存储类型等知识点。 函数的递归调用 函数的递归调用是指函数直接或间接地自我调用的一种调用方式...

    C语言函数的嵌套调用和递归调用学习教案.pptx

    C语言函数的嵌套调用和递归调用学习教案.pptx

    程序设计-函数的嵌套调用.pptx

    程序设计-函数的嵌套调用 函数的嵌套调用是程序设计中的一种重要概念。根据C语言规定,函数的定义不能嵌套,也就是说,在一个函数内不能定义其他函数,但是函数的调用可以嵌套。 函数的嵌套调用可以让程序呈现出...

    java方法调用

    ### Java方法调用详解 #### 一、方法调用概览 在Java编程语言中,方法(也称为函数)是程序的基本构建块之一,用于封装特定功能以便在需要时重复使用。方法调用是实现这一功能的关键步骤。通过调用方法,我们可以...

    springboot mybatis多数据源加事务嵌套

    springboot mybatis多数据源加事务嵌套 事务之间的调用 回滚 亲测可用 定义2个库分别建立 CREATE TABLE `user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户编号', `user_name` varchar(25) ...

    C/C++ 动态库嵌套调用源代码文件

    通过分析和运行这些示例,开发者可以更直观地了解和掌握动态库嵌套调用的实践方法。 总结来说,C/C++的动态库嵌套调用是一项强大的技术,它使得软件组件能够灵活地交互和扩展。通过正确地创建、加载和管理动态库,...

    AOP实现自我调用的事物嵌套问题

    当我们遇到"AOP实现自我调用的事物嵌套问题"时,这通常涉及到Spring框架中的事务管理,特别是自调用方法在事务处理时可能会引发的问题。 首先,让我们理解Spring AOP的事务管理是如何工作的。Spring使用代理模式来...

    C语言编程技术实践 函数嵌套调用和递归调用教学单元设计.doc

    本教学单元将深入探讨两个关键概念——函数的嵌套调用和递归调用,它们是C语言高级编程技巧的基础。 1. **函数的嵌套调用**: 函数嵌套调用是指在一个函数的执行过程中调用了另一个函数。这种调用方式使得程序结构...

    dedecms实现在标签中嵌套调用sql的方法.doc

    dedecms实现在标签中嵌套调用sql的方法.doc

    Go-Sqalx-sqlx嵌套事务

    2. 开启嵌套事务:在已经开启的事务中,你可以再次调用`Tx.Begin()`来创建一个新的子事务。子事务将继承父事务的上下文,这意味着如果父事务回滚,所有的子事务也会回滚。 3. 提交和回滚:在嵌套事务中,每个子事务...

    UG对话框多重调用和嵌套调用在模具CAD中的应用.pdf.crdownload

    UG对话框多重调用和嵌套调用在模具CAD中的应用.pdf.crdownload

    第6章 函数-5函数的嵌套调用和递归调用1

    本章主要探讨了函数的两种特殊调用方式:嵌套调用和递归调用,特别在C语言程序设计的上下文中。 1. **函数的嵌套调用**:在C语言中,虽然函数不能定义在另一个函数内部,但可以在一个函数的执行过程中调用另一个...

    六年级信息上册 第8课过程的嵌套调用教案 人教版.doc

    在小学信息技术课程中,"过程的嵌套调用"是一个重要的概念,它涉及到编程思维的培养和逻辑结构的理解。本节课主要目的是让学生理解什么是过程的嵌套调用,并能熟练运用这一技巧进行图形绘制。 1. **过程(Procedure...

Global site tag (gtag.js) - Google Analytics