本文首发于我的微信公众号:Coder小黑html
如上图所示,当@Transactional 遇到@CacheEvict,缓存放在 redis 中,这样写代码会有什么问题呢?大家的程序中是否写着这样的代码呢?若是是,请你马上修改!java
首先,@Transactional
是给当前方法添加事务支持,是经过 AOP 动态代理实现的,在方法执行完以后才提交事务。其次,@CacheEvict
是在该方法执行完以后,清除 redis 中的缓存,也是使用 AOP 动态代理实现的。mysql
那么,上述方法想表达语义应该是:先保存对象,提交事务,而后清除缓存。可是,这样写真的能达到这个语义吗?redis
首先,执行清除缓存的是org.springframework.cache.Cache#evict
方法,此处又是使用 redis 做为缓存的提供者,因此在清除缓存时,必然会调用 redis 缓存实现类的方法,即:org.springframework.data.redis.cache.RedisCache#evict
。因而,在该方法处加一个断点。spring
对于 JDBC 事务而言,想要提交事务,那就必需要调用java.sql.Connection#commit
方法。因为笔者此处使用的是 MySQL 数据库,因此这里对应的实现类为com.mysql.jdbc.ConnectionImpl#commit
。因而,一样在该方法加一个断点。sql
打上断点以后,让咱们来运行程序。数据库
在执行 save 方法以前,经过调用 getById 方法已经将对应的数据缓存到了 redis 中。同时,数据库中 countNumber 的值为 1。编程
程序再向下运行,能够发现,首先命中了org.springframework.data.redis.cache.RedisCache#evict
方法的断点,执行完该方法以后,能够看到,对应的缓存数据已被清除。缓存
由于尚未中事务提交的断点,因此此时很明显数据库中对应 id 为 1 的记录的 countNumber 值依旧为 1。微信
程序再向下执行,则执行事务提交。
执行完 commit 方法以后,事务提交,对应记录更新成功。
到这里也就解决了本文开篇所提到的问题,咱们但愿程序是先提交事务,而后更新缓存。而真正的执行顺序是,先清除缓存,而后提交事务。
那这样会有什么问题呢?先清除缓存,而后在事务尚未提交以前,程序就收到了用户的请求,发现缓存中没有数据,则去数据库中获取数据(事务尚未提交则获取到旧值),同时将获取的数据添加到缓存中。此时会致使数据库和缓存数据不一致。
事务是一个很容易出问题的操做,@Transactional
事务不要滥用 ,用的时候要尽量的缩小事务范围,在事务方法中只作事务相关的操做。引用阿里巴巴 Java 开发手册的一句话:
若是能够改为先提交事务,再清除缓存,同样能够解决这个问题。那 Spring 中有没有什么方法能够去修改 AOP 的执行顺序呢?
@Transactional
和@CacheEvict
都是经过动态代理来实现的,在执行 save 方法处打一个断点,命中断点以后,点击Step Into
,就能够进入到代理对象的执行方法内。
能够看到,执行 save 方法以前,被CglibAopProxy.DynamicAdvisedInterceptor#intercept
方法所拦截了。
在 SpringBoot2.0 以后,SpringBoot 中 AOP 的默认实现被设置成了默认使用 CGLIB 来实现了。具体能够阅读笔者以前的文章:
Spring5 AOP 默认使用 CGLIB ?从现象到源码的深度分析
经过 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 中的排序呢?
以BeanFactoryTransactionAttributeSourceAdvisor
为例,order 的值来自于AnnotationAttributes enableTx
对象的某个属性。
经过源码能够发现,AnnotationAttributes enableTx
的属性所有都来自于@EnableTransactionManagement
注解。
同理,@EnableCaching
注解上也能够配置 order,这里不在赘述。
下面,咱们就来尝试解决这个问题,看可否经过配置 order 来修改 AOP 的执行顺序。
经过@EnableCaching(order = Ordered.HIGHEST_PRECEDENCE)
这个属性值的配置,运行程序以后,的确作到了先提交事务,再清理缓存的效果,bug 修复成功~~
至于这个 order 设置是怎么生效的,本文就不在此进行相关说明了。感兴趣的读者能够自行参阅相关源码,对应的源码在org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean
,同时使用的比较器为:org.springframework.core.annotation.AnnotationAwareOrderComparator
。
看到这里不知道读者有没有疑问,优先级越高不是应该越先执行吗?!缓存 AOP 的优先级最高怎么比事务提交 AOP 执行的时机要晚呢?
咱们来查阅一下 Spring 的官方文档:
简单翻译一下:(这个英文翻译有点难,建议你们阅读原文)
当多个 advice 运行在同一个 join point 时会怎么样呢? Spring AOP 遵循与 AspectJ 相同的优先级规则来肯定建议执行的顺序。能够经过实现org.springframework.core.Ordered
接口或者使用@Order
注解来控制其执行顺序。优先级最高的 advice 首先“在入口”运行,从 join point“出来”时,优先级最高的 advice 将最后运行。
那应该怎么理解呢?
能够把 Spring AOP 想象成一个同心圆。被加强的原始方法在圆心,每一层 AOP 就是增长一个新的同心圆。同时,优先级最高的在最外层。方法被调用时,从最外层按照 AOP一、AOP2 的顺序依次执行 around、before 方法,而后执行 method 方法,最后按照 AOP二、AOP1 的顺序依次执行 after 方法。
当@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 的多线程环境下,该方法是会产生什么问题?
欢迎关注公众号,一块儿学习成长。