最新公告
  • 新注册用户请前往个人中心绑定邮箱以便接收相关凭证邮件!!!点击前往个人中心
  • 当@Transactional遇到@CacheEvict,你的代码是不是有bug!

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

    思考 ?

    首先,@Transactional是给当前方法添加事务支持,是通过 AOP 动态代理实现的,在方法执行完之后才提交事务。其次,@CacheEvict是在该方法执行完之后,清除 redis 中的缓存,也是使用 AOP 动态代理实现的。
    那么,上述方法想表达语义应该是:先保存对象,提交事务,然后清除缓存。但是,这样写真的能达到这个语义吗?

    Debug 寻找真相 ?

    首先,执行清除缓存的是org.springframework.cache.Cache#evict方法,此处又是使用 redis 作为缓存的提供者,所以在清除缓存时,必然会调用 redis 缓存实现类的方法,即:org.springframework.data.redis.cache.RedisCache#evict。于是,在该方法处加一个断点。
    org.springframework.data.redis.cache.RedisCache#evict
    对于 JDBC 事务而言,想要提交事务,那就必须要调用java.sql.Connection#commit方法。由于笔者此处使用的是 MySQL 数据库,所以这里对应的实现类为com.mysql.jdbc.ConnectionImpl#commit。于是,同样在该方法加一个断点。
    com.mysql.jdbc.ConnectionImpl#commit
    打上断点之后,让我们来运行程序。
    demo程序
    在执行 save 方法之前,通过调用 getById 方法已经将对应的数据缓存到了 redis 中。同时,数据库中 countNumber 的值为 1。
    添加缓存到redis中
    程序再向下运行,可以发现,首先命中了org.springframework.data.redis.cache.RedisCache#evict方法的断点,执行完该方法之后,可以看到,对应的缓存数据已被清除。
    缓存已被清除
    因为还没有中事务提交的断点,所以此时很明显数据库中对应 id 为 1 的记录的 countNumber 值依旧为 1。
    数据库中的记录
    程序再向下执行,则执行事务提交。
    提交事务
    执行完 commit 方法之后,事务提交,对应记录更新成功。
    更新成功
    到这里也就解决了本文开篇所提到的问题,我们希望程序是先提交事务,然后更新缓存。而真正的执行顺序是,先清除缓存,然后提交事务
    那这样会有什么问题呢?先清除缓存,然后在事务还没有提交之前,程序就收到了用户的请求,发现缓存中没有数据,则去数据库中获取数据(事务还没有提交则获取到旧值),同时将获取的数据添加到缓存中。此时会导致数据库和缓存数据不一致。

    如何解决 ?

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

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

    方案 2:修改 AOP 执行顺序

    如果可以改成先提交事务,再清除缓存,一样可以解决这个问题。那 Spring 中有没有什么方法可以去修改 AOP 的执行顺序呢?
    @Transactional@CacheEvict都是通过动态代理来实现的,在执行 save 方法处打一个断点,命中断点之后,点击Step Into,就可以进入到代理对象的执行方法内。
    step into
    CglibAopProxy.DynamicAdvisedInterceptor#intercept
    可以看到,执行 save 方法之前,被CglibAopProxy.DynamicAdvisedInterceptor#intercept方法所拦截了。
    在 SpringBoot2.0 之后,SpringBoot 中 AOP 的默认实现被设置成了默认使用 CGLIB 来实现了。具体可以阅读笔者之前的文章:
    https://mp.weixin.qq.com/s/oyH4GVwJeG24GVqLM48bVg
    Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
    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 中的排序呢?
    org.springframework.aop.support.AbstractPointcutAdvisor
    BeanFactoryTransactionAttributeSourceAdvisor为例,order 的值来自于AnnotationAttributes enableTx对象的某个属性。
    ProxyTransactionManagementConfiguration#transactionAdvisor
    通过源码可以发现,AnnotationAttributes enableTx的属性全部都来自于@EnableTransactionManagement注解。
    AbstractTransactionManagementConfiguration#setImportMetadata
    @EnableTransactionManagement
    同理,@EnableCaching注解上也可以配置 order,这里不在赘述。
    下面,我们就来尝试解决这个问题,看能否通过配置 order 来修改 AOP 的执行顺序。
    修改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
    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 方法
    AOP

    总结

    当@Transactional 遇到@CacheEvict,默认设置的情况下,可能会因为先清除缓存后提交事务,从而产生缓存和数据库数据不一致的问题。
    同时,文本也提出了两种解决方案。但是,笔者更建议使用方案 1,因为方案 1 更多的是体现了一种编程思想,让事务方法尽可能的小。

    作业

    阅读下面源码:
    @Transactional
    publicsynchronizedvoidincrement(Integer id){
    Counter counter = counterRepository.getOne(id);
    counter.setCountNumber(counter.getCountNumber() + 1);
    counterRepository.save(counter);
    }
    本站所有文章均由网友分享,仅用于参考学习用,请勿直接转载,如有侵权,请联系网站客服删除相关文章。若由于商用引起版权纠纷,一切责任均由使用者承担
    极客文库 » 当@Transactional遇到@CacheEvict,你的代码是不是有bug!

    常见问题FAQ

    如果资源链接失效了怎么办?
    本站用户分享的所有资源都有自动备份机制,如果资源链接失效,请联系本站客服QQ:2580505920更新资源地址。
    如果用户分享的资源与描述不符怎么办?
    可以联系客服QQ:2580505920,如果要求合理可以安排退款或者退赞助积分。
    如何分享个人资源获取赞助积分或其他奖励?
    本站用户可以分享自己的资源,但是必须保证资源没有侵权行为。点击个人中心,根据操作填写并上传即可。资源所获收益完全归属上传者,每周可申请提现一次。
    如果您发现了本资源有侵权行为怎么办?
    及时联系客服QQ:2580505920,核实予以删除。

    参与讨论

    • 211会员总数(位)
    • 3737资源总数(个)
    • 0本周发布(个)
    • 0 今日发布(个)
    • 817稳定运行(天)

    欢迎加入「极客文库」,成为原创作者从这里开始!

    立即加入 了解更多
    成为赞助用户享有更多特权立即升级