Когда @Transactional встречается с @CacheEvict, ваш код все еще работает?

Spring Boot

Эта статья была впервые опубликована в личном паблике WeChat: Coder Xiaohe

有bug吗

Как показано на рисунке выше, когда @Transactional встречает @CacheEvict, кеш помещается в redis. В чем проблема написать такой код? У вас есть такой код, написанный в вашей программе? Если да, пожалуйста, измените его немедленно!

думаю 🤔

первый,@TransactionalЭто добавление поддержки транзакций к текущему методу, который реализован через динамический прокси АОП, и транзакция фиксируется после выполнения метода. Второй,@CacheEvictПосле выполнения метода происходит очистка кеша в redis, что также реализовано с помощью динамического прокси АОП.

Так,Семантика вышеуказанного метода должна быть следующей: сначала сохранить объект, зафиксировать транзакцию, а затем очистить кеш. Однако может ли такая фотография достичь такой семантики?

Отладьте, чтобы найти правду 👣

Во-первых, тот, который выполняет очистку кеша,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程序

Перед выполнением метода сохранения соответствующие данные были кэшированы в Redis путем вызова метода getById. При этом значение countNumber в базе равно 1.

添加缓存到redis中

Программа снова запускается, и можно обнаружить, что первое попаданиеorg.springframework.data.redis.cache.RedisCache#evictТочка останова метода, после выполнения метода вы можете видеть, что соответствующие данные кеша были очищены.

缓存已被清除

Поскольку для фиксации транзакции нет точки останова, очевидно, что значение countNumber записи, соответствующей идентификатору 1 в базе данных, по-прежнему равно 1.

数据库中的记录

Когда программа снова запускается вниз, выполняется фиксация транзакции.

提交事务

После выполнения метода фиксации транзакция фиксируется, и соответствующая запись успешно обновляется.

更新成功

На данный момент проблема, упомянутая в начале этой статьи, решена.Мы хотим, чтобы программа сначала зафиксировала транзакцию, а затем обновила кеш. И реальный порядок выполнения - сначала очистить кеш, а затем зафиксировать транзакцию..

Так в чем проблема? Сначала очищаем кеш, а потом перед фиксацией транзакции программа получает запрос пользователя и находит, что данных в кеше нет, затем обращается в БД за данными (транзакция не зафиксирована, старое значение получается), и полученные данные одновременно добавляются в кеш. В этом случае база данных и кэшированные данные будут несовместимы.

Как исправить 👀

Сценарий 1. Измените код, чтобы сузить область транзакции.

Транзакция — очень проблематичная операция,@TransactionalНе злоупотребляйте транзакцией.При ее использовании вы должны максимально уменьшить объем транзакции и выполнять только операции, связанные с транзакцией, в методе транзакции. Цитата из руководства по разработке Java для Alibaba:

image.png

缩小事务范围

На скриншоте небольшая ошибка, при использовании решения 1 для решения проблемы нет необходимости использовать аннотацию транзакции на исходном методе.

Сценарий 2: изменение порядка выполнения АОП

Если его можно изменить, чтобы сначала зафиксировать транзакцию, а затем очистить кеш, эта проблема также может быть решена. Есть ли способ в Spring изменить порядок выполнения АОП?

@Transactionalа также@CacheEvictВсе реализовано через динамический прокси, нажмите точку останова, где выполняется метод сохранения, после попадания в точку останова нажмитеStep Into, вы можете ввести метод выполнения прокси-объекта.

step into

CglibAopProxy.DynamicAdvisedInterceptor#intercept

Как видите, перед выполнением метода сохраненияCglibAopProxy.DynamicAdvisedInterceptor#interceptметод заблокирован.

После SpringBoot 2.0 реализация АОП по умолчанию в SpringBoot по умолчанию настроена на использование CGLIB. Подробности читайте в моих предыдущих статьях:

Tickets.WeChat.QQ.com/Yes/o YH4GV для JE…

Spring5 AOP по умолчанию использует CGLIB? Углубленный анализ от явления до исходного кода

image.png

Через отладку вы можете найти:advised.advisorsявляется списком, и два советника в списке:

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

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

Так как же нам изменить порядок элементов в списке?

просмотревBeanFactoryCacheOperationSourceAdvisorа такжеBeanFactoryTransactionAttributeSourceAdvisorИсходный код показывает, что эти два класса наследуютorg.springframework.aop.support.AbstractPointcutAdvisor,а такжеAbstractPointcutAdvisorЭтот абстрактный класс реализуетorg.springframework.core.Orderedинтерфейс.

Гипотеза: можем ли мы повлиять на порядок в списке, изменив возвращаемое значение метода getOrder()?

org.springframework.aop.support.AbstractPointcutAdvisor

кBeanFactoryTransactionAttributeSourceAdvisorНапример, стоимость заказа определяетсяAnnotationAttributes enableTxнекоторое свойство объекта.

ProxyTransactionManagementConfiguration#transactionAdvisor

Его можно найти в исходном коде,AnnotationAttributes enableTxСвойства всех исходят из@EnableTransactionManagementаннотация.

AbstractTransactionManagementConfiguration#setImportMetadata

@EnableTransactionManagement

Так же,@EnableCachingПорядок также можно настроить в аннотации, которая здесь повторяться не будет.

Далее попробуем решить эту проблему и посмотрим, сможем ли мы изменить порядок выполнения АОП, настроив порядок.

修改AOP执行顺序

пройти через@EnableCaching(order = Ordered.HIGHEST_PRECEDENCE)Конфигурация значения этого атрибута после запуска программы позволяет сначала отправить транзакцию, а затем очистить кеш, и ошибка успешно исправлена ​​~~

Что касается того, как эта настройка порядка действует, в этой статье мы не будем ее описывать. Заинтересованные читатели могут самостоятельно обратиться к соответствующему исходному коду.org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean, компараторы, используемые одновременно:org.springframework.core.annotation.AnnotationAwareOrderComparator.

Advice Ordering

Я не знаю, есть ли у читателей какие-либо вопросы после просмотра этого,Не должен ли сначала выполняться более высокий приоритет? ! Почему АОП кэша имеет наивысший приоритет, чем АОП фиксации транзакции, который выполняется позже?

Давайте проверим официальную документацию Spring:

docs.spring.IO/весна/документы…

Advice Ordering

Просто переведите: (Этот английский перевод немного сложен, я предлагаю вам прочитать исходный текст)

Что происходит, когда несколько советов выполняются в одной и той же точке соединения? Spring AOP следует тем же правилам приоритета, что и AspectJ, для определения порядка выполнения рекомендаций. может быть достигнуто путемorg.springframework.core.Orderedинтерфейс или использование@OrderАннотация для контроля порядка ее выполнения. Совет с наивысшим приоритетом запускается первым «при входе», а при «выходе» из точки соединения совет с наивысшим приоритетом запускается последним.

Как это следует понимать?

Думайте о Spring AOP как о концентрическом круге. Усовершенствованный исходный метод находится в центре круга, и каждый слой АОП должен добавлять новый концентрический круг. В то же время наивысший приоритет у самого внешнего слоя. При вызове метода выполните методы around и before в порядке AOP1 и AOP2 с самого внешнего уровня, затем выполните метод метода и, наконец, выполните метод after в порядке AOP2 и AOP1..

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 какие проблемы вызовет этот метод?


Добро пожаловать, чтобы обратить внимание на общественный номер, учиться и расти вместе.

Coder小黑