【«Спроси, если не понимаешь», — это новая серия, предназначенная, в основном, для того, чтобы разобраться с некоторыми из наиболее интересных/сложных/легко обсуждаемых проблем, возникающих в моей группе буклетов, а также дать анализ и решения проблем и т. д. Друзья, которым это нравится, могут поставить лайк и подписаться на меня ~ ~ Чтобы узнать исходный код, вы можете посмотреть мой буклет ~ ~]
Во время праздника Праздника лодок-драконов, я полагаю, что многие мои друзья тайно учатся (они договорились играть вместе во время праздника, но учатся за моей спиной), нет, сразу после Праздника лодок-драконов, некоторые люди в одном из моих кружки программистов скульптуры из песка обсуждают проблему.Сейчас этот вопрос кажется очень хлопотным для обсуждения, но на самом деле проблема очень проста, давайте обсудим ее ниже.
оригинальный вопрос
Кэш первого уровня MyBatis конфликтует с декларативными транзакциями SpringFramework? Откройте транзакцию в Сервисе и запросите одни и те же данные дважды подряд, но результаты двух запросов несовместимы.
- Использование картографаselectById
Найдите объект, затем измените значение атрибута объекта, а затемselectById
Найдите сущность, сравните ее с предыдущей и обнаружите, что обнаруженная сущность — это та, которая была только что изменена, а не из базы данных.
- Если транзакция не открыта, результаты двух запросов совпадают, и консоль дважды печатает SQL.
первоначальный анализ
Справедливости ради, увидев эту проблему, я сразу догадался, что это проблема многократного чтения кеша первого уровня MyBatis.
Кэш первого уровня MyBatis включен по умолчанию и принадлежит
SqlSession
сфера. Во время открытия транзакции один и тот же запрос запроса к базе данных будет запрашивать базу данных только один раз, а затем будут получены повторные запросы из кэша первого уровня. Когда транзакция не открыта, одни и те же несколько запросов к базе данных будут отправлять запросы к базе данных.
Все вышеперечисленное — это базовые знания, не так много объяснений. Дело в том, что объект, который он модифицирует,Запрос непосредственно из кеша первого уровня MyBatis. Все мы знаем, что эти запрошенные сущности должны принадлежать объектам, и мы получаем ссылку на объект, если мы изменим его в Сервисе, это повлияет на соответствующие объекты в кеше первого уровня. Видно, что основную причину этой проблемы найти несложно.
Повторение проблемы
Чтобы продемонстрировать эту проблему, давайте просто воспроизведем сцену.
Инженерное сооружение
Мы используем SpringBoot+mybatis-spring-boot-starter
Быстро собрать проект, где версия SpringBoot 2.2.8,mybatis-spring-boot-starter
Версия 2.1.2.
pom
Есть 3 основные зависимости pom:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.199</version>
</dependency>
Конфигурация базы данных
Для базы данных мы по-прежнему выбираем h2 в качестве базы данных для быстрого повторения проблемы.Нам нужно только добавить следующую конфигурацию в application.properties для инициализации базы данных h2. Кстати, давайте просто настроим конфигурацию MyBatis:
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:mybatis-transaction-cache
spring.datasource.username=sa
spring.datasource.password=sa
spring.datasource.platform=h2
spring.datasource.schema=classpath:sql/schema.sql
spring.datasource.data=classpath:sql/data.sql
spring.h2.console.settings.web-allow-others=true
spring.h2.console.path=/h2
spring.h2.console.enabled=true
mybatis.type-aliases-package=com.linkedbear.demo.entity
mybatis.mapper-locations=classpath:mapper/*.xml
Инициализировать базу данных
Выше мы использовалиdatasource
Схема и данные инициализируют базу данных, поэтому, естественно, должны быть эти два файла .sql.
schema.sql
:
create table if not exists sys_department (
id varchar(32) not null primary key,
name varchar(32) not null
);
data.sql
:
insert into sys_department (id, name) values ('idaaa', 'testaaa');
insert into sys_department (id, name) values ('idbbb', 'testbbb');
insert into sys_department (id, name) values ('idccc', 'testccc');
insert into sys_department (id, name) values ('idddd', 'testddd');
написать тестовый код
Мы используем одну из самых простых одностоловых моделей для быстрого воспроизведения сцены.
entity
Создайте новый класс отдела и объявите свойства id и name:
public class Department {
private String id;
private String name;
// getter setter toString ......
}
mapper
Метод динамического прокси интерфейса MyBatis может быстро объявить оператор запроса, нам нужно объявить только одинfindById
Просто:
@Mapper
public interface DepartmentMapper {
Department findById(String id);
}
mapper.xml
Соответственно интерфейсу в качестве ответа нужен xml: (аннотированный Mapper здесь не используется)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.linkedbear.demo.mapper.DepartmentMapper">
<select id="findById" parameterType="string" resultType="department">
select * from sys_department where id = #{id}
</select>
</mapper>
service
Внедрите Mapper в сервис и напишите транзакцию, которая требует транзакцийupdate
метод, который имитирует действие обновления:
@Service
public class DepartmentService {
@Autowired
DepartmentMapper departmentMapper;
@Transactional(rollbackFor = Exception.class)
public Department update(Department department) {
Department temp = departmentMapper.findById(department.getId());
temp.setName(department.getName());
Department temp2 = departmentMapper.findById(department.getId());
System.out.println("两次查询的结果是否是同一个对象:" + temp == temp2);
return temp;
}
}
controller
Внедрите службу в контроллер и вызовите службуupdate
метод запуска теста:
@RestController
public class DepartmentController {
@Autowired
DepartmentService departmentService;
@GetMapping("/department/{id}")
public Department findById(@PathVariable("id") String id) {
Department department = new Department();
department.setId(id);
department.setName(UUID.randomUUID().toString().replaceAll("-", ""));
return departmentService.update(department);
}
}
основной стартовый класс
В основном классе запуска ничего особенного не нужно, просто не забудьте запустить транзакцию:
@EnableTransactionManagement
@SpringBootApplication
public class MyBatisTransactionCacheApplication {
public static void main(String[] args) {
SpringApplication.run(MyBatisTransactionCacheApplication.class, args);
}
}
запустить тест
Запустите основной класс запуска SpringBoot в режиме отладки и введите в браузереhttp://localhost:8080/h2
Введите прямо сейчасapplication.properties
Конфигурацию, заявленную в , можно открыть в консоли управления базой данных h2.
воплощать в жизньSELECT * FROM SYS_DEPARTMENT
, вы можете обнаружить, что данные были успешно инициализированы:
Следующий тестовый эффект, введите в браузереhttp://localhost:8080/department/idaaa
, результат, напечатанный в консоли, будетtrue, что доказывает, что кэш первого уровня MyBatis действителен, а объекты класса сущностей, полученные двумя запросами, согласованы.
решение
Решение этой проблемы, по сути, заключается в отключении кеша первого уровня. Наиболее распространенные варианты перечислены ниже:
- Глобальное отключение: установлено
mybatis.configuration.local-cache-scope=statement
- Указать маппер выкл: в
mapper.xml
по указанному заявлениюflushCache="true"
- Альтернативный метод: добавить строку случайных чисел в SQL оператора (слишком неосновной...)
select * from sys_department where #{random} = #{random}
Расширение принципа
На самом деле здесь проблема решена, но не переживайте, задумайтесь над вопросом: зачем вы это заявили?local-cache-scope
заstatement
или установить в теге оператора mapperflushCache=true
, кэш первого уровня отключен? Давайте посмотрим на обоснование этого.
Принцип инвалидации кеша первого уровня
существуетDepartmentService
в, выполнитьmapper.findById
действие в конечном итоге войдет вDefaultSqlSession
изselectOne
середина:
public <T> T selectOne(String statement) {
return this.selectOne(statement, null);
}
@Override
public <T> T selectOne(String statement, Object parameter) {
// Popular vote was to return null on 0 results and throw exception on too many.
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
} else {
return null;
}
}
видимыйselectOne
Нижний слой называетсяselectList
,послеget(0)
Выньте первую часть данных и вернитесь.
selectList
Нижний слой будет состоять из двух шагов:ПолучатьMappedStatement
→ выполнить запрос, в следующем кодеtry
часть:
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
воплощать в жизньquery
метод, давайBaseExecutor
, он выполняет три шага:Получить предварительно скомпилированный SQL → создать ключ кэша → реальный запрос.
public <E> List<E> query(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
Кэш-ключ здесь имеет определенную конструкцию, и его структуру можно просто рассматривать как"stateId + SQL + параметры"По этим трем элементам можно однозначно определить результат запроса.
прибыл сюдаquery
метод, он берет этот ключ кэша и выполняет действие реального запроса, как показано в следующем длинном исходном коде: (обратите внимание на комментарии в исходном коде)
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
// 如果statement有设置flushCache="true",则查询之前先清理一级缓存
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
}
List<E> list;
try {
queryStack++;
// 先检查一级缓存
list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 如果一级缓存中有,则直接取出
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 一级缓存没有,则查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
} finally {
queryStack--;
}
if (queryStack == 0) {
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
}
// issue #601
deferredLoads.clear();
// 如果全局配置中有设置local-cache-scope=statement,则清除一级缓存
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
// issue #482
clearLocalCache();
}
}
return list;
}
В приведенных выше комментариях можно найти, что пока одно из трех решений выше настроено, кеш первого уровня будет недействительным, и анализируйте их отдельно:
- Глобальные настройки
local-cache-scope=statement
, то даже если он будет помещен в кеш первого уровня после запроса, он будет очищен сразу после сохранения, а база будет проверена в следующий раз; - настройки выписки
flushCache="true"
, перед запросом необходимо очистить кеш первого уровня или проверить базу данных; - Установите случайное число.Если верхний предел случайного числа достаточно велик, вероятность случайного получения одного и того же числа достаточно мала, и это также можно рассматривать как разные запросы к базе данных.Кэшированные ключи разные, и, естественно, это будет не соответствует кешу.
Весь исходный код, описанный в этой статье, можно найти на GitHub:GitHub.com/linked bear/…
[Я видел все это здесь. Ребята, вы хотите обратить внимание и вам понравилось? Если вам нужно изучить исходный код, вы можете прочитать мой буклет, изучите его ~ Олли]