最近同事发现一个业务状态部分更新的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应用场景:

  • 统一的验证用户逻辑

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

原理分析

我们先复习下Spring AOP动态代理的原理。

AOP是一种通用的编程思想,Java里有2种实现方式:

  • Spring AOP,基于动态代理实现

    • JDK代理
    • Cglib代理
  • AspectJ,基于编译期实现

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

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

@Transactional 事务的底层原理的更多相关文章

  1. 漫画 | Spring AOP的底层原理是什么?

    1.Spring中配置的bean是在什么时候实例化的? 2.描述一下Spring中的IOC.AOP和DI IOC和AOP是Spring的两大核心思想 3.谈谈IOC.AOP和DI在项目开发中的应用场景 ...

  2. Spring系列28:@Transactional事务源码分析

    本文内容 @Transactional事务使用 @EnableTransactionManagement 详解 @Transactional事务属性的解析 TransactionInterceptor ...

  3. 【Srping】事务的执行原理(一)

    在使用事务的时候需要添加@EnableTransactionManagement注解来开启事务,那么就从@EnableTransactionManagement入手查看一下事务的执行原理. @Enab ...

  4. Neo4j图数据库简介和底层原理

    现实中很多数据都是用图来表达的,比如社交网络中人与人的关系.地图数据.或是基因信息等等.RDBMS并不适合表达这类数据,而且由于海量数据的存在,让其显得捉襟见肘.NoSQL数据库的兴起,很好地解决了海 ...

  5. 【T-SQL进阶】02.理解SQL查询的底层原理

    本系列[T-SQL]主要是针对T-SQL的总结. [T-SQL基础]01.单表查询-几道sql查询题 [T-SQL基础]02.联接查询 [T-SQL基础]03.子查询 [T-SQL基础]04.表表达式 ...

  6. Spring_day01--课程安排_Spring概念_IOC操作&IOC底层原理&入门案例_配置文件没有提示问题

    Spring_day01 Spring课程安排 今天内容介绍 Spring概念 Spring的ioc操作 IOC底层原理 IOC入门案例 配置文件没有提示问题 Spring的bean管理(xml方式) ...

  7. 理解SQL查询的底层原理

    阅读目录 一.SQL Server组成部分 二.查询的底层原理 本系列[T-SQL]主要是针对T-SQL的总结. T-SQL基础 [T-SQL基础]01.单表查询-几道sql查询题 [T-SQL基础] ...

  8. TCC分布式事务的实现原理(转载 石杉的架构笔记)

    拜托,面试请不要再问我TCC分布式事务的实现原理![石杉的架构笔记] 原创: 中华石杉 目录 一.写在前面 二.业务场景介绍 三.进一步思考 四.落地实现TCC分布式事务 (1)TCC实现阶段一:Tr ...

  9. SQL 执行 底层原理(一)

    一.SQL Server组成部分 1.关系引擎:主要作用是优化和执行查询.包含三大组件: (1)命令解析器:检查语法和转换查询树. (2)查询执行器:优化查询. (3)查询优化器:负责执行查询. 2. ...

随机推荐

  1. 2020年腾讯实习生C++面试题&持续更新中(2)

    2020年腾讯实习生C++面试题&持续更新中(2) hello,大家好~ 我是好好学习天天,天天编程的天天,一个每天都死磕技术,及时分享的技术宅~ 昨天分享的题目不知道大家是否看过了,以后我计 ...

  2. Java 如何实现优雅停服?刨根问底

    在 Java 的世界里遨游,如果能拥有一双善于发现的眼睛,有很多东西留心去看,外加耐心助力,仔细去品,往往会品出不一样的味道. 通过本次分享,能让你轻松 get 如下几点,绝对收获满满. a)如何让 ...

  3. 王艳 201771010127《面向对象程序设计(java)》第六周学习总结

    实验六 继承定义与使用 一:理论部分: 第五章:继承类. 1.继承:已有类来构建新类的一种机制.档定义了一个新类继承另一个类时,这个新类就继承了这个类的方法和域,同时在新类中添加新的方法和域以适应新的 ...

  4. POJ1984

    题目链接:https://vjudge.net/problem/POJ-1984 解题思路:并查集+离线操作. 用dx[ ]和dy[ ]两个数组存储某点相对于该点所在集合的源头的方位,因此不难推知dx ...

  5. POJ2377

    题目链接:http://poj.org/problem?id=2377 解题思路: Prim算法. Warning ! 注意考虑重边 ! 其实就是求最大生成树,没什么好说的,就上面那个坑. AC代码: ...

  6. ExtJS--grid表格前多选列

    为grid添加selModel属性: selModel:Ext.create('Ext.selection.CheckboxModel',{mode:"SIMPLE"}),//设置 ...

  7. Mac打不开inkscape怎么办

    本经验题目提到的是一款矢量图片编辑软件,对于打开不开的软件,完全可以通过卸载软件后进行安装.这里就从安装以及卸载的过程说明一下这个软件的安装卸载过程. 方法/步骤 打开电脑任意一个浏览器图标,进入浏览 ...

  8. PHP实现白名单或黑名单

    /**  * 安全IP检测,支持IP段检测  * @param string $ip 要检测的IP  * @param string|array $ips  白名单IP或者黑名单IP  * @retu ...

  9. 01 . 消息队列之(Kafka+ZooKeeper)

    消息队列简介 什么是消息队列? 首先,我们来看看什么是消息队列,维基百科里的解释翻译过来如下: 队列提供了一种异步通信协议,这意味着消息的发送者和接受者不需要同时与消息保持联系,发送者发送的消息会存储 ...

  10. 关于hexo中plugins博客配置对无法生成index.html文件的影响

    用hexo搭建的博客网站在访问时出现403错误,经调查后发现是public文件夹下的index.html文件丢失. 在csdn上搜了一下发现大家都是查看是否有一下hexo的插件未安装,将未安装插件安装 ...