Spring基础篇(1)-事务

JAVA && Spring && SpringBoot2.x — 学习目录

1. 编程式事务和声明式事务

spring支持编程式事务管理声明式事务管理两种方式。

  • 编程式事务Spring推荐使用TransactionTemplate。需要在业务代码中掺杂事务管理的代码,粒度可以作用到代码块级别。

  • 声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务可以在配置文件中做相关的事务规则声明(或基于@Transactional注解的方式),不需要侵入业务代码。但是声明式事务粒度只能做到方法级别。

Spring基于XML的声明式事务配置:

  1. 配置事务管理器,将数据源交由事务管理器。
<bean id="transactionManager" class="...DataSourceTransactionManager">
     <property name="dataSource" ref="dataSource"/>
</bean>
  1. 配置事务通知,根据方法名,指定事务的属性。
<tx:advice id="txAdvice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="get*" read-only="true" propagation='"SUPPORTS">
        <tx:method name="purchase" protagation="REQUIRED">
    </tx:attributes>
</tx:advice>
  1. 配置aop,将事务和切点关联起来。
<aop:config>
    <aop:pointcut expresssion="execution(* com.yxr.*(...))" id="txPointcut">
    <aop:advisor advice-ref="txAdvice" pointcut-ref="txPointcut"/>
</aop:config>

Spring事务管理,本质上就是AOP编程的实现,将数据源配置到数据管理器之后。在配置事务通知,规定哪些方法需要事务。最后将事务通知和切点整合起来。

Spring基于注解的声明式事务配置:

  1. 配置事务管理器,将数据源交由事务管理器。
<bean id="transactionManager" class="...DataSourceTransactionManager">
     <property name="dataSource" ref="dataSource"/>
</bean>
  1. 启动事务注解
<tx:annotation-driven transaction-manager="transactionManager">
  1. 可以在对应方法上添加Transaction注解
@Transaction
public void purchase(){}

SpringBoot如何使用事务:

Springboot默认集成事物,只要在方法上加上@Transactional即可

2. Spring事务的使用和注意事项

说起来Spring事务,我们需要先读一下系统之间那点事-异常知多少,复习下Java异常的东西。

注意事项:

  1. @Transaction可以用于接口以及接口方法上,类以及类方法上。当作用于类上时,该类的所有public方法都具有该类型的事务属性。同时可以使用在方法级别使用注解来覆盖类级别的定义。
  2. Spring不建议@Transaction注解作用于接口以及接口方法上,因为这只有在使用基于接口的代理事务情况下才会生效。另外,@Transaction注解只能被应用到public方法上,这是由于Spring AOP本质上决定的,在其他访问修饰符的方法使用@Transaction注解,事务不会起作用。
  3. 只有【外部方法】调用【事务方法】时才会被AOP代理捕获,也就是说,类内部方法调用本类事务方法并不会引起事务行为。(内部方法使用this调用时:this为目标对象)。
  4. Spring默认情况下会对运行期异常(非检查异常RuntimeException)或者ERROR进行回滚。可以使用rollbackFor=Exception.class对检查时异常进行回滚。
  5. MySQL事务库表引擎应为InnoDB,否则不支持事务。

3. Spring事务的传播行为

正如行为的英文含义,很好的解释各种行为的含义:

事务的传播(propagation [ˌprɒpə'ɡeɪʃn])行为是指:如果在开始当前事务之前,一个事务上下文已经存在,此时我们可以有多个选项指定事务性方法的执行行为。

  1. PROPAGATION_REQUIRED:[adj 必须的]默认传播行为,指的是若当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务。
  2. PROPAGATION_REQUIRES_NEW:[v 需要新的]需要创建一个新的,若当前有事务,则将当前事务挂起。
  3. PROPAGATION_SUPPORTS:[v 支持]当前存在事务,就在事务中运行;当前不存在事务,则不在事务中运行。
  4. PROPAGATION_NOT_SUPPORTED[v 不被支持]不运行在事务中,当前有事务,则挂掉当前事务。
  5. PROPAGATION_NEVER:[adv 绝不]不运行在事务中,如果当前有事务,则抛出异常。
  6. PROPAGATION_MANDARORY[[ˈmændətəri]强制的]`必须运行在事务中,如果当前方法没有事务,则抛出异常。
  7. PROPAGATION_NESTED[[nestɪd] 嵌套的]当前存在事务,则创建一个事务作为当前事务的嵌套事务运行,如果当前没有事务,则创建一个新的事务。

4. Spring事务隔离级别

隔离级别是指若干个并发事务之间的隔离(ISOLATION [ˌaɪsəˈleɪʃn])程度。

  1. READ_UNCOMMITTED:读未提交,一个事务可以读取到另一个事务未提交的数据。(脏读,不可重复读,幻读)。
  2. READ_COMMITTED:读已提交,一个事务只能读取另一个事务已经提交的数据。(不可重复读,幻读)。
  3. REPEATABLE_READ:可重复读[rɪˈpi:təbl]一个事务在整个过程中多次重复执行某个查询,每次返回的结果都相同。(幻读)
  4. SERIALIZABLE:序列化[sɪərɪəlaɪ'zəbl]所有事务依次逐个执行,这样事务之间不可能存在干扰。

5. Spring事务回滚规则

5.1 Exception异常回滚事务

指示spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文抛出异常。Spring事务管理器会捕获任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。

默认配置下,spring只有在抛出运行时异常(RuntimeException及其子类)或者Error异常时才会回滚,但是可以配置rollbackFor=Exception.class检查时异常进行回滚。

    @Transactional(rollbackFor = Exception.class)
    @Override
    public int addEmployee(Employee record) {
        int delResult = employeeMapper.deleteByPrimaryKey(11);
        int addResult = employeeMapper.insert(record);
        if (delResult == 0 || addResult == 0) {
            throw new RuntimeException("123");
        }
        return addResult;
    }

5.2 手动回滚事务

Spring事务原理就是AOP,即当Spring捕获住RuntimeException异常后,自动执行【回滚】操作。但是若不想抛出异常(捕获处理异常),但依旧想回滚事务,该如何处理?

我们可以使用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();手动进行回滚,这样上层便无需处理异常。

 @Transactional
    @Override
    public int addEmployee(Employee record) {
        int delResult = employeeMapper.deleteByPrimaryKey(11);
        int addResult = employeeMapper.insert(record);
        if (delResult == 0 || addResult == 0) {
            //手动回滚,上层无需要处理异常。
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }
        return addResult;
    }

5.3 套嵌事务的回滚

spring 事务疑惑

情况大概是这样的:

  1. 【事务嵌套调用】事务A方法调用了事务B方法;
  2. 【事务B出现异常】事务A捕获事务B的异常(其实事务A不想回滚);
  3. 【事务A回滚操作】最终事务A还是回滚了;
public void testA() throws BussinessException {  
    try {  
        bService.testB();  
    } catch (BussinessException e) {  
        System.out.println(e.getErrorCode());  
    }  
    pictureService.addPicture("", "", "a.jpg", "", new File("d:/1.jpg"));  
}  
  <bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">  
    <property name="sessionFactory" ref="sessionFactory" />  
</bean>  
  
<tx:advice id="txAdvice" transaction-manager="txManager">  
    <tx:attributes>  
        <tx:method name="*" rollback-for="com.soft.core.BussinessException" />  
    </tx:attributes>  
</tx:advice>  
  
<aop:config>  
    <aop:pointcut id="all" expression="execution(* com.soft.*.*.*ServiceImpl.*(..))"/>  
    <aop:advisor advice-ref="txAdvice" pointcut-ref="all"/>  
</aop:config>  

回答:
事务B方法的Transaction配置propagation属性是使用的默认值(required)。这样的话,本质上事务A和事务B共用了一个Transaction。transactionManager中有一个参数:globalRollbackOnParticipationFailure(参与者失败导致全局回滚)

f (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) {  
            if (defStatus.isDebug()) {  
                logger.debug("Global transaction is marked as rollback-only but transactional code requested commit");  
            }  
            processRollback(defStatus);  
            // Throw UnexpectedRollbackException only at outermost transaction boundary  
            // or if explicitly asked to.  
            if (status.isNewTransaction() || isFailEarlyOnGlobalRollbackOnly()) {  
                throw new UnexpectedRollbackException(  
                        "Transaction rolled back because it has been marked as rollback-only");  
            }  
            return;  
        }  

解决办法:

  • 【方案一】如果想要事务B失败不影响事务A,可以将事务B的传播行为设置为propagation=Propagation.REQUIRES_NEW
  • 【方案二】将globalRollbackOnParticipationFailure参数设置为false。
  <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
    <property name="globalRollbackOnParticipationFailure" value="false"/>
  </bean>

5.4 内部方法调用事务方法

上面说到,【外部方法】调用【事务方法】事务才会生效;【内部方法】调用【本类的事务方法】,事务不会起作用。

service层代码

@Service
public class TestTransactionService {

    @Resource
    private UsertMapper usertMapper;

    //业务逻辑方法
    public void businessUsert(Usert usert) {
        insertUsert(usert);
        // 调用远程接口
    }

    @Transactional
    public void insertUsert(Usert usert) {
        usertMapper.insert(usert);
        throw new RuntimeException("抛出异常");
    }
}

Controller层代码

@ResponseBody
    @RequestMapping(value = "user", method = RequestMethod.GET)
    public String showUserName() {
        Usert usert = new Usert();
        usert.setId(4);
        usert.setAge(20);
        usert.setUserName("李吉吉");
        usert.setPassword("123");
        testTransactionService.businessUsert(usert);
        return "success";
    }

执行结果

16:21:31.251 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
16:21:31.264 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b9581da] was not registered for synchronization because synchronization is not active
16:21:31.354 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@429902b9] will not be managed by Spring
16:21:31.357 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ooo Using Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@429902b9]
16:21:31.365 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ==>  Preparing: insert into user_t (id, user_name, password, age) values (?, ?, ?, ?) 
16:21:31.552 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ==> Parameters: 4(Integer), 李吉吉(String), 123(String), 20(Integer)
16:21:31.593 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5b9581da]
16:21:31.597 [http-bio-8081-exec-1] INFO com.springmvc.common.GlobalExceptionHandler - 全局捕获异常日志打印...
================全局捕获异常=================
java.lang.RuntimeException: 抛出异常
    at com.springmvc.service.TestTransactionService.insertUsert(TestTransactionService.java:34)
    at com.springmvc.service.TestTransactionService.businessUsert(TestTransactionService.java:28)
    at com.springmvc.service.TestTransactionService$$FastClassBySpringCGLIB$$75b3ded0.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:640)
    at com.springmvc.service.TestTransactionService$$EnhancerBySpringCGLIB$$1b4acae.businessUsert(<generated>)
    at com.springmvc.web.UserController.showUserName(UserController.java:52)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)

我们可以看到,事务方法没有生效(即没有AOP增强,也没有Rollback操作)。因为我们本类方法调用事务方法。

实际开发中,我们的【业务方法】中可能存在调用远程接口等耗时操作,需要将【事务】抽取出来,但【业务方法】调用【事务方法】事务不起作用。那么如何解决?

解决方法:

@Service
public class TestTransactionService {

    @Resource
    private UsertMapper usertMapper;

    /**
     * 内部方法调用本类事务方法的关键,重新进行AOP增强
     */
    @Resource
    TestTransactionService testTransactionService;

    //业务逻辑方法
    public void businessUsert(Usert usert) {
        testTransactionService.insertUsert(usert);
    }

    @Transactional
    public void insertUsert(Usert usert) {
        usertMapper.insert(usert);
        throw new RuntimeException("抛出异常");
    }
}

日志打印

16:39:00.643 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Creating a new SqlSession
16:39:00.643 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@508ee7ba]
16:39:00.644 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.transaction.SpringManagedTransaction - JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@4be855b6] will be managed by Spring
16:39:00.644 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ooo Using Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@4be855b6]
16:39:00.644 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ==>  Preparing: insert into user_t (id, user_name, password, age) values (?, ?, ?, ?) 
16:39:00.644 [http-bio-8081-exec-1] DEBUG com.springmvc.generic.mybatis.mapper.UsertMapper.insert - ==> Parameters: 4(Integer), 李吉吉(String), 123(String), 20(Integer)
16:39:00.646 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@508ee7ba]
16:39:00.656 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization rolling back SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@508ee7ba]
16:39:00.656 [http-bio-8081-exec-1] DEBUG org.mybatis.spring.SqlSessionUtils - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@508ee7ba]
16:39:00.658 [http-bio-8081-exec-1] INFO com.springmvc.common.GlobalExceptionHandler - 全局捕获异常日志打印...
================全局捕获异常=================
java.lang.RuntimeException: 抛出异常
    at com.springmvc.service.TestTransactionService.insertUsert(TestTransactionService.java:37)
    at com.springmvc.service.TestTransactionService$$FastClassBySpringCGLIB$$75b3ded0.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:708)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:98)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:262)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:95)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:644)
    at com.springmvc.service.TestTransactionService$$EnhancerBySpringCGLIB$$d0b071e3.insertUsert(<generated>)
    at com.springmvc.service.TestTransactionService.businessUsert(TestTransactionService.java:31)

在service类中,将【本类对象】引入【本类】中,可以看到内部方法调用事务方法时,事务方法进行了AOP增强,即出现异常时回滚。

小伙伴会问,若是使用@Autowired注解会出现什么情况?

当使用@Autowired注解时,用来注入已有的bean。但是有些时候,会注入失败。原因就是@Autowired默认就是@Autowired(required=true),表示注入的时候,该bean必须存在,否则就会注入失败。

org.springframework.beans.factory.BeanCreationException: Error creating 
bean with name 'testTransactionService': Injection of autowired dependencies 
failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire
 field: com.springmvc.service.TestTransactionService 
com.springmvc.service.TestTransactionService.testTransactionService; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException:
 No qualifying bean of type [com.springmvc.service.TestTransactionService] 
found for dependency: expected at least 1 bean which qualifies as autowire 
candidate for this dependency. Dependency annotations: 
{@org.springframework.beans.factory.annotation.Autowired(required=true)}

使用@Autowired注入属性之后,会但是在bean容器中并没有该类型的对象,于是在项目启动的时候出现上图的错误,但若是我们设置@Autowired(required=false)后,本质上,该对象并未注入进去,会出现空指针异常。

故:我们要使用@Resource属性。

有些小伙伴会问,会不会出现循环依赖呢?

spring是将Bean对象实例化(依赖无参构造函数),在设置对象属性的值。避免setter和field的循环依赖。

循环依赖其实就是循环引用,也就是两个或者两个以上的bean互相持有对象,最终形成闭环。

解决Spring循环依赖的依据其实是基于Java的引用传递,当我们获取对象引用时,对象的field是可以延后设置的。【但构造器必须是在获取引用之前】。

Spring单例对象初始化过程
  1. createBeanInstance:实例化,其实就是调用对象的构造方法实例化对象。
  2. populateBean:填充对象,这一步主要是多bean的依赖属性进行填充。
  3. initializeBean:调用spring xml的init方法。

我们要解决循环引用也应该是从初始化过程着手,对于单例来说,在spring容器整个生命周期内,只有一个对象。保存在Cache中。spring为解决单例的循环依赖问题,使用了三级缓存。

在createBeanInstance之后,其实单例对象已经被创建出来(调用了构造器),虽然不够完美(未进行初始化的第二步和第三步),但是已经能够被认出来(根据对象引用能定位到堆中的对象)。


Spring-bean的循环依赖以及解决方式

项目github地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335