最近同事发现一个业务状态部分更新的bug,这个bug会致使两张表的数据一致性问题。花了些时间去查问题的缘由,如今总结下里面遇到的知识点原理。java
咱们先看一段实例代码,来讲明下问题:spring
@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应用场景:缓存
咱们先复习下Spring AOP动态代理的原理。
AOP是一种通用的编程思想,Java里有2种实现方式:session
整个事务的加强执行过程是这样的:
mvc
如上图所示 TransactionInterceptor (事务拦截器)在目标方法执行先后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。框架
可是当发生方法内调用的时候,被调用的函数Class.transactionTask()
尽管看起来加了事务注解,可是并无执行代理类对应的方法ProxyClass.transactionTask()
,致使注解跟没写同样。
函数
把@Transactional
注解加在private
修饰的方法也会同样的现象,原理其实同样的。fetch
搞清楚了原理,问题的缘由就清晰了:
这个问题的缘由从表面来讲,是由于在同一个Class内,非代理加强方法中调用了被@Transactional注解加强的方法,注解会失效。背后的实际缘由是Spring AOP是基于代理,同一个类内这样调用的话,只有第一次调用了动态代理生成的ProxyClass,以后调用是不带任何切面信息的方法自己,由于没有直接调用Spring生成的代理对象。
把updatePayment
方法放到另一个类里,让Spring自动为其生成代理对象,调用方就能调用到updatePayment对应的ProxyObject的方法了。
咱们还提到了AspectJ也是实现AOP的一种方式,那么AspectJ有这样的方法内调用失效问题吗?
能够关注**好奇心森林**公众号后台回复AOP,索取我总结的AOP思惟脑图,答案就在里面
仍是以前的一段代码,咱们把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里一个对象有多种状态:
经过findByTrxId
查出来的Payment对象处于托管态,任何改变pay对象的操做好比pay.setStatus()都会在事务结束的时候自动提交。
另外同事发现一个有趣的区别:
在Controller调用PaymentServiceImpl.updatePayment()不须要显式保存pay对象,也能持久化到数据库,然而用Spring的定时器调用就不会生效。
通过Debug发现,Spring框架在每一个request经过OpenEntityManagerInViewInterceptor
的preHandle
方法里为每一个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的例子,咱们总结了:
若是以为有所收获,麻烦帮我顺手点个在看吧,你的举手之劳对我来讲就是最大的鼓励。 END~
欢迎关注个人公众号:好奇心森林