当前位置: 首页 > news >正文

@transactional 方法执行完再commit_当@Transactional遇到@CacheEvict,你的代码是不是有bug!...

d950290a67c1cd5cd0482fee95c477ce.gif

点击蓝字关注不迷路

8560595756ebf43d7f13e06f0367384b.gif
3f80a2b8d4c24ab1235447fdb5d8a610.png
有bug吗

如上图所示,当@Transactional 遇到@CacheEvict,缓存放在 redis 中,这样写代码会有什么问题呢?你们的程序中是否写着这样的代码呢?如果是,请你立刻修改!

思考 ?

首先,@Transactional是给当前方法添加事务支持,是通过 AOP 动态代理实现的,在方法执行完之后才提交事务。其次,@CacheEvict是在该方法执行完之后,清除 redis 中的缓存,也是使用 AOP 动态代理实现的。

那么,上述方法想表达语义应该是:先保存对象,提交事务,然后清除缓存。但是,这样写真的能达到这个语义吗?

Debug 寻找真相 ?

首先,执行清除缓存的是org.springframework.cache.Cache#evict方法,此处又是使用 redis 作为缓存的提供者,所以在清除缓存时,必然会调用 redis 缓存实现类的方法,即:org.springframework.data.redis.cache.RedisCache#evict。于是,在该方法处加一个断点。

faca0d59703574ebd2a0671f6937fa9d.png
org.springframework.data.redis.cache.RedisCache#evict

对于 JDBC 事务而言,想要提交事务,那就必须要调用java.sql.Connection#commit方法。由于笔者此处使用的是 MySQL 数据库,所以这里对应的实现类为com.mysql.jdbc.ConnectionImpl#commit。于是,同样在该方法加一个断点。

3c27b6e37b30e0da47b925d6c30085c8.png
com.mysql.jdbc.ConnectionImpl#commit

打上断点之后,让我们来运行程序。

26f4740a0b5a5e6e9f7be35deea13cfe.png
demo程序

在执行 save 方法之前,通过调用 getById 方法已经将对应的数据缓存到了 redis 中。同时,数据库中 countNumber 的值为 1。

9bafa4caecbfdd7fb0b7b55cf13a5fc4.png
添加缓存到redis中

程序再向下运行,可以发现,首先命中了org.springframework.data.redis.cache.RedisCache#evict方法的断点,执行完该方法之后,可以看到,对应的缓存数据已被清除。

0521fa2397009378d69c1073ee30cb69.png
缓存已被清除

因为还没有中事务提交的断点,所以此时很明显数据库中对应 id 为 1 的记录的 countNumber 值依旧为 1。

914e7799a8d1560aabf8b4aa5560e676.png
数据库中的记录

程序再向下执行,则执行事务提交。

66dcc8a053ea10fe99cfb6fe98a5bbde.png
提交事务

执行完 commit 方法之后,事务提交,对应记录更新成功。

cefe2eb45feaf1af78584894a0beb516.png
更新成功

到这里也就解决了本文开篇所提到的问题,我们希望程序是先提交事务,然后更新缓存。而真正的执行顺序是,先清除缓存,然后提交事务

那这样会有什么问题呢?先清除缓存,然后在事务还没有提交之前,程序就收到了用户的请求,发现缓存中没有数据,则去数据库中获取数据(事务还没有提交则获取到旧值),同时将获取的数据添加到缓存中。此时会导致数据库和缓存数据不一致。

如何解决 ?

方案 1:修改代码,缩小事务范围

事务是一个很容易出问题的操作,@Transactional事务不要滥用 ,用的时候要尽可能的缩小事务范围,在事务方法中只做事务相关的操作。引用阿里巴巴 Java 开发手册的一句话:

02b6c3283e20ee2cb193ecb22edab929.png
image.png
76f5465b867fb178bed32953e8086fb1.png
缩小事务范围

方案 2:修改 AOP 执行顺序

如果可以改成先提交事务,再清除缓存,一样可以解决这个问题。那 Spring 中有没有什么方法可以去修改 AOP 的执行顺序呢?

@Transactional@CacheEvict都是通过动态代理来实现的,在执行 save 方法处打一个断点,命中断点之后,点击Step Into,就可以进入到代理对象的执行方法内。

741d0cdf99bbb314761aa76b433cd369.png
step into
0253e803f8f0af74c22e951cf56da6ac.png
CglibAopProxy.DynamicAdvisedInterceptor#intercept

可以看到,执行 save 方法之前,被CglibAopProxy.DynamicAdvisedInterceptor#intercept方法所拦截了。

在 SpringBoot2.0 之后,SpringBoot 中 AOP 的默认实现被设置成了默认使用 CGLIB 来实现了。具体可以阅读笔者之前的文章:

https://mp.weixin.qq.com/s/oyH4GVwJeG24GVqLM48bVg

Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析

795be0a519c8dd01567e41d50db0b79c.png
image.png

通过 debug 可以发现:advised.advisors是一个 List,List 中的两个 Advisor 分别为:

org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor: advice org.springframework.cache.interceptor.CacheInterceptor@4b2e3e8f

org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor: advice org.springframework.transaction.interceptor.TransactionInterceptor@27a97e08

那我们要怎么样去修改 List 内元素的顺序呢?

通过查看BeanFactoryCacheOperationSourceAdvisorBeanFactoryTransactionAttributeSourceAdvisor的源码可知,这两个类均继承了org.springframework.aop.support.AbstractPointcutAdvisor,而AbstractPointcutAdvisor这个抽象类实现了org.springframework.core.Ordered接口。

猜想:那我们是不是可以通过修改 getOrder()方法的返回值来影响 List 中的排序呢?

860600aa8730803339a53a0389c577fb.png
org.springframework.aop.support.AbstractPointcutAdvisor

BeanFactoryTransactionAttributeSourceAdvisor为例,order 的值来自于AnnotationAttributes enableTx对象的某个属性。

61c02489f6c0f15441ad02e741620203.png
ProxyTransactionManagementConfiguration#transactionAdvisor

通过源码可以发现,AnnotationAttributes enableTx的属性全部都来自于@EnableTransactionManagement注解。

e3f8472bf9ad594ce1cb53c7dc2c217d.png
AbstractTransactionManagementConfiguration#setImportMetadata
5317f42803c752074195fad21cd54a76.png
@EnableTransactionManagement

同理,@EnableCaching注解上也可以配置 order,这里不在赘述。

下面,我们就来尝试解决这个问题,看能否通过配置 order 来修改 AOP 的执行顺序。

7d4c0bc3f15aa21d1b4c8e163198439f.png
修改AOP执行顺序

通过@EnableCaching(order = Ordered.HIGHEST_PRECEDENCE)这个属性值的配置,运行程序之后,的确做到了先提交事务,再清理缓存的效果,bug 修复成功~~

至于这个 order 设置是怎么生效的,本文就不在此进行相关说明了。感兴趣的读者可以自行参阅相关源码,对应的源码在org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean,同时使用的比较器为:org.springframework.core.annotation.AnnotationAwareOrderComparator

Advice Ordering

看到这里不知道读者有没有疑问,优先级越高不是应该越先执行吗?!缓存 AOP 的优先级最高怎么比事务提交 AOP 执行的时机要晚呢?

我们来查阅一下 Spring 的官方文档:

https://docs.spring.io/spring/docs/5.2.1.RELEASE/spring-framework-reference/core.html#aop-ataspectj-advice-ordering

75c085510ee5ea0b7201301e6f3be5bd.png
Advice Ordering

简单翻译一下:(这个英文翻译有点难,建议大家阅读原文)

当多个 advice 运行在同一个 join point 时会怎么样呢?Spring AOP 遵循与 AspectJ 相同的优先级规则来确定建议执行的顺序。可以通过实现org.springframework.core.Ordered接口或者使用@Order注解来控制其执行顺序。优先级最高的 advice 首先“在入口”运行,从 join point“出来”时,优先级最高的 advice 将最后运行。

那应该怎么理解呢?

可以把 Spring AOP 想象成一个同心圆。被增强的原始方法在圆心,每一层 AOP 就是增加一个新的同心圆。同时,优先级最高的在最外层。方法被调用时,从最外层按照 AOP1、AOP2 的顺序依次执行 around、before 方法,然后执行 method 方法,最后按照 AOP2、AOP1 的顺序依次执行 after 方法

9b6d6752e8320be1b588ddee4fed4511.png
AOP

总结

当@Transactional 遇到@CacheEvict,默认设置的情况下,可能会因为先清除缓存后提交事务,从而产生缓存和数据库数据不一致的问题。

同时,文本也提出了两种解决方案。但是,笔者更建议使用方案 1(截图有错误,原方法不需要添加事务注解),因为方案 1 更多的是体现了一种编程思想,让事务方法尽可能的小。

作业

阅读下面源码:

@Transactional
public synchronized void increment(Integer id) {
Counter counter = counterRepository.getOne(id);
counter.setCountNumber(counter.getCountNumber() + 1);
counterRepository.save(counter);
}

思考:在单 JVM 的多线程环境下,该方法是会产生什么问题?有想法的小伙伴可以在公众号后台给我留言。

如果本文对你有帮助的话,可以点赞、转发、收藏哦~

cf230e004a14789f313aec505225ae68.png你点的每个好看,我都认真当成了喜欢

相关文章:

  • mysql 1067_MySQL发生系统错误1067的解决办法?
  • mysql 存储过程 批量_mysql 生成批量存储过程
  • mysql名称证书秘钥_mysql通过ssl的方式生成秘钥
  • ios操作mysql数据库_ios数据库操作
  • mybatis 调用mysql函数_mybatis mapper调用mysql存储过程
  • error msb6006: “cmd.exe”已退出 代码为1_Django安装与简单配置(1)
  • mysql 5.6.3 current_timestamp_mysql5.6以上版本: timestamp current_timestamp报1064/1067错误
  • mysql导入导致锁表_mysql 导出数据导致锁表
  • mysql order by 报错_mysql 高版本order by 报错解决方案
  • apache mysql windows_windows上apache+php+mysql环境部署
  • mysql5.6吞吐量_MySQL 5.5和MySQL 5.6的吞吐量测试
  • java中while循环_Java中do...while循环和for循环还有死循环
  • spark向MySQL刷新一个字段_Spark 实现MySQL update操作
  • vb将指针指向内容传数组_C语言指针
  • mysql json链接表_将JSON插入MySQL表中?
  • [iOS]Core Data浅析一 -- 启用Core Data
  • Android框架之Volley
  • Apache的基本使用
  • C学习-枚举(九)
  • DOM的那些事
  • Koa2 之文件上传下载
  • 阿里云应用高可用服务公测发布
  • 基于HAProxy的高性能缓存服务器nuster
  • 开发基于以太坊智能合约的DApp
  • 设计模式走一遍---观察者模式
  • 使用阿里云发布分布式网站,开发时候应该注意什么?
  • 事件委托的小应用
  • ​ ​Redis(五)主从复制:主从模式介绍、配置、拓扑(一主一从结构、一主多从结构、树形主从结构)、原理(复制过程、​​​​​​​数据同步psync)、总结
  • ​如何使用ArcGIS Pro制作渐变河流效果
  • ​软考-高级-信息系统项目管理师教程 第四版【第14章-项目沟通管理-思维导图】​
  • #Linux(权限管理)
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • (¥1011)-(一千零一拾一元整)输出
  • (二)学习JVM —— 垃圾回收机制
  • (附源码)spring boot火车票售卖系统 毕业设计 211004
  • (附源码)springboot 房产中介系统 毕业设计 312341
  • (附源码)springboot优课在线教学系统 毕业设计 081251
  • (附源码)ssm教材管理系统 毕业设计 011229
  • (接口封装)
  • (六)激光线扫描-三维重建
  • (四)linux文件内容查看
  • (四)汇编语言——简单程序
  • (原創) 如何優化ThinkPad X61開機速度? (NB) (ThinkPad) (X61) (OS) (Windows)
  • ****Linux下Mysql的安装和配置
  • .NET Framework 4.6.2改进了WPF和安全性
  • .net 打包工具_pyinstaller打包的exe太大?你需要站在巨人的肩膀上-VC++才是王道
  • .net 验证控件和javaScript的冲突问题
  • .NetCore实践篇:分布式监控Zipkin持久化之殇
  • ??eclipse的安装配置问题!??
  • @Import注解详解
  • [ 渗透工具篇 ] 一篇文章让你掌握神奇的shuize -- 信息收集自动化工具
  • [100天算法】-二叉树剪枝(day 48)
  • [Android Pro] Notification的使用
  • [APIO2015]巴厘岛的雕塑
  • [AutoSar]BSW_Memory_Stack_003 NVM与APP的显式和隐式同步