0. Предисловие
Спасибо, что нашли время в его занятости, чтобы прочитать мои заметки. Если у вас есть неправильное место, возьми проблемы в критику. Если у вас есть место, и я не согласен, я рад, что вы изучаете. Наконец, если вы найдете эту полезную ноту для вас, проблемы с похвалом, спасибо ~
1. Введение
Транзакции базы данных существуют, чтобы гарантировать «атомарность» «множественных операций с базой данных». В простейшем бизнес-сценарии банковских переводов A переводит 1000 юаней B. Существует два основных действия по денежному переводу: ① вычет 1000 юаней с банковского счета А и ② увеличение на 2000 юаней с банковского счета Б. Если операция ① завершится успешно, а операция ② завершится неудачно, то счет A потеряет 1000 юаней напрасно, а счет B не увеличится на 1000. Таким образом, нам нужно использовать технологию, чтобы обеспечить атомарность операции ① и операции ② в целом (то есть, пусть операции ① и ② либо завершатся успешно, либо потерпят неудачу одновременно), база данныхделаОн был рожден для этого.
Когда мы используем среду Springboot для разработки, Springboot уже помог нам инкапсулировать операции над базовыми транзакциями базы данных, снижая стоимость обучения и эксплуатации транзакций базы данных. В этой заметке просто описано, как настраивать и использовать транзакции в среде Springboot (версия Springboot 2.3.1.RELEASE, которая интегрирует mybatis, а база данных использует MySQL), а также подводные камни, возникающие при использовании транзакций Springboot.
2. Три технологии Springboot для реализации поддержки транзакций
SpringBoot хочет использовать метод использования транзакций базы данных, просто нужно добавить @transactional в соответствующий метод. (Конфигурация для каждого параметра @transactional аннотации,выйти в онлайнили посмотрите комментарии к исходному коду @Transactional. )
Springboot имеет 3 технических способа включить метод с @Transactional для использования транзакций базы данных, а именно «динамический прокси (переплетение во время выполнения)», «переплетение во время компиляции» и «переплетение во время загрузки класса». Эти три технологии основаны на идее АОП (аспектно-ориентированного программирования). (Я читал много статей в Интернете, и все называют АОП технологией. На самом деле АОП относится не к технологии конкретно, а к разновидности технологии.парадигма программирования, основанный на парадигме программирования АОП, разные языки программирования имеют свои реализации. )
Давайте поговорим о том, как настроить Springboot для поддержки транзакций открытия базы данных @Transactional на основе «динамического прокси» и «переплетения во время компиляции (с использованием AspectJ)» соответственно. (Основываясь на методе «динамического прокси» (поддерживающего @Transactional), будут некоторые ямки, на которые нужно обратить внимание, которые будут указаны позже.)
2.1. Поддержка @Transactional на основе динамического прокси
2.1.1 Конфигурация
-
Добавьте зависимость spring-tx в pom
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.7.RELEASE</version> </dependency>
-
Включите в приложении SpringBoot поддержку транзакций с помощью аннотаций (вы также можете настраивать классы с помощью xml или Java, но это не так быстро, как использование аннотаций). Аннотировано @EnableTransactionManagement (из представленного выше пакета spring-tx)
@SpringBootApplication @EnableTransactionManagement public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
Spring рекомендует добавить @EnableTransactionManagement к классу, аннотированному @Configuration, в то время как @SpringBootApplication аннотируется с помощью @SpringBootConfiguration, а @SpringBootConfiguration — это @Configuration, поэтому здесь мы можем добавить аннотацию @EnableTransactionManagement к классу, аннотированному с помощью @SpringBootApplication Superior.
2.1.2. Тестирование
-
Создайте TransactionController для тестирования
package com.huang.spring.practice.transaction; import com.huang.spring.practice.user.dao.UserMapper; import com.huang.spring.practice.user.dto.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * */ @RestController @RequestMapping("/api/transaction") public class TransactionController { @Autowired private UserMapper userMapper; /** * 测试Spring事务 * 插入一个新user之后,故意抛出运行时异常,Spring事务会回滚,插入user失败 */ @Transactional @PostMapping("/testTransactionThrowExcep") public User testTransactionThrowExcep() { User user = new User(); user.setName("小李"); user.setAge((short) 13); user.setCity("北京"); userMapper.insert(user); throw new RuntimeException("故意抛出一个运行时异常"); } /** * 测试Spring事务 * 成功插入user */ @Transactional @PostMapping("/testTransactionNoExcep") public User testTransactionNoExcep() { User user = new User(); user.setName("小李"); user.setAge((short) 13); user.setCity("北京"); userMapper.insert(user); return user; } }
-
Интерфейс первого вызова/api/транзакции/testTransactionThrowExcep
С тех пор, как мы последним testtransactionthrowexcep Интерфейс бросает runtimeexception, поэтому интерфейс возвращает 500.
Потому что выброшено исключение, транзакция в интерфейсе testTransactionThrowExcep будет откатана, а информация о пользователе, которую мы вставляем «Li», не попадет в базу данных.Проверьте текущие данные в пользовательской таблице базы данных, нет «Li " data. , что указывает на то, что транзакция Spring вступает в силу
mysql> select id, name, age,city from user order by id desc; +----+--------+-----+--------+ | id | name | age | city | +----+--------+-----+--------+ | 1 | 小明 | 18 | 深圳 | +----+--------+-----+--------+ 1 row in set (0.00 sec)
- Затем вызовите интерфейс /api/transaction/testTransactionNoExcep, интерфейс будет успешно выполнен и вернет код состояния HTTP 200 и информацию о новом пользователе «Li», вставленную в базу данных:
Проверяем текущие данные в пользовательской таблице базы данных, информация пользователя "Li" успешно вставляется в пользовательскую таблицу:
mysql> select id, name, age,city from user order by id desc; +----+--------+-----+--------+ | id | name | age | city | +----+--------+-----+--------+ | 4 | 小李 | 13 | 北京 | | 1 | 小明 | 18 | 深圳 | +----+--------+-----+--------+ 2 rows in set (0.00 sec)
-
2.1.3 Яма провала транзакции
При использовании @Transactional на основе динамической поддержки прокси я столкнулся с некоторыми сценариями, когда @Transactional не действует.При его использовании следует обратить особое внимание.Лучше всего написать модульный тест для проверки добавленного вами метода @Transactional, транзакции Это работает, как мы ожидали.
Для конкретных сценариев сбоя транзакции @Transactional см. эту статью.8 причин, по которым транзакции Spring терпят неудачу!, это довольно подробно. Для двух сценариев, упомянутых в этой статье, «метод, аннотированный @Transactional, не является общедоступным» и «метод, аннотированный @Transactional, вызывается другими методами в том же классе», причина, по которой транзакция не удалась, или из-за « динамический прокси». Выше мы упоминали, что аннотация @Transactional представляет собой парадигму программирования на основе АОП в среде Spring.Метод, аннотированный @Transactional, может реализовывать транзакции базы данных с помощью технологии динамического прокси. Предположим, что в классе A есть метод a, который помечен @Transactional, но когда разрешение на доступ к методу a является закрытым, среда Spring внедряет экземпляр класса A в контейнер Spring, чтобы он стал bean-компонентом, и использует «динамический прокси-сервер». " для преобразования bean-компонента A. При добавлении частный метод будет игнорироваться, поскольку вне экземпляра вы не можете напрямую вызвать его частный метод через объект экземпляра. Например, в следующем примере метод updateUserAgeByIdTransactional службы TransactionService является закрытым и не может быть вызывается непосредственно в TransactionController:
Поэтому динамический прокси-сервер не может проксировать закрытый метод, а аннотация @Transactional, добавленная к частному методу, будет недействительной.
И проблема, заключающаяся в том, что транзакция не может вступить в силу, когда метод, аннотированный @Transactional, вызывается другими методами в том же классе, на самом деле является причиной «динамического прокси». метод внутри класса вызывается сам по себе, что эквивалентно ②. Поэтому, когда мы вызываем метод внутри класса, мы вызываем метод, аннотированный @Transactional, через экземпляр этого класса (настоящий оригинальный bean-компонент в Spring, который не был динамически проксирован), а не через Spring использует динамическое расширение прокси (поддержка аналитики аннотацию @Transactional) для вызова объекта экземпляра, поэтому, естественно, аннотация @Transactional не может вступить в силу.
public void updateUserAgeById(long userId, short age) {
updateUserAgeByIdTransactional(userId, age); //①
this.updateUserAgeByIdTransactional(userId, age); //②
}
@Transactional
public void updateUserAgeByIdTransactional(long userId, short age) {
User user = new User();
user.setId(userId);
user.setAge(age);
userMapper.updateByPrimaryKeySelective(user);
throw new RuntimeException();
}
Однако, если нам нужно разрешить размещение аннотации @Transactional в частном методе и позволить @Transactional вступить в силу при вызове метода внутри класса, мы можем использовать «переплетение во время компиляции» или «время загрузки класса». плетение», перед запуском кода улучшите методы нашего целевого класса, не используя ограничения реализации «динамического прокси». В этой статье будет рассказано о том, как использовать AspectJ для объединения методов, аннотированных @Transactional во время компиляции.
2.2. Поддержка @Transactional на основе плетения AspectJ во время компиляции
Принцип плетения AspectJ во время компиляции на самом деле представляет собой технологию динамической генерации байт-кода класса, изменения файла класса, который мы изначально хотели сгенерировать, и добавления в него кода функции, который мы хотим.
2.2.1 Конфигурация
Установите режим в @EnableTransactionManagement на AdviceMode.ASPECTJ (по умолчанию AdviceMode.PROXY, который является нашим динамическим прокси~)
package com.huang.spring.practice;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
@SpringBootApplication(scanBasePackages = {"com.huang.*"})
//@EnableTransactionManagement
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
@MapperScan({"com.huang.spring.practice"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Добавьте соответствующие зависимости в файл pom и настройте плагин maven для AspectJ (наш проект управляется maven), чтобы проект мог быть изменен и создан AspectJ во время компиляции, и файл класса, который необходимо сплести:
<!-- aspectj代码织入 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.9.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.7.RELEASE</version>
</dependency>
<!-- 添加AspectJ插件 -->
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.11</version>
<configuration>
<aspectLibraries>
<aspectLibrary>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
</aspectLibrary>
</aspectLibraries>
<complianceLevel>1.8</complianceLevel>
<source>1.8</source>
<target>1.8</target>
<showWeaveInfo>true</showWeaveInfo>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
2.2.2. Тестирование
Давайте проверим, добавим аннотацию @Transactional к частному методу и вызовем его из класса, чтобы увидеть, может ли транзакция вступить в силу.Код выглядит следующим образом:
Предоставьте интерфейс для тестирования в TransactionController /api/transaction/updateUserAgeById.
package com.huang.spring.practice.transaction.controller;
import com.huang.spring.practice.transaction.service.TransactionService;
import com.huang.spring.practice.user.dao.UserMapper;
import com.huang.spring.practice.user.dto.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/transaction")
public class TransactionController {
@Autowired
private TransactionService transactionService;
@PostMapping("/updateUserAgeById/{userId}/{age}")
public void updateUserAgeById(@PathVariable("userId") long userId, @PathVariable("age") short age) {
transactionService.updateUserAgeById(userId, age);
}
}
Метод TransactionService.updateUserAgeById выглядит следующим образом. Частный метод updateUserAgeByIdTransactional с аннотацией @Transactional вызывается посредством внутреннего самовызова класса. Этот метод обновит возраст пользователя с указанным идентификатором и выдаст исключение RuntimeException в конце. метода. Если мы вызовем /api/transaction/updateUserAgeById, возраст пользователя будет обновлен, что говорит о том, что гражданские дела не вступили в силу, иначе транзакция вступила в силу.
package com.huang.spring.practice.transaction.service;
import com.huang.spring.practice.user.dao.UserMapper;
import com.huang.spring.practice.user.dto.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
@Service
public class TransactionService {
@Autowired
private UserMapper userMapper;
public void updateUserAgeById(long userId, short age) {
this.updateUserAgeByIdTransactional(userId, age);
}
@Transactional
private void updateUserAgeByIdTransactional(long userId, short age) {
User user = new User();
user.setId(userId);
user.setAge(age);
userMapper.updateByPrimaryKeySelective(user);
throw new RuntimeException();
}
}
Глядя на таблицу пользователей базы данных, возраст пользователя «Сяо Мин» с id = 1 составляет 11 лет:
Запустите приложение:
Если вы запускаете приложение в IDEA для тестирования, транзакция метода, аннотированная @Transactional, по-прежнему не вступает в силу.Предполагается, что «код», используемый IDEA для запуска приложения, не был вплетен в период компиляции AspectJ. , который обычно умен и сообразителен, в этот раз ошибся.
Таким образом, мы должны использовать команду maven для сборки проекта и распечатать пакет jar.Когда maven создаст фазу компиляции нашего проекта, мы вызовем аспектj-maven-plugin в соответствии с нашей конфигурацией в файле pom для включения в компиляцию период:
maven package
После завершения сборки maven создайте пакет jar нашего приложения Springboot в целевом каталоге проекта:
Чтобы убедиться, что AspectJ вплел транзакцию в метод с помощью @Transactional, мы можем использовать инструмент декомпиляции для декомпиляции пакета jar, который мы только что набрали, чтобы увидеть, был ли включен TransactionService.java. Инструмент декомпилятора, я использую "java-декомпилятор", он еще очень полезен, если вам интересно, вы можете перейти к ихОфициальный сайтСкачайте и играйте.
После декомпиляции пакета приложения вы можете увидеть, что TransactionService.java был обработан подключаемым модулем AspectJ для создания трех файлов классов.
Код для просмотра TransactionService.class выглядит следующим образом:
package com.huang.spring.practice.transaction.service; import com.huang.spring.practice.user.dao.UserMapper; import com.huang.spring.practice.user.dto.User; import java.io.PrintStream; import org.aspectj.lang.JoinPoint.StaticPart; import org.aspectj.runtime.internal.Conversions; import org.aspectj.runtime.reflect.Factory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.aspectj.AbstractTransactionAspect; import org.springframework.transaction.aspectj.AnnotationTransactionAspect; @Service public class TransactionService { @Autowired private UserMapper userMapper; private static final JoinPoint.StaticPart ajc$tjp_0; private static final JoinPoint.StaticPart ajc$tjp_1; private static void ajc$preClinit() { Factory localFactory = new Factory("TransactionService.java", TransactionService.class);ajc$tjp_0 = localFactory.makeSJP("method-execution", localFactory.makeMethodSig("1", "testTransactionModifyDto", "com.huang.spring.practice.transaction.service.TransactionService", "", "", "", "com.huang.spring.practice.user.dto.User"), 17);ajc$tjp_1 = localFactory.makeSJP("method-execution", localFactory.makeMethodSig("1", "updateUserAgeByIdTransactional", "com.huang.spring.practice.transaction.service.TransactionService", "long:short", "userId:age", "", "void"), 38); } public void updateUserAgeById(long userId, short age) { updateUserAgeByIdTransactional(userId, age); updateUserAgeByIdTransactional(userId, age); } @Transactional public void updateUserAgeByIdTransactional(long userId, short age) { long l = userId; short s = age; Object[] arrayOfObject = new Object[3]; arrayOfObject[0] = this; arrayOfObject[1] = Conversions.longObject(l); arrayOfObject[2] = Conversions.shortObject(s); AnnotationTransactionAspect.aspectOf().ajc$around$org_springframework_transaction_aspectj_AbstractTransactionAspect$1$2a73e96c(this, new TransactionService.AjcClosure3(arrayOfObject), ajc$tjp_1); } static final void updateUserAgeByIdTransactional_aroundBody2(TransactionService ajc$this, long userId, short age) { User user = new User(); user.setId(Long.valueOf(userId)); user.setAge(Short.valueOf(age)); ajc$this.userMapper.updateByPrimaryKeySelective(user); throw new RuntimeException(); } static {} }
Вы можете видеть, что код в TransactionService.class был вплетен AspectJ. Как упоминалось выше, AspectJ использует технологию «динамического создания байт-кода класса», чтобы помочь нам автоматически в месте, указанном в коде. Измените сгенерированный байт-код класса, чтобы "улучшить" код, как мы ожидаем. Это действительно освобождает нам много рабочей силы и снижает сложность разработки.Если код, автоматически сгенерированный AspectJ выше, должен быть написан нами вручную, это будет утомительно.
Хорошо, теперь давайте запустим наше приложение с типизированным пакетом jar. Мы не запускаем проект в IDEA, а напрямую используем команду java на локальном компьютере для запуска пакета приложения, который мы только что собрали с помощью maven:
java -jar spring.practice-0.0.1-SNAPSHOT.jar
После запуска приложения интерфейс вызывается для обновления возраста «Сяо Мин» до 66, и интерфейс возвращает 500, потому что в нашем интерфейсе возникает исключение RuntimeException. Давайте проверим пользовательскую таблицу базы данных, чтобы увидеть, соответствует ли возраст "Сяо Мин" нет. Он был обновлен, чтобы мы могли знать, действует ли наша аннотация @Transactional,
Bingo ~, обновив данные пользовательского стола, возраст Сяоминга по-прежнему 11, указывая на то, что наша @transactional аннотация действует, и способ поддержки @transactional аннотации, поддерживаемого ASPECTJ. Мы добавим. Мы добавим @Transactiona к методу в будущем. Вам не нужно учитывать права доступа (частным) метода и называется метод из в пределах класса, когда он называется!
2.3 Яма, на которую стоит обратить внимание
В процессе использования транзакции Springboot+MyBatis+ (@Transactional) обнаружил небольшую ямку:
В транзакции Spring запросите определенный фрагмент данных из базы данных, верните java-объект этих данных, значение переменной-члена A этого java-объекта равно a, а затем измените значение переменной-члена A этого java-объекта. на b, но после модификации не обновляет измененный результат в базе данных. Затем в той же транзакции снова запросите эти данные, и значение переменной-члена A объекта java возвращаемых данных фактически будет ранее измененным значением b. Конкретный тестовый код выглядит следующим образом:
2.3.1.1 Воспроизведение сцены
-
Добавьте интерфейс testTransactionModifyDto в упомянутый выше TransactionController.
package com.huang.spring.practice.transaction.controller; import com.huang.spring.practice.transaction.service.TransactionService; import com.huang.spring.practice.user.dao.UserMapper; import com.huang.spring.practice.user.dto.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/transaction") public class TransactionController { @Autowired private TransactionService transactionService; @PostMapping("/testTransactionModifyDto") public User testTransactionModifyDto() { transactionService.testTransactionModifyDto(); User user2 = userMapper.selectByPrimaryKey(4L); System.out.println("在事务外,从DB查询id为4的用户,然后打印他的age : " + user2.getAge()); return user2; } }
-
Создайте TransactionService и добавьте метод testTransactionModifyDto() с аннотацией транзакции @Transactional.
package com.huang.spring.practice.transaction.service; import com.huang.spring.practice.user.dao.UserMapper; import com.huang.spring.practice.user.dto.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class TransactionService { @Autowired private UserMapper userMapper; @Transactional public User testTransactionModifyDto() { User user = userMapper.selectByPrimaryKey(4L); System.out.println("在事务中,从DB查询id为4的用户,然后打印他的age : " + user.getAge()); user.setAge((short) 80); System.out.println("在事务中,将id为4的用户的age设置为80, 不执行update命令将其更新到数据库中。"); user = userMapper.selectByPrimaryKey(4L); System.out.println("在事务中,从DB查询id为4的用户,然后再打印他的age : " + user.getAge()); return user; } }
-
Вызовите интерфейс /api/transaction/testTransactionModifyDto, и журналы, напечатанные во время вызова интерфейса, будут следующими:
在事务中,从DB查询id为4的用户,然后打印他的age : 13 在事务中,将id为4的用户的age设置为80, 不执行update命令将其更新到数据库中。 在事务中,从DB查询id为4的用户,然后再打印他的age : 80 在事务外,从DB查询id为4的用户,然后打印他的age : 13
-
2.3.2.2. Местоположение проблемы
Ничего не говорите, отладка завершена.
Сначала отладьте эту строку кода в красном поле:
Пришла построчная отладка и обнаружила, что в методе запроса кода BaseExecutor.java MyBatis, под номером 1 на рисунке, он пойдет в localCache, чтобы попытаться получить результат выполнения текущего sql, если есть результат выполнения текущего sql, он вернет ключ текущего результата (списка) на рисунке 2, если список пуст, выполните метод queryFromDatabase на рисунке 3, чтобы выполнить результат запроса sql в базе данных.
Давайте посмотрим, что хранится в CacheKey, используемом для запроса кеша.
Как показано на рисунке выше, cachekey хранит информацию Mapper о выполняемом SQL, операторе SQL и входных параметрах SQL. Так что сделайте смелое предположение:
После запроса sql Mybatis будет использовать «информацию Mapper, оператор sql и входные параметры sql» в качестве ключа кеша, кэшировать результат выполнения этого sql, а затем выполнять его в следующий раз, если есть этот кеш результата выполнения SQL , его можно использовать напрямую.
Однако кэш результатов выполнения sql, хранящийся в localCache, должен храниться только в определенной области, иначе во время работы приложения мы каждый раз выполняем следующий sql, запрашиваем информацию о пользователе с id=4 и возвращаем запрос Если результат получен из localCache, результаты запроса будут каждый раз одинаковыми, что сбивает с толку, поэтому кеш результата выполнения, сохраненный в localCache определенного SQL, должен быть допустимым в ограниченной области.
User user = userMapper.selectByPrimaryKey(4L);
Далее нам нужно выяснить, когда кэш в localCache добавляется, а когда удаляется.
Мы видим, что localCache — это экземпляр с именем PerpetualCache.
Войдя в класс PerpatualCache, вы увидите, что кэш хранится в именованном кэше HashMap.
Мы добавляем точки останова к трем методам putObject, removeObject и clear, которые работают с переменными кеша в классе PerpatualCache, а затем снова отлаживаем или отлаживаем процесс запроса sql в красном поле на рисунке ниже.
При отладке метода putObject класса PerpatualCache мы видим стек вызовов метода и ясно видим, что после выполнения метода queryFromDatabase класса BaseExecutor результат, запрошенный из базы данных, будет сохранен в localCache (PrepetualCache)
Затем продолжите отладку до второго красного поля на рисунке ниже (такого же, как sql в первом красном поле).В течение этого периода метод очистки PerpatualCache не вызывался, указывая результат запроса в первом красном поле. кеш тоже сохраняется в PerpatualCache, продолжаем отладку во второй красный квадратик на рисунке ниже
SQL-запрос во втором красном поле, как мы и ожидали выше, напрямую получает результат запроса в первом красном поле из localCache и возвращает,
Здесь мы можем объяснить явление, упомянутое выше:
В транзакции Spring запросите определенный фрагмент данных из базы данных, верните java-объект этих данных, значение переменной-члена A этого java-объекта равно a, а затем измените значение переменной-члена A этого java-объекта. на b, но после модификации не обновляет измененный результат в базе данных. Затем в той же транзакции снова запросите эти данные, и значение переменной-члена A объекта java возвращаемых данных фактически будет ранее измененным значением b.
Кратко нарисуйте картинку, чтобы описать:
①: Запросить пользовательские данные с id=4 из базы данных и открыть пространство памяти в куче JVM для хранения пользовательских данных.
②③: После нахождения данных в базе данных mybatis кэширует результаты в PrepetualCache (localCache), а PrepetualCache имеет указатель на адрес памяти, где находятся пользовательские данные.
④: пользовательская переменная в методе TestTransactionModifydto указывает на адрес памяти пользовательских данных, найденных в базе данных и хранится в куче на шаге ①.
⑤: Возрастная куча с пользовательскими переменными данными в пользователь 80
⑥⑦: В той же транзакции транзакция не была отправлена, поэтому кэш в PrepetualCache текущего потока не был очищен, выполнить тот же sql и получить память в куче последних запрошенных пользовательских данных из PrepetualCache. адрес, пользовательская переменная в методе testTransactionModifyDto снова указывает на этот адрес памяти
Дальше продолжаем отлаживать и выходим из этого метода в транзакцию
Будет вызван метод очистки PerpetualCache (здесь мы заранее установили точку останова), чтобы очистить все кеши. Глядя на стек вызовов метода, можно увидеть, что вызывается метод фиксации многих классов, потому что выполнение метода транзакции закончилось, а spring будет отправлять sql во время транзакции в базу данных, чтобы наши операции с данными во время транзакции в конечном итоге попадет на БД.
На самом деле, в этом процессе отладки можно сказать много чего, например, PrepetualCache принадлежит фреймворку Mybatis, однако, когда транзакция, принадлежащая фреймворку Spring, заканчивается, вызывается метод clear PrepetualCache фреймворка Mybatis. .Вот пусть код фреймворка Spring Как реализован код, который вызывает фреймворк Mybatis? (В ответ на эту проблему я преднамеренно провел отладку и обнаружил, что она была реализована с помощью Mybatis SqlSessionUtils и Spring TransactionSynchronizationManager и TransactionSynchronizationUtils. Конкретный процесс немного громоздкий и трудоемкий, чтобы выразить его словами. Если вам интересно, вы можете отлаживать и см., вы можете найти Spring. Чтобы интегрировать другие структуры уровня сохраняемости, транзакция предоставляет интерфейс TransactionSynchronization, сторонний фреймворк уровня сохраняемости реализует этот интерфейс и регистрирует свою собственную реализацию в синхронизациях в Spring TransactionSynchronizationManager, чтобы Spring мог пройти сторонняя структура уровня сохраняемости используется для обработки транзакций.Схожие методы, если вы обратите внимание, нетрудно обнаружить, что многие проектные программы будут использовать его.Предоставляя интерфейсы, реализацию и код структуры в различных ситуациях развязаны, а затем, в соответствии с реальной ситуацией, зарегистрируйте соответствующую реализацию в фреймворке. Мы можем узнать больше об этом навыке кодирования (идее), который очень полезен для создания надежного и легко поддерживаемого проекта.)
2.3.2.3. Обращение
Если вы хотите избежать влияния LocalCache MyBatis, пусть результаты SQL (statment) в том же SQLSession не кэшируются локальным кэшем, вы можете установить localcachescope myBatis на оператор, см.Официальная документация MyAtbis:
Setting Description Valid Values Default localCacheScope MyBatis uses local cache to prevent circular references and speed up repeated nested queries. By default (SESSION) all queries executed during a session are cached. If localCacheScope=STATEMENT local session will be used just for statement execution, no data will be shared between two different calls to the same SqlSession. SESSION | STATEMENT SESSION
Конкретная конфигурация выглядит следующим образом (конфигурация mybatis настраивается в виде файла конфигурации Springboot здесь, и вы также можете настроить ее в коде Java):
mybatis:
configuration:
local-cache-scope: statement
После того, как тест настроен таким образом, localCache не вступит в силу.
Если вы не хотите задавать область действия оператора mybatis localCache to и хотите избежать сценария, описанного в разделе 2.3.1.1 этой статьи, вы можете использовать метод глубокого копирования объектов.Конкретный код выглядит следующим образом. :
@Transactional
public User testTransactionModifyDto() {
User user = userMapper.selectByPrimaryKey(4L);
System.out.println("在事务中,从DB查询id为4的用户,然后打印他的age : " + user.getAge());
/**
* 使用对象字节流的方式进行对象的深拷贝,
* 具体实现的代码不用自己写,网上很多开源的工具包可以拿来直接用,
* 我这里用的ObjectUtil是来自是hutool这个工具包,官网地址:https://www.hutool.cn/
*
* ObjectUtil.cloneByStream具体的源码很简单,实际上就是对ObjectOutputStream和ObjectIputStream的使用
*
* 需要注意的一点是用字节流的方式进行深拷贝的话,被拷贝的对象必须实现了Serializable接口,
* 否则无法进行序列化、反序列化,拷贝会失败。
*/
user = ObjectUtil.cloneByStream(user);
user.setAge((short) 80);
System.out.println("在事务中,将id为4的用户的age设置为80, 不执行update命令将其更新到数据库中。");
user = userMapper.selectByPrimaryKey(4L);
System.out.println("在事务中,从DB查询id为4的用户,然后再打印他的age : " + user.getAge());
return user;
}
Результат, напечатанный после модификации:
在事务中,从DB查询id为4的用户,然后打印他的age : 13
在事务中,将id为4的用户的age设置为80, 不执行update命令将其更新到数据库中。
在事务中,从DB查询id为4的用户,然后再打印他的age : 13
在事务外,从DB查询id为4的用户,然后打印他的age : 13