Эта статья была впервые опубликована в личном паблике WeChat: Coder Xiaohe
Как показано на рисунке выше, когда @Transactional встречает @CacheEvict, кеш помещается в redis. В чем проблема написать такой код? У вас есть такой код, написанный в вашей программе? Если да, пожалуйста, измените его немедленно!
думаю 🤔
первый,@Transactional
Это добавление поддержки транзакций к текущему методу, который реализован через динамический прокси АОП, и транзакция фиксируется после выполнения метода. Второй,@CacheEvict
После выполнения метода происходит очистка кеша в redis, что также реализовано с помощью динамического прокси АОП.
Так,Семантика вышеуказанного метода должна быть следующей: сначала сохранить объект, зафиксировать транзакцию, а затем очистить кеш. Однако может ли такая фотография достичь такой семантики?
Отладьте, чтобы найти правду 👣
Во-первых, тот, который выполняет очистку кеша,org.springframework.cache.Cache#evict
метод, здесь в качестве провайдера кеша используется redis, поэтому при очистке кеша будет вызываться метод класса реализации кеша redis, а именно:org.springframework.data.redis.cache.RedisCache#evict
. Итак, добавьте точку останова в этот метод.
Для транзакций JDBC, если вы хотите зафиксировать транзакцию, вы должны вызватьjava.sql.Connection#commit
метод. Поскольку здесь автор использует базу данных MySQL, соответствующий класс реализации здесьcom.mysql.jdbc.ConnectionImpl#commit
. Итак, также добавьте точку останова в этот метод.
После достижения точки останова запустим программу.
Перед выполнением метода сохранения соответствующие данные были кэшированы в Redis путем вызова метода getById. При этом значение countNumber в базе равно 1.
Программа снова запускается, и можно обнаружить, что первое попаданиеorg.springframework.data.redis.cache.RedisCache#evict
Точка останова метода, после выполнения метода вы можете видеть, что соответствующие данные кеша были очищены.
Поскольку для фиксации транзакции нет точки останова, очевидно, что значение countNumber записи, соответствующей идентификатору 1 в базе данных, по-прежнему равно 1.
Когда программа снова запускается вниз, выполняется фиксация транзакции.
После выполнения метода фиксации транзакция фиксируется, и соответствующая запись успешно обновляется.
На данный момент проблема, упомянутая в начале этой статьи, решена.Мы хотим, чтобы программа сначала зафиксировала транзакцию, а затем обновила кеш. И реальный порядок выполнения - сначала очистить кеш, а затем зафиксировать транзакцию..
Так в чем проблема? Сначала очищаем кеш, а потом перед фиксацией транзакции программа получает запрос пользователя и находит, что данных в кеше нет, затем обращается в БД за данными (транзакция не зафиксирована, старое значение получается), и полученные данные одновременно добавляются в кеш. В этом случае база данных и кэшированные данные будут несовместимы.
Как исправить 👀
Сценарий 1. Измените код, чтобы сузить область транзакции.
Транзакция — очень проблематичная операция,@Transactional
Не злоупотребляйте транзакцией.При ее использовании вы должны максимально уменьшить объем транзакции и выполнять только операции, связанные с транзакцией, в методе транзакции. Цитата из руководства по разработке Java для Alibaba:
На скриншоте небольшая ошибка, при использовании решения 1 для решения проблемы нет необходимости использовать аннотацию транзакции на исходном методе.
Сценарий 2: изменение порядка выполнения АОП
Если его можно изменить, чтобы сначала зафиксировать транзакцию, а затем очистить кеш, эта проблема также может быть решена. Есть ли способ в Spring изменить порядок выполнения АОП?
@Transactional
а также@CacheEvict
Все реализовано через динамический прокси, нажмите точку останова, где выполняется метод сохранения, после попадания в точку останова нажмитеStep Into
, вы можете ввести метод выполнения прокси-объекта.
Как видите, перед выполнением метода сохраненияCglibAopProxy.DynamicAdvisedInterceptor#intercept
метод заблокирован.
После SpringBoot 2.0 реализация АОП по умолчанию в SpringBoot по умолчанию настроена на использование CGLIB. Подробности читайте в моих предыдущих статьях:
Tickets.WeChat.QQ.com/Yes/o YH4GV для JE…
Spring5 AOP по умолчанию использует CGLIB? Углубленный анализ от явления до исходного кода
Через отладку вы можете найти: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()?
кBeanFactoryTransactionAttributeSourceAdvisor
Например, стоимость заказа определяетсяAnnotationAttributes enableTx
некоторое свойство объекта.
Его можно найти в исходном коде,AnnotationAttributes enableTx
Свойства всех исходят из@EnableTransactionManagement
аннотация.
Так же,@EnableCaching
Порядок также можно настроить в аннотации, которая здесь повторяться не будет.
Далее попробуем решить эту проблему и посмотрим, сможем ли мы изменить порядок выполнения АОП, настроив порядок.
пройти через@EnableCaching(order = Ordered.HIGHEST_PRECEDENCE)
Конфигурация значения этого атрибута после запуска программы позволяет сначала отправить транзакцию, а затем очистить кеш, и ошибка успешно исправлена ~~
Что касается того, как эта настройка порядка действует, в этой статье мы не будем ее описывать. Заинтересованные читатели могут самостоятельно обратиться к соответствующему исходному коду.org.springframework.aop.framework.autoproxy.AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean
, компараторы, используемые одновременно:org.springframework.core.annotation.AnnotationAwareOrderComparator
.
Advice Ordering
Я не знаю, есть ли у читателей какие-либо вопросы после просмотра этого,Не должен ли сначала выполняться более высокий приоритет? ! Почему АОП кэша имеет наивысший приоритет, чем АОП фиксации транзакции, который выполняется позже?
Давайте проверим официальную документацию Spring:
Просто переведите: (Этот английский перевод немного сложен, я предлагаю вам прочитать исходный текст)
Что происходит, когда несколько советов выполняются в одной и той же точке соединения? Spring AOP следует тем же правилам приоритета, что и AspectJ, для определения порядка выполнения рекомендаций. может быть достигнуто путемorg.springframework.core.Ordered
интерфейс или использование@Order
Аннотация для контроля порядка ее выполнения. Совет с наивысшим приоритетом запускается первым «при входе», а при «выходе» из точки соединения совет с наивысшим приоритетом запускается последним.
Как это следует понимать?
Думайте о Spring AOP как о концентрическом круге. Усовершенствованный исходный метод находится в центре круга, и каждый слой АОП должен добавлять новый концентрический круг. В то же время наивысший приоритет у самого внешнего слоя. При вызове метода выполните методы around и before в порядке AOP1 и AOP2 с самого внешнего уровня, затем выполните метод метода и, наконец, выполните метод after в порядке AOP2 и AOP1..
Суммировать
Когда @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 какие проблемы вызовет этот метод?
Добро пожаловать, чтобы обратить внимание на общественный номер, учиться и расти вместе.