zoukankan      html  css  js  c++  java
  • @Transactional 事务的底层原理

    最近同事发现一个业务状态部分更新的bug,这个bug会导致两张表的数据一致性问题。花了些时间去查问题的原因,现在总结下里面遇到的知识点原理。

    问题一:事务没生效

    我们先看一段实例代码,来说明下问题:

    @Service
    public class PaymentServiceImpl implements PaymentService {
        public void fetchLatestStatus(String trxId) {
          //1. do RPC request and get the payment status
          StatusResponse response = doRPC(trxId);
          //2. save request data
          saveRequest(response);
        }
        
        @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
        public void updatePayment(StatusResponse response) {
          Payment pay = payRepository.findByTrxId(response.getTrxId);
          //do something to update payment record by response and persist
          pay.setStatus(success);
          payRepository.save(pay);
        }
    }
    

    在上面代理里,updatePayment方法的@Transactional注解会失效,并没有新开一个事务去保存Payment对象。

    开发中少不了用到事务注解@Transactional来管理事务,@Transactional注解底层是基于Spring AOP来进行实现的。

    我们来看两个典型的AOP应用场景:

    • 统一的验证用户逻辑

    AOP场景一

    • 反复使用的开启事务,关闭事务逻辑

    AOP场景二

    原理分析

    我们先复习下Spring AOP动态代理的原理。
    AOP是一种通用的编程思想,Java里有2种实现方式:

    • Spring AOP,基于动态代理实现
      • JDK代理
      • Cglib代理
    • AspectJ,基于编译期实现

    Spring AOP

    1. Spring实现AOP的方法则就是利用了动态代理机制实现的;
    2. 在应用系统调用声明@Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象ProxyObject,如:

    ProxyObject代理对象

    整个事务的增强执行过程是这样的:

    如上图所示 TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。

    但是当发生方法内调用的时候,被调用的函数Class.transactionTask()尽管看起来加了事务注解,但是并没有执行代理类对应的方法ProxyClass.transactionTask(),导致注解跟没写一样。

    @Transactional注解加在private修饰的方法也会一样的现象,原理其实一样的。

    搞清楚了原理,问题的原因就清晰了:
    这个问题的原因从表面来说,是因为在同一个Class内,非代理增强方法中调用了被@Transactional注解增强的方法,注解会失效。背后的实际原因是Spring AOP是基于代理,同一个类内这样调用的话,只有第一次调用了动态代理生成的ProxyClass,之后调用是不带任何切面信息的方法本身,因为没有直接调用Spring生成的代理对象。

    解决方法

    updatePayment方法放到另外一个类里,让Spring自动为其生成代理对象,调用方就能调用到updatePayment对应的ProxyObject的方法了。

    思考

    我们还提到了AspectJ也是实现AOP的一种方式,那么AspectJ有这样的方法内调用失效问题吗?

    可以关注**好奇心森林**公众号后台回复AOP,索取我总结的AOP思维脑图,答案就在里面

    问题二:定时器运行没启动em

    还是之前的一段代码,我们把updatePayment方法放在一个单独的类里。会发现之前payRepository.save(pay)必须显式声明保存,但是如果抽出来后就不用再写也能自动保存。

    @Service
    public class PaymentServiceImpl implements PaymentService {
        @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
        public void updatePayment(StatusResponse response) {
          Payment pay = payRepository.findByTrxId(response.getTrxId);
          //do something to update payment record by response and persist
          pay.setStatus(success);
          //payRepository.save(pay);
          xxxRepository.save(xxx);
        }
    }
    

    这个区别需要知道Hibernet对Entity的状态管理机制,在Hibernet里一个对象有多种状态:

    • Transient 瞬时态:直接new出来的对象,既没有被保存到数据库中,也不处于session缓存中
    • Persistent 持久态:已经被保存到数据库中并且加入到session缓存中
    • Detached 游离态:已经被保存到数据库中但不处于session缓存中

    通过findByTrxId查出来的Payment对象处于托管态,任何改变pay对象的操作比如pay.setStatus()都会在事务结束的时候自动提交。

    另外同事发现一个有趣的区别:

    在Controller调用PaymentServiceImpl.updatePayment()不需要显式保存pay对象,也能持久化到数据库,然而用Spring的定时器调用就不会生效。

    经过Debug发现,Spring框架在每个request通过OpenEntityManagerInViewInterceptorpreHandle方法里为每个request都建了一个EntityManager, 具体参见Spring源码:

    在Spring配置里加上spring.jpa.open-in-view=false 就会关闭每个request的EntityManager,通过controller调用就和定时器现象一样了。

    Open Session In View简称OSIV,是为了解决在mvc的controller中使用了hibernate的lazy load的属性时没有session抛出的LazyInitializationException异常。

    对hibernate来说ToMany关系默认是延迟加载,而ToOne关系则默认是立即加载;而在mvc的controller中脱离了persisent contenxt,于是entity变成了detached状态,这个时候要使用延迟加载的属性时就会抛出LazyInitializationException异常,而Open Session In View 旨在解决这个问题。

    Tips:

    通过OSIV技术来解决LazyInitialization问题会导致open的session生命周期过长,它贯穿整个request,在view渲染完之后才能关闭session释放数据库连接;另外OSIV将service层的技术细节暴露到了controller层,造成了一定的耦合,因而不建议开启,对应的解决方案就是在controller层中使用dto,而非detached状态的entity,所需的数据不再依赖延时加载,在组装dto的时候根据需要显式查询。

    总结

    通过一个bug的例子,我们总结了:

    • @Transactional 的底层实现
    • Spring AOP的不同实现方式和原理
    • Hibernet的对象生命周期
    • Spring的OSIV机制的目的和弊端

    如果觉得有所收获,麻烦帮我顺手点个在看吧,你的举手之劳对我来说就是最大的鼓励。 END~

    欢迎关注我的公众号:好奇心森林
    Wechat

  • 相关阅读:
    通过shell脚本排查jar包中类冲突
    批量复制及执行命令shell脚本
    java String hashCode遇到的坑
    hive常用命令
    hadoop-2.10.0安装hive-2.3.6
    centos7安装mysql-5.7.28
    centos7安装mysql-5.5和mysql-5.6
    centos7搭建hadoop2.10高可用(HA)
    centos7搭建hadoop2.10完全分布式
    kafka(一)-为什么选择kafka
  • 原文地址:https://www.cnblogs.com/hackingForest/p/12995144.html
Copyright © 2011-2022 走看看