首页 > 数据库 >SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题

时间:2022-10-03 11:02:43浏览次数:118  
标签:缓存 SpringBoot -- Transactional springframework AOP org 执行 方法


简介

说明

        本文介绍@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题的原因及解决方案。

注解的作用

        @Transactional:给当前方法添加事务支持,是通过 AOP 动态代理实现的,在方法执行完之后提交事务。

        @CacheEvict:在该方法执行完之后,清除 redis 中的缓存,也是使用 AOP 动态代理实现的。

问题引出

如果是@Transactional与@CacheEvict用在同一个方法上边,是否有问题?

是否可以达到如下效果:先保存到数据库,提交数据库事务,然后清除缓存?

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_数据库

打断点分析

给@CacheEvict打断点

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

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_redis_02

给@Transactional打断点

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

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_缓存_03

测试程序

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

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_spring boot_04

测试

调用save方法,id为1,countNumber为2。

  1. 首先命中了org.springframework.data.redis.cache.RedisCache#evict方法的断点,执行完该方法之后,对应的Redis数据已被清除(countNumber为空)。此时数据库里countNumber仍然是1。
  2. 再向下运行,到达com.mysql.jdbc.ConnectionImpl#commit,执行事务提交,执行完后,数据库里countNumber变成2

我们希望先提交事务,然后更新缓存。而真正的执行顺序是,先清除缓存,然后提交事务。

这样存在的问题:数据库和缓存数据不一致

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

解决方案

方案1:将事务单独拿出为一个方法

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_redis_05

方案2:修改AOP执行顺序

修改 AOP 的执行顺序:改成先提交事务,再清除缓存。

方法如下:

@EnableCaching(order = Ordered.HIGHEST_PRECEDENCE)

注意

优先级越高不是应该越先执行吗?缓存 AOP 的优先级最高怎么比事务提交 AOP 执行的要晚呢?

多个 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 方法。

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_spring boot_06

修改AOP顺序的方法分析

断点入口

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_缓存_07

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

拦截的方法

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_redis_08

        可以看到,执行 save 方法之前,被CglibAopProxy.DynamicAdvisedInterceptor#intercept方法所拦截了。(在 SpringBoot2.0 之后,SpringBoot 中 AOP 的默认实现被设置成了默认使用 CGLIB 来实现了。) 

所有的切面

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_redis_09

通过 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 内元素的顺序呢?

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

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

Order的设置方法

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_spring boot_10

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_数据库_11

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

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_redis_12

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

SpringBoot--解决@Transactional与@CacheEvict联合使用导致的缓存与数据库的一致性问题_数据库_13

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

其他网址

​当@Transactional遇到@CacheEvict,你的代码是不是有bug! - 腾讯云开发者社区-腾讯云​


标签:缓存,SpringBoot,--,Transactional,springframework,AOP,org,执行,方法
From: https://blog.51cto.com/knifeedge/5729944

相关文章

  • 智慧农业系统 - 可视化大屏(Echarts)&管理系统(HTTP(S)协议)&物联网平台(MQTT协议)
    一、平台功能特点农业数据实时监控,实时视频监控,历史数据分析;支持电子地图,设备地理位置精确定位;支持多级组织结构管理,满足集团大客户需求;可视化大屏展示,数据指标一目了然,彰显......
  • 玩转 Flowable 流程实例
    文章目录​​1.捋清三个概念​​​​2.五种流程启动方式​​​​3.简单实践​​​​4.删除流程实例​​​​5.获取运行的活动节点​​​​上篇文章​​松哥和大家聊了......
  • Go的Struct结构体和(Json Form tag)
    Go的Struct结构体和(JsonFormtag)1.Struct1.1定义使用struct关键字可以定义一个结构体,结构体中的成员,称为结构体的字段或属性。typeMemberstruct{idin......
  • 视图集
    两个视图基类1.drf提供了一个顶层的视图APIView,可以通过继承APIView写视图,之后我们写的代码可能重复代码比较多,就可以使用面向对象的继承、封装,GenericAPIView继承了APIVi......
  • Docker中实现macvlan的穿透访问
    需求因为性能等原因,我们需要用macvlan方式部署container而kernel中有关macvlan的安全策略会完全过滤来自host访问host想要访问container服务端口很不方便。破解实现ho......
  • 洛谷 P3388 【模板】割点(割顶)
    题目链接:https://www.luogu.com.cn/problem/P3388 【模板】割点(割顶)题目背景割点题目描述给出一个$n$个点,$m$条边的无向图,求图的割点。输入格式第一行输入两个......
  • 人如蝼蚁,跌倒再起。
    ......
  • 10.2模拟赛总结
    模拟赛总结T1预计得分:100实际得分:30挂分原因:式子推错了没开longlongT2预计得分:80实际得分:80T3预计得分:50实际得分:10挂分原因:单调栈右半部分写错了T4......
  • 多媒体——图片——从相册中选取图片
       系统相册既支持只选择一张图片,也支持同时选择多张图片。 跳到系统相册的代码例子如下: //创建一个内容获取动作的意图(准备跳到系统相册)IntentalbumInte......
  • 2022icpc网络赛
    EAnInterestingSequence题意:请构造一个总和最小,长度为n且首项为k,并且相邻两项的gcd=1的数组,输出数组各项之和。分析:显然对于n的奇数和偶数我们要进行分类讨论,我们......