整理spring事务失效的场景(源码解析)

Spring事务管理方式,我们大部分都是使用声明式来实现,即贴@Transacational注解。但是在我们使用的过程中,会因为使用不当而导致事务失效的问题。下面就罗列出事务失效的常见使用场景,并加以讲解。

场景1:spring的事务注解@Transactional只能放在public非final修饰的方法上才起作用,如果放在其他非public(private,protected)方法上,事务不起作用。

在应用系统调用声明了 @Transactional 的目标方法时,Spring 默认使用 AOP 代理,而Spring AOP实现方式有两种:jdk动态代理实现和cglib动态代理实现,但是无论使用jdk动态代理还是cglib动态代理,@Transactional也只能放在public或public final修饰的方法上才起作用。
下面是从源码的角度来看其中的原因。

jdk动态代理实现

目前,我们大部分业务代码都是写在Service层,即创建一个Service接口,然后创建ServiceImpl类,且该类实现Service接口,再在实现类的对应实现方法上写相关业务逻辑。
这种方式,如果我们在ServiceImpl类或方法上贴上@Transactional 注解,实际上底层是使用了jdk动态代理,会动态的生成一个Service代理对象。

类似这样:

/**
 * @author: Longer
 */
public interface PersonService {
    
    void eat();
}

/**
 * @author: Longer
 */
@Service
public class PersonServiceImpl implements PersonService {

    @Transactional
    @Override
    public void eat() {

    }
}

源码解析
newProxyInstance类是jdk动态生成代理对象的时候需要调用的类。

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        Objects.requireNonNull(h);

        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * 首先从缓存查找是否有代理类,没有就生成一个
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * 通过InvocationHandler调用目标类的构造函数
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }

            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            //如果构造函数不是public修饰,修改
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    }

其中查找Proxy类的源码如下:

 private static Class<?> getProxyClass0(ClassLoader loader,
                                           Class<?>... interfaces) {
        //长度检查
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }
         
        //调用了下面的WeakCache<K, P, V>.get(K key, P parameter)方法,loader作为key,interfaces作为parameter参数
        //定义如下:proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory())
        return proxyClassCache.get(loader, interfaces);
    }


   //首先当前key(也就是上面的ClassLoader)已经加载存在,就直接从缓存中返回
   //如果不存在,就会通过ProxyClassFactory来创建代理对象
   public V get(K key, P parameter) {
        Objects.requireNonNull(parameter);

        expungeStaleEntries();
         //根据key的hash值和一个ReferenceQueue来构造
        Object cacheKey = CacheKey.valueOf(key, refQueue);

        // 从map中取出cacheKey的值
        ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
        if (valuesMap == null) {
            ConcurrentMap<Object, Supplier<V>> oldValuesMap
                = map.putIfAbsent(cacheKey,
                                  valuesMap = new ConcurrentHashMap<>());
            if (oldValuesMap != null) {
                valuesMap = oldValuesMap;
            }
        }

        Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
        Supplier<V> supplier = valuesMap.get(subKey);
        Factory factory = null;

        while (true) {
            if (supplier != null) {
                // supplier可能是Factory或者CacheValue<V>
                V value = supplier.get();
                if (value != null) {
                    return value;
                }
            }
            // 缓存中没有supplier,同时supplier中没有
            // 懒加载的方式创建一个Factory
            if (factory == null) {
                factory = new Factory(key, parameter, subKey, valuesMap);
            }

            if (supplier == null) {
                supplier = valuesMap.putIfAbsent(subKey, factory);
                if (supplier == null) {
                    // 安装 Factory
                    supplier = factory;
                }
            } else {
                if (valuesMap.replace(subKey, supplier, factory)) {
                    supplier = factory;
                } else {
                    supplier = valuesMap.get(subKey);
                }
            }
        }
    }

再看上面提到的ProxyClassFactory类

//类定义
 private static final class ProxyClassFactory
        implements BiFunction<ClassLoader, Class<?>[], Class<?>>{

        @Override
        public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

            Map<Class<?>, Boolean> interfaceSet = new IdentityHashMap<>(interfaces.length);
            for (Class<?> intf : interfaces) {
                /*
                 * 校验当前类加载器ClassLoader解析到的名称和定义的名称是否相同 
                 */
                Class<?> interfaceClass = null;
                try {
                    interfaceClass = Class.forName(intf.getName(), false, loader);
                } catch (ClassNotFoundException e) {
                }
                if (interfaceClass != intf) {
                    throw new IllegalArgumentException(
                        intf + " is not visible from class loader");
                }
                /*
                 * 校验是否是接口类型,这也就是为什么JDK动态代理只能基于接口
                 */
                if (!interfaceClass.isInterface()) {
                    throw new IllegalArgumentException(
                        interfaceClass.getName() + " is not an interface");
                }
                /*
                 * 防重
                 */
                if (interfaceSet.put(interfaceClass, Boolean.TRUE) != null) {
                    throw new IllegalArgumentException(
                        "repeated interface: " + interfaceClass.getName());
                }
            }

           // 代理对象的目录
            String proxyPkg = null;     
            int accessFlags = Modifier.PUBLIC | Modifier.FINAL;

            .....

            /*
             * 生成指定Proxy代理对象的字节码
             */
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
            try {
                //调用的native方法
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            } catch (ClassFormatError e) {
                /*
                 * 生成的代理类有bug
                 */
                throw new IllegalArgumentException(e.toString());
            }
        }

}

从ProxyClassFactory类,我们可以看到,该类是会去校验目标类是否是接口类型。这也就是为什么jdk动态代理只能基于接口实现。

JDK动态代理生成的代理class类名实际上是这样的:public final class $Proxy0 extends Proxy implements PersonService{}(PersonService是被代理接口)
继承了Proxy 和实现了目标接口类PersonService。因为Java不允许多重继承,这就限制了:使用JDK代理不能是普通类或者抽象类,只能是接口类型。
由于接口定义的方法是public的,java要求实现类所实现接口的方法必须是public的(不能是protected,private等),同时不能使用static的修饰符。所以,可以实施接口动态代理的方法只能是使用“public”或“public final”修饰符的方法,其它方法不可能被动态代理,相应的也就不能实施AOP增强,也即不能进行Spring事务增强

cglib动态代理实现

对于普通@Service注解的类(未实现接口)并通过 @Autowired直接注入类的方式,是通过cglib动态代理实现的。

类似这样:

/**
 * @author: Longer
 */
@Service
public class PersonServiceImpl{

    @Transactional
    public void eat() {

    }
}
public class PersonController{
    @Autowired
    private PersonServiceImpl personService;
    @PostMapping(value = "/eat")
    public Result eat() {
      personService.eat();
      return Result.ok();
    }
}

cglib动态代理生成的代理class类名实际上是这样的:

public class Student$$EnhancerByCGLIB$$92f3e3f6 extends Student implements Factory(){}

Student 是被代理类。可以看到代理类是继承了被代理类Student ,这就是与jdk动态代理的区别。jdk代理类是实现被代理接口。
由于cglib是继承代理类,而Java继承由于使用final,static,private修饰符的方法都不能被子类复写,所以这些方法将不能被实施的AOP增强,即不会生成cglib代理对象。所以事务是不生效的。
结论:
cglib字节码动态代理的方案是通过扩展被增强类,动态创建子类的方式进行AOP增强植入的,由于使用final,static,private修饰符的方法都不能被子类复写,所以这些方法将不能被实施的AOP增强。即除了public的非final的实例方法,其他方法均无效。

场景2:方法自调用

目标类直接调用该类的其他标注了@Transactional 的方法(相当于调用了this.对象方法),事务不会起作用。事务不起作用其根本原因就是未通过代理调用,因为事务是在代理中处理的,没通过代理,也就不会有事务的处理。
类似下面的写法,事务是不生效:

/**
 * @author: Longer
 */
@RestController
@RequestMapping("/person")
public class PersonController {
    private PersonServiceImpl personServiceImpl;
    @GetMapping(value = "/eat")
    public Result<?> eat() {
        personServiceImpl.eat();
        return Result.ok();
    }
}

/**
 * @author: Longer
 */
@Service
public class PersonServiceImpl{

    public void eat() {
        run();
    }
    @Transactional
    public void run(){

    }
}

场景3:事务方法抛出非RuntimeException异常,事务不回滚

原因:Spring 默认只为 RuntimeException 异常回滚事务,如果方法往外抛出 checked exception,该方法虽然不会再执行后续操作,但仍会提交已执行的数据操作。这样可能使得只有部分数据提交,造成数据不一致。
比如下面的代码,不会回滚:

 @Transactional
    public void eat() throws IOException {
        Person person = new Person();
        person .setId(1);
        this.save(person);
        throw new IOException("testCheckedTran");
    }

代码不回滚的原因是在插入数据库之后,抛出了IOException,这个异常不属于RuntimeException ,所以不会回滚。

解决办法:自定义回滚策略,可使用@Transactional 的 noRollbackFor,noRollbackForClassName,rollbackFor,rollbackForClassName 属性。如使用:@Transactional(rollbackFor = Exception.class)或@Transactional(rollbackFor = Throwable.class)

场景4:方法部分代码try...catch住,而catch语句块没有往外跑出回滚异常,事务也不会回滚。

在业务代码上,一般不try...catch住,如果要捕获异常的话,需要在catch里跑出回滚异常,否则事务不会回滚。

场景5:数据库存储引擎不支持事务

如:使用mysql作为数据库的话,如果存储引擎不是INNODB,而是MyISAM的话,则事务不生效。

场景6:如果采用spring+spring mvc,则context:component-scan重复扫描问题可能会引起事务失败

如果spring和mvc的配置文件中都扫描了service层,那么事务就会失效。
原因:因为按照spring配置文件的加载顺序来讲,先加载springmvc配置文件,再加载spring配置文件,我们的事物一般都在srping配置文件中进行配置,如果此时在加载srpingMVC配置文件的时候,把servlce也给注册了,但是此时事物还没加载,也就导致后面的事物无法成功注入到service中。所以把对service的扫描放在spring配置文件中或是其他配置文件中。

场景7: 多个事务管理器

当一个应用存在多个事务管理器时,如果不指定事务管理器,@Transactional 会按照事务管理器在配置文件中的初始化顺序使用其中一个。
如果存在多个数据源 datasource1 和 datasource2,假设默认使用 datasource1 的事务管理器,当对 datasource2 进行数据操作时就处于非事务环境。
解决办法是,可以通过@Transactional 的 value 属性指定一个事务管理器。在使用多个事务管理器的情况下,事务不生效的原因在本系列后续文章中会有分析

参考文献链接:
Spring事务Transactional和动态代理-事务失效的场景
jdk动态代理
cglib动态代理

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