[Спросите, если вы не понимаете] Кэш первого уровня MyBatis по-прежнему будет вызывать проблемы?

задняя часть MyBatis
[Спросите, если вы не понимаете] Кэш первого уровня MyBatis по-прежнему будет вызывать проблемы?

«Спроси, если не понимаешь», — это новая серия, предназначенная, в основном, для того, чтобы разобраться с некоторыми из наиболее интересных/сложных/легко обсуждаемых проблем, возникающих в моей группе буклетов, а также дать анализ и решения проблем и т. д. Друзья, которым это нравится, могут поставить лайк и подписаться на меня ~ ~ Чтобы узнать исходный код, вы можете посмотреть мой буклет ~ ~]

Во время праздника Праздника лодок-драконов, я полагаю, что многие мои друзья тайно учатся (они договорились играть вместе во время праздника, но учатся за моей спиной), нет, сразу после Праздника лодок-драконов, некоторые люди в одном из моих кружки программистов скульптуры из песка обсуждают проблему.Сейчас этот вопрос кажется очень хлопотным для обсуждения, но на самом деле проблема очень проста, давайте обсудим ее ниже.

оригинальный вопрос

Кэш первого уровня 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/…

[Я видел все это здесь. Ребята, вы хотите обратить внимание и вам понравилось? Если вам нужно изучить исходный код, вы можете прочитать мой буклет, изучите его ~ Олли]