Spring 下默认事务机制中@Transactional 无效的原因

Posted 衣舞晨风

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring 下默认事务机制中@Transactional 无效的原因相关的知识,希望对你有一定的参考价值。

1、Spring中 @Transactional 注解的限制

1、 同一个类中, 一个nan-transactional的方法去调用transactional的方法, 事务会失效

If you use (default) Spring Proxy AOP, then all AOP functionality provided by Spring (like @Transational) will only be taken into account if the call goes through the proxy. – This is normally the case if the annotated method is invoked from another bean.

2、在private方法上标注transactional, 事务无效

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

这里有一个详细的说明 http://stackoverflow.com/questions/4396284/does-spring-transactional-attribute-work-on-a-private-method

2、测试代码

TestCase01.java

package com.rockbb.test.api.service;


public interface TestCase01 
    void init();
    void clean();
    void txnInLocalPrivate();
    void txnInLocalPublic();
    void txnInOpenPublic();
    void txnInOpenPublicByInvokePrivate();
    void txnInOpenPublicByInvokePublic();

TestCase01Impl.java

package com.rockbb.test.impl.service.impl;

import com.rockbb.test.api.dto.AccountDTO;
import com.rockbb.test.api.dto.AccountDetailDTO;
import com.rockbb.test.api.service.TestCase01;
import com.rockbb.test.impl.mapper.AccountDetailMapper;
import com.rockbb.test.impl.mapper.AccountMapper;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;

@Repository("testCase01")
public class TestCase01Impl implements TestCase01 
    @Resource(name="accountMapper")
    private AccountMapper accountMapper;
    @Resource(name="accountDetailMapper")
    private AccountDetailMapper accountDetailMapper;

    @Resource(name="testCase01")
    private TestCase01 testCase01;

    /**
     * 无效, 未回退
     *
     * 结论: 在私有方法上加事务注解无效
     */
    @Override
    public void txnInLocalPrivate() 
        localPrivate();
    

    /**
     * 无效, 未回退
     *
     * 结论: 在公有方法上事务注解, 再通过接口方法调用, 无效
     */
    @Override
    public void txnInLocalPublic() 
        localPublic();
    

    /**
     * 有效, 无论下面调用的是否是私有方法
     *
     * 结论: 在接口方法上加事务, 无论下面调用的是否是私有方法, 都有效
     */
    @Override
    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    public void txnInOpenPublic() 
        localPrivate();
    

    @Override
    public void txnInOpenPublicByInvokePrivate() 

    

    /**
     *
     * 结论: 普通接口方法直接调用同类带事务的方法, 无效. 通过接口调用则有效
     */
    @Override
    public void txnInOpenPublicByInvokePublic() 
        //txnInOpenPublic(); // 无效
        testCase01.txnInOpenPublic(); // 有效
    

    @Override
    public void init() 
        accountMapper.truncate();
        for (int i = 0; i < 10; i++) 
            BigDecimal amount = BigDecimal.valueOf(i * 10);
            add(String.valueOf(i), BigDecimal.ZERO);
            increase(String.valueOf(i), BigDecimal.valueOf(100 + i));
        
    

    @Override
    public void clean() 
        accountMapper.truncate();
    

    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    private void localPrivate() 
        AccountDTO dto = new AccountDTO().initialize();
        dto.setId("test");
        dto.setAmount(BigDecimal.ZERO);
        accountMapper.insert(dto);
        throw new RuntimeException("localPrivate");
    

    @Transactional(propagation = Propagation.REQUIRED, readOnly = false)
    public void localPublic() 
        AccountDTO dto = new AccountDTO().initialize();
        dto.setId("test");
        dto.setAmount(BigDecimal.ZERO);
        accountMapper.insert(dto);
        throw new RuntimeException("localPublic");
    

    public void openPublic() 
        AccountDTO dto = new AccountDTO().initialize();
        dto.setId("test");
        dto.setAmount(BigDecimal.ZERO);
        accountMapper.insert(dto);
        throw new RuntimeException("openPublic");
    

    private int add(String id, BigDecimal amount) 
        AccountDTO dto = new AccountDTO().initialize();
        dto.setId(id);
        dto.setAmount(amount);
        return accountMapper.insert(dto);
    

    private int increase(String id, BigDecimal amount) 
        AccountDTO dto = accountMapper.select(id);
        AccountDetailDTO detail = new AccountDetailDTO().initialize();
        detail.setAmount(amount);
        detail.setPreAmount(dto.getAmount());
        dto.setAmount(dto.getAmount().add(amount));
        detail.setPostAmount(dto.getAmount());
        if (accountMapper.update(dto) != 1) 
            throw new RuntimeException();
        
        return accountDetailMapper.insert(detail);
    

TestCase02.java

package com.rockbb.test.api.service;

public interface TestCase02 
    void txnInOpenPublicByPublic();
    void txnInOpenPublicByPrivate();

TestCase02Impl.java

package com.rockbb.test.impl.service.impl;

import com.rockbb.test.api.service.TestCase01;
import com.rockbb.test.api.service.TestCase02;
import org.springframework.stereotype.Repository;

import javax.annotation.Resource;

@Repository("testCase02")
public class TestCase02Impl implements TestCase02 
    @Resource(name="testCase01")
    private TestCase01 testCase01;

    /**
     * 有效
     *
     * 结论: 在接口方法上加事务, 再被他类的接口方法调用, 无论此接口方法是否加事务, 都有效
     */
    @Override
    public void txnInOpenPublicByPublic() 
        testCase01.txnInOpenPublic();
    

    /**
     * 有效
     *
     * 结论: 在接口方法上加事务, 再被他类的私有方法调用后, 依然有效
     */
    @Override
    public void txnInOpenPublicByPrivate() 
        localPrivate();
    

    private void localPrivate() 
        testCase01.txnInOpenPublic();
    

测试样例 AccountCheckTest.java

package com.rockbb.test.test;

import com.rockbb.test.api.service.TestCase01;
import com.rockbb.test.api.service.TestCase02;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/spring/spring-commons.xml")
public class AccountCheckTest 
    private static final Logger logger = LoggerFactory.getLogger(AccountCheckTest.class);
    @Resource(name="testCase01")
    private TestCase01 testCase01;
    @Resource(name="testCase02")
    private TestCase02 testCase02;

    @Test
    public void test01() 
        testCase01.init();
        try 
            testCase01.txnInLocalPrivate();
         catch (Exception e) 
            logger.error(e.getMessage(), e);
        
    

    @Test
    public void test02() 
        testCase01.init();
        try 
            testCase01.txnInLocalPublic();
         catch (Exception e) 
            logger.error(e.getMessage(), e);
        
    

    @Test
    public void test03() 
        testCase01.init();
        try 
            testCase01.txnInOpenPublic();
         catch (Exception e) 
            logger.error(e.getMessage(), e);
        
    

    @Test
    public void test04() 
        testCase01.init();
        try 
            testCase02.txnInOpenPublicByPublic();
         catch (Exception e) 
            logger.error(e.getMessage(), e);
        
    

    @Test
    public void test05() 
        testCase01.init();
        try 
            testCase02.txnInOpenPublicByPrivate();
         catch (Exception e) 
            logger.error(e.getMessage(), e);
        
    

    @Test
    public void test06() 
        testCase01.init();
        try 
            testCase01.txnInOpenPublicByInvokePublic();
         catch (Exception e) 
            logger.error(e.getMessage(), e);
        
    

3、测试用例情况总结

  • @Transactional 加于private方法, 无效
  • @Transactional 加于未加入接口的public方法, 再通过普通接口方法调用, 无效
  • @Transactional 加于未加入接口的public方法, 再通过它类调用, 有效
  • @Transactional 加于接口方法, 无论下面调用的是private或public方法, 都有效
  • @Transactional 加于接口方法后, 被本类普通接口方法直接调用, 无效
  • @Transactional 加于接口方法后, 被本类普通接口方法通过接口调用, 有效
  • @Transactional 加于接口方法后, 被它类的接口方法调用, 有效
  • @Transactional 加于接口方法后, 被它类的私有方法调用后, 有效

总结: Transactional是否生效, 仅取决于是否加载于接口方法, 并且是否通过接口方法调用(而不是本类调用)

4、@Transactional(readOnly = true) 的含义

执行单条查询语句时, 数据库默认支持SQL执行期间的读一致性. 而执行多条查询语句(例如统计查询, 报表查询)时, 多条查询SQL必须保证整体的读一致性, 否则在每条查询之间数据可能发生变动, 导致最终的查询结果出现数据不一致的错误,此时应该启用事务支持.

read-only="true"表示该事务为只读事务, 上面的多条查询可以使用只读事务. 由于只读事务不存在对数据的修改, 因此数据库将会为只读事务提供一些优化手段, 例如Oracle对于只读事务不启动回滚段, 不记录回滚log.

在JDBC中指定只读事务的办法为 connection.setReadOnly(true), Hibernate中指定只读事务的办法为 session.setFlushMode(FlushMode.NEVER). 在Spring的Hibernate封装中,指定只读事务的办法为bean配置文件中 prop属性增加“read-Only”. 或者用注解方式@Transactional(readOnly=true)

在将事务设置成只读后, 相当于将数据库设置成只读数据库, 此时若要进行写的操作会报错.

5、个人思考

Spring AOP defaults to using standard JDK dynamic proxies for AOP proxies. This enables any interface (or set of interfaces) to be proxied.

Spring AOP can also use CGLIB proxies. This is necessary to proxy classes rather than interfaces. By default, CGLIB is used if a business object does not implement an interface. As it is good practice to program to interfaces rather than classes, business classes normally implement one or more business interfaces. It is possible to force the use of CGLIB, in those (hopefully rare) cases where you need to advise a method that is not declared on an interface or where you need to pass a proxied object to a method as a concrete type.

摘自:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#aop

从Spring官方文档中可以看出, Spring会灵活使用JDK dynamic proxies或者CGLIB proxies。

比如(Spring默认情况AOP Proxies,而非强制启用CGLIB):

// 实例A
@Autowired
AccountServiceImpl accountServiceImpl;
// 实例B
@Autowired
AccountService accountService;

其中AccountServiceImpl类实现了AccountService接口

对于实例A注入用的是基于JDK的代理,因为这里注入的是接口;
实例B注入用的就是CGLIB的代理,因为这里注入的实例。

虽然代理方式不同,但只要是被代理方法上的@Transactional标识,都会生效。

  • JDK代理的接口
  • CGLIB代理的pulic方法

原文地址:https://www.cnblogs.com/milton/p/6046699.html

个人微信公众号:

作者:jiankunking 出处:http://blog.csdn.net/jiankunking

以上是关于Spring 下默认事务机制中@Transactional 无效的原因的主要内容,如果未能解决你的问题,请参考以下文章

面试中经常要问到的, Spring事务的传递性是如何区分的呢?

Spring事务的机制

图解MVCC机制

spring的@Transactional注解

Spring常用事务传播特性

Spring常用事务传播特性