предисловие
Исходя из личных интересов, я поделюсь с вами особенностями кеша Mybatis первого уровня и кеша второго уровня.
- Что такое Мибатис.
- Как настроить и использовать кэши первого и второго уровня Mybatis.
- Анализ рабочего процесса и исходного кода кэшей Mybatis уровня 1 и уровня 2.
Код и таблицы базы данных, задействованные в этом анализе, размещены на Github по адресу:mybatis-cache-demo.
содержание
Для достижения вышеуказанных трех целей данная статья развивается в следующем порядке.
- Основная концепция Mybatis.
- Введение в кэш первого уровня и связанная с ним конфигурация.
- Рабочий процесс кэширования уровня 1 и анализ исходного кода.
- Сводка кэша L1.
- Введение в кэш L2 и связанная с ним конфигурация.
- Анализ исходного кода кэша L2.
- Сводка кэша L2.
- Полный текст резюме.
Основные концепции Mybatis
В этой главе дается общее введение в Mybatis, которое разделено на официальные определения и введение основных компонентов.
Первое — это официальное определение Mybatis, как показано ниже.
MyBatis — это превосходная структура уровня сохраняемости, которая поддерживает пользовательский SQL, хранимые процедуры и расширенное сопоставление. MyBatis избегает почти всего кода JDBC и ручной настройки параметров и получения наборов результатов. MyBatis может использовать простой XML или аннотации для настройки и собственную карту для сопоставления интерфейсов и Java POJO (обычные старые объекты Java, обычные объекты Java) с записями в базе данных.
Затем следует несколько основных концепций Mybatis.
- SqlSession : представляет сеанс с базой данных, предоставляя пользователям методы для управления базой данных.
- MappedStatement: представляет инструкцию для отправки в базу данных для выполнения, которую можно понимать как абстрактное представление Sql.
- Исполнитель: исполнитель, специально используемый для взаимодействия с базой данных, принимает MappedStatement в качестве параметра.
- Интерфейс сопоставления: Sql, который должен выполняться в интерфейсе, представлен методом, а конкретный Sql записывается в файл сопоставления.
- Файл сопоставления: его можно понимать как место, где Mybatis записывает Sql, Вообще говоря, каждая отдельная таблица соответствует файлу сопоставления, в котором определены входные и выходные параметры оператора Sql.
На следующем рисунке показан интерфейсный файл StudentMapper для операций с таблицей Student. В StudentMapper мы можем использовать несколько методов. За этим методом он представляет значение Sql, которое необходимо выполнить.
Обычно методы, включающие многотабличные запросы, также могут быть определены в StudentMapper, если предметом запроса по-прежнему является информация таблицы Student. Вы также можете извлечь отдельный файл интерфейса для оператора, включающего многотабличный запрос.
После определения файла интерфейса мы разработаем файл сопоставления Sql, который в основном состоит из элементов сопоставления и элементов select|insert|update|delete, как показано на следующем рисунке.
Элемент mapper представляет, что этот файл является файлом сопоставления, который привязан к определенному интерфейсу сопоставления с использованием пространства имен, а значение namespace — это полное имя класса этого интерфейса. select|insert|update|delete представляет оператор Sql. Каждый метод, определенный в интерфейсе сопоставления, также будет привязан к оператору в файле сопоставления по идентификатору. Имя метода — это идентификатор оператора, а запись оператора будет быть определены одновременно.Параметры и выходные параметры используются для завершения преобразования между объектами Java.
Когда Mybatis инициализируется, каждый оператор будет представлен соответствующим MappedStatement, а оператор будет представлен пространством имен + идентификатором самого оператора. Как показано в следующем коде, используйте mapper.StudentMapper.getStudentById для представления соответствующего кода Sql.
SELECT id,name,age FROM student WHERE id = #{id}
Когда Mybatis будет выполнен, он войдет в метод соответствующего интерфейса, сгенерирует идентификатор через комбинацию имени класса и имени метода, найдет требуемый MappedStatement и передаст его исполнителю для использования.
На данный момент были представлены основные концепции Mybatis.
Кэш L1
Введение в кэш L1
Во время работы системного кода мы можем выполнять Sql с одними и теми же условиями запроса несколько раз в сеансе базы данных.Учитывая, что большинство сценариев ежедневных приложений больше читают и меньше пишут, этот повторный запрос приведет к определенной сети В то же время, если количество запросов на выборку относительно велико, это окажет относительно большое влияние на производительность базы данных.
Если это база данных Mysql, если поддержка предварительной компиляции включена как на стороне сервера, так и на стороне Jdbc, оператор может кэшироваться на стороне локальной JVM, а Sql может выполняться непосредственно на стороне сервера Mysql. О предварительно скомпилированном кеше Jdbc и Mysql вы можете прочитать в этом моем блоге.JDBC и Mysql эти штуки.
Mybatis предоставляет решение кэширования первого уровня для оптимизации проблемы повторяющихся запросов между сеансами базы данных. Способ реализации заключается в том, что каждый SqlSession содержит свой собственный кеш.Один из них — это уровень SESSION, то есть все операторы, выполняемые в сеансе Mybatis, будут совместно использовать этот кеш. Одним из них является уровень STATEMENT, который можно понять, поскольку кеш действителен только для текущего выполняемого оператора. Если граф используется для представления процесса запроса первого уровня, он может быть представлен следующим графом.
Каждый SqlSession содержит свой Executor, и у каждого Executor есть локальный кэш. Когда пользователь инициирует запрос, Mybatis сгенерирует ключ на основе исполняемого в данный момент MappedStatement и запросит его в локальном кэше.Если кеш сработает, он вернется. Если кеш не попал, он будет записан в локальный кеш, и, наконец, результат будет возвращен пользователю.
Конфигурация кэша L1
Выше описана реализация кэша первого уровня и какую проблему он решает. В этой главе мы узнаем, как использовать кеш первого уровня Mybatis. Вам нужно только добавить следующий оператор в файл конфигурации Mybatis, чтобы использовать кеш первого уровня. Есть два варианта: SESSION или STATEMENT, по умолчанию используется уровень SESSION.
<setting name="localCacheScope" value="SESSION"/>
Эксперимент с кешем L1
После завершения настройки изучите влияние кэша первого уровня Mybatis с помощью экспериментов. Пожалуйста, восстанавливайте измененные данные после каждого модульного теста.
Первый — создать образец таблицы student, для которого создаются соответствующие классы POJO и методы добавления и изменения, которые можно просмотреть в пакете entity и пакете Mapper.
CREATE TABLE `student` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
`age` tinyint(3) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
В следующем эксперименте ученицу с идентификатором 1 зовут Карен.
Эксперимент 1
Откройте кеш первого уровня, областью действия является уровень сеанса, и трижды вызовите getStudentById Код выглядит следующим образом:
public void getStudentById() throws Exception {
SqlSession sqlSession = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper.getStudentById(1));
System.out.println(studentMapper.getStudentById(1));
}
Результаты:
Мы видим, что только первый раз к базе данных действительно запрашивается, а последующие запросы используют кеш первого уровня.
Эксперимент 2
В этом эксперименте мы добавили операцию модификации в базу данных, чтобы проверить, будет ли кэш первого уровня недействительным, если операция модификации произойдет с базой данных в сеансе базы данных.
@Test
public void addStudent() throws Exception {
SqlSession sqlSession = factory.openSession(true); // 自动提交事务
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
System.out.println(studentMapper.getStudentById(1));
System.out.println("增加了" + studentMapper.addStudent(buildStudent()) + "个学生");
System.out.println(studentMapper.getStudentById(1));
sqlSession.close();
}
Результаты:
Мы видим, что тот же запрос, выполненный после операции модификации, запросил базу данных,Инвалидация кеша L1.
Эксперимент 3
Откройте два сеанса SqlSessions, запросите данные в sqlSession1, включите кэш первого уровня, обновите базу данных в sqlSession2 и убедитесь, что кэш первого уровня используется совместно только в рамках сеанса базы данных.
@Test
public void testLocalCacheScope() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2更新了" + studentMapper2.updateStudentName("小岑",1) + "个学生的数据");
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}
Мы видим, что sqlSession2 обновил имя студента с идентификатором 1 с Karen на Xiaocen, но в запросе после session1 имя студента с идентификатором 1 по-прежнему Karen, и появляются грязные данные, что также доказывает, что мы ранее пришел к выводу, что кэш первого уровня существует только и используется только в рамках сеанса базы данных.
Рабочий процесс кэширования уровня 1 и анализ исходного кода
В этой главе в основном изучается кэш первого уровня из рабочего процесса и уровня исходного кода кэша первого уровня.
процесс работы
Согласно рабочему процессу кэша L1 мы рисуем временную диаграмму выполнения кэша L1, как показано на следующем рисунке.
Основные шаги заключаются в следующем:
- Для оператора Select сгенерируйте ключ на основе оператора.
- Определите, существует ли ключ с соответствующими данными в локальном кэше.
- Если это произойдет, пропустите запрос к базе данных и продолжите вниз.
- Если промахнется:
4.1 去数据库中查询数据,得到查询结果; 4.2 将key和查询到的结果作为key和value,放入Local Cache中。 4.3. 将查询结果返回;
- Определите, является ли уровень кеша уровнем STATEMENT, и если да, очистите локальный кеш.
Анализ исходного кода
Поняв конкретный рабочий процесс, мы направляем Mybatis для запроса соответствующих основных классов и исходного кода кэша первого уровня для чтения. Это также полезно при дальнейшем изучении кэша второго уровня.
SqlSession: Предоставляет все методы, необходимые для взаимодействия между пользователем и базой данных для внешнего мира, скрывая основные детали. Одним из его классов реализации по умолчанию является DefaultSqlSession.
Executor: SqlSession предоставляет пользователям методы для работы с базой данных, но обязанности, связанные с операциями с базой данных, делегируются Executor.
Как показано на рисунке ниже, Executor имеет несколько классов реализации, которые наделяют Executor различными возможностями.Вы можете в частном порядке изучить основные функции каждого класса в соответствии с именем класса.
В главе о кэше первого уровня мы в основном узнаем о BaseExecutor.
BaseExecutor: BaseExecutor — это абстрактный класс, который реализует интерфейс Executor, определяет несколько абстрактных методов и делегирует определенные операции подклассам для выполнения во время выполнения.
Во введении о кеше первого уровня мы упомянули, что запрос и запись в локальный кеш выполняются внутри Executor. Прочитав код BaseExecutor, мы также обнаружили, что Local Cache является внутри него переменной-членом, как показано в следующем коде.protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException; protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException; protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException; protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException;
Cache: Интерфейс Cache в Mybatis предоставляет самые основные операции, связанные с кэшированием.Существует несколько классов реализации, которые собираются друг с другом с помощью шаблона декоратора, предоставляя богатые возможности для манипулирования кешем.public abstract class BaseExecutor implements Executor { protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads; protected PerpetualCache localCache;
PerpetualCache, одна из переменных-членов BaseExecutor, является наиболее простой реализацией интерфейса Cache.Его реализация очень проста и содержит внутреннюю хэш-карту.Операция кеша первого уровня на самом деле является операцией этой хэш-карты. Как показано в коде ниже.
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
После прочтения соответствующего кода основного класса на уровне исходного кода соответствующий код, участвующий в работе кеша первого уровня, для экономии места исходный код соответствующим образом удаляется.Читатели и друзья могут объединить эту статью для более подробного изучения. позже.
Для взаимодействия с БД сначала открывается SqlSession через DefaultSqlSessionFactory, в процессе создания SqlSession создается новый Executor через класс Configuration как параметр конструктора DefaultSqlSession, код следующий.
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
............
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
Если пользователь не вводит спецификацию, когда Configuration создает Executor, по умолчанию создается тип SimpleExecutor, который представляет собой простой класс выполнения, который просто выполняет Sql. Ниже приведен конкретный код, используемый для создания.
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
// 尤其可以注意这里,如果二级缓存开关开启的话,是使用CahingExecutor装饰BaseExecutor的子类
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
После создания SqlSession в соответствии с различными типами Statment он будет вводить различные методы SqlSession.Если это оператор Select, он, наконец, будет выполнен в списке selectList SqlSession.Код выглядит следующим образом.
@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
В приведенном выше коде SqlSession делегирует определенные обязанности по запросу Executor. Если включен только кеш первого уровня, сначала будет введен метод запроса BaseExecutor. Код показан ниже.
@Override
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);
}
В приведенном выше коде CacheKey будет сначала сгенерирован на основе входящих параметров, а затем введите этот метод, чтобы увидеть, как генерируется CacheKey.Код выглядит следующим образом.
CacheKey cacheKey = new CacheKey();
cacheKey.update(ms.getId());
cacheKey.update(rowBounds.getOffset());
cacheKey.update(rowBounds.getLimit());
cacheKey.update(boundSql.getSql());
//后面是update了sql中带的参数
cacheKey.update(value);
В приведенном выше коде мы видим, что он передает идентификатор MappedStatement, смещение sql, предел Sql, сам Sql и параметры в Sql в класс CacheKey и, наконец, генерирует CacheKey. Давайте посмотрим на структуру этого класса.
private static final int DEFAULT_MULTIPLYER = 37;
private static final int DEFAULT_HASHCODE = 17;
private int multiplier;
private int hashcode;
private long checksum;
private int count;
private List<Object> updateList;
public CacheKey() {
this.hashcode = DEFAULT_HASHCODE;
this.multiplier = DEFAULT_MULTIPLYER;
this.count = 0;
this.updateList = new ArrayList<Object>();
}
Во-первых, это его переменные-члены и конструкторы с начальным хэш-кодом и множителем при сохранении внутреннего списка обновлений. В методе обновления CacheKey будет выполнен расчет хэш-кода и контрольной суммы, а входящие параметры будут добавлены в список обновлений. Как показано в коде ниже.
public void update(Object object) {
int baseHashCode = object == null ? 1 : ArrayUtil.hashCode(object);
count++;
checksum += baseHashCode;
baseHashCode *= count;
hashcode = multiplier * hashcode + baseHashCode;
updateList.add(object);
}
Как мы судим, что CacheKey равен? Ответ нам дается в методе equals CacheKey, Код выглядит следующим образом.
@Override
public boolean equals(Object object) {
.............
for (int i = 0; i < updateList.size(); i++) {
Object thisObject = updateList.get(i);
Object thatObject = cacheKey.updateList.get(i);
if (!ArrayUtil.equals(thisObject, thatObject)) {
return false;
}
}
return true;
}
За исключением сравнения хэш-кода, контрольной суммы и количества, пока элементы в списке обновлений равны один к одному, можно считать, что CacheKey равен. Пока следующие пять значений двух Sql совпадают, их можно считать одним и тем же Sql.
Statement Id + Offset + Limmit + Sql + Params
Метод запроса BaseExecutor продолжает работать, и код выглядит следующим образом.
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);
}
Если не найдется, будет проверяться из БД, в queryFromDatabase будет записан локальный кеш.
В конце выполнения метода запроса он определит, является ли уровень кэша первого уровня уровнем STATEMENT, и если да, то кэш будет очищен, поэтому кэш первого уровня на уровне STATEMENT не может совместно использовать localCache. . Код показан ниже.
if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
clearLocalCache();
}
В конце анализа исходного кода давайте подтвердим причину, по которой кеш будет обновляться, если это метод вставки/удаления/обновления.
Метод вставки и метод удаления SqlSession будут следовать процессу обновления унифицированным образом.Код выглядит следующим образом.
@Override
public int insert(String statement, Object parameter) {
return update(statement, parameter);
}
@Override
public int delete(String statement) {
return update(statement, null);
}
Метод обновления также делегируется исполнителю для выполнения. Метод выполнения BaseExecutor показан ниже.
@Override
public int update(MappedStatement ms, Object parameter) throws SQLException {
ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
if (closed) {
throw new ExecutorException("Executor was closed.");
}
clearLocalCache();
return doUpdate(ms, parameter);
}
LocalCache очищается перед выполнением каждого обновления.
На этом объяснение рабочего процесса кэша первого уровня и анализ исходного кода завершены.
Суммировать
- Жизненный цикл кэша первого уровня Mybatis такой же, как у SqlSession.
- Кэш Mybatis является крупнозернистым кешем.Нет концепции обновления кеша и истечения срока действия кеша.В то же время он использует только хэш-карту по умолчанию и не ограничивает емкость.
- Максимальная область кэша первого уровня Mybatis находится внутри SqlSession.В нескольких SqlSession или распределенной среде, если есть операционная база данных для записи, это вызовет грязные данные.Рекомендуется установить уровень по умолчанию для первого -level cache to Statement, то есть не использовать его Кэш 1-го уровня.
Кэш L2
Введение в кэш L2
В упомянутом выше кеше первого уровня самая большая общая область находится в пределах SqlSession, поэтому как разделить кеш между несколькими SqlSessions? Ответ — это кеш второго уровня.
Когда кеш L2 включен, CachingExecutor будет использоваться для оформления Executor.Перед входом в последующее выполнение кеш L2 будет запрашиваться в CachingExecutor.Конкретный рабочий процесс выглядит следующим образом.
При использовании кэша второго уровня все операторы операций в пространстве имен влияют на один и тот же кэш, то есть кэш второго уровня совместно используется несколькими сеансами SqlSessions и является глобальной переменной.
Когда кеш включен, процесс выполнения запроса данных — это кеш второго уровня -> кеш первого уровня -> база данных.
Конфигурация кэша L2
Чтобы правильно использовать кэш L2, вам необходимо выполнить следующую настройку.
1 Включите кэш второго уровня в файле конфигурации Mybatis.
<setting name="cacheEnabled" value="true"/>
2 Настройте cache или cache-ref в XML-файле сопоставления Mybatis.
<cache/>
Тег cache используется для объявления того, что это пространство имен использует кэш второго уровня и может быть настроено.
- type: тип, используемый кешем, по умолчанию PerpetualCache, который упоминался в кеше первого уровня.
- выселение: определите стратегию утилизации, наиболее распространенными являются FIFO, LRU.
- flushInterval: настроить определенное время для автоматического обновления кеша, единица измерения — миллисекунды.
- размер: максимальное количество объектов в кэше
- readOnly: независимо от того, доступен ли он только для чтения, если конфигурация может быть прочитана и записана, соответствующий класс сущности должен быть сериализуемым.
- блокировка: если соответствующий ключ не может быть найден в кеше, будет ли он продолжать блокироваться до тех пор, пока соответствующие данные не поступят в кеш.
<cache-ref namespace="mapper.StudentMapper"/>
cache-ref представляет конфигурацию Cache, которая ссылается на другие пространства имен, и операции двух пространств имен используют один и тот же Cache.
Эксперимент с кешем L2
В этой главе путем экспериментов вы поймете некоторые особенности используемого кэша Mybatis L2.
В этом эксперименте имя ученика с идентификатором 1 инициализируется точкой.
Эксперимент 1
Проверьте действие кеша второго уровня, не отправляйте транзакцию, после того как sqlSession1 запрашивает данные, будет ли тот же запрос sqlSession2 получать данные из кеша.
@Test
public void testCacheWithoutCommitOrClose() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}
Результаты:
Мы видим, что когда sqlsession не вызывает метод commit(), кэш второго уровня не работает.
Эксперимент 2
Проверьте эффект кэша второго уровня.Когда транзакция отправлена, после того, как sqlSession1 запрашивает данные, будет ли тот же запрос sqlSession2 получать данные из кэша.
@Test
public void testCacheWithCommitOrClose() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
sqlSession1.commit();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}
Как видно из рисунка, запрос sqlsession2 использует кеш, и коэффициент попадания в кеш равен 0,5.
Эксперимент 3
Проверьте, обновит ли операция обновления кэш второго уровня в пространстве имен.
@Test
public void testCacheWithUpdate() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
SqlSession sqlSession3 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
StudentMapper studentMapper3 = sqlSession3.getMapper(StudentMapper.class);
System.out.println("studentMapper读取数据: " + studentMapper.getStudentById(1));
sqlSession1.commit();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
studentMapper3.updateStudentName("方方",1);
sqlSession3.commit();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentById(1));
}
Мы видим, что после того, как sqlSession3 обновляет базу данных и отправляет транзакцию, запросы в пространстве имен StudentMapper sqlsession2 направляются в базу данных, но не в кэш.
Эксперимент 4
Убедитесь, что кэш второго уровня Mybatis не подходит для многотабличных запросов в файле сопоставления. Вообще говоря, мы создадим отдельный файл сопоставления для каждой отдельной таблицы.Если есть запрос, включающий несколько таблиц, поскольку кеш второго уровня Mybatis основан на пространстве имен, пространство имен, в котором находится оператор запроса с несколькими таблицами, не может воспринимать другие Оператор в пространстве имен изменяет таблицы, участвующие в многотабличном запросе, вызывая проблему грязных данных.
@Test
public void testCacheWithDiffererntNamespace() throws Exception {
SqlSession sqlSession1 = factory.openSession(true);
SqlSession sqlSession2 = factory.openSession(true);
SqlSession sqlSession3 = factory.openSession(true);
StudentMapper studentMapper = sqlSession1.getMapper(StudentMapper.class);
StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);
ClassMapper classMapper = sqlSession3.getMapper(ClassMapper.class);
System.out.println("studentMapper读取数据: " + studentMapper.getStudentByIdWithClassInfo(1));
sqlSession1.close();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentByIdWithClassInfo(1));
classMapper.updateClassName("特色一班",1);
sqlSession3.commit();
System.out.println("studentMapper2读取数据: " + studentMapper2.getStudentByIdWithClassInfo(1));
}
Результаты:
В этом эксперименте мы ввели две новые таблицы, одну для класса и одну для класса. Идентификатор класса и имя класса сохраняются в классе, а идентификатор класса и идентификатор учащегося сохраняются в классе. Мы добавили метод запроса getStudentByIdWithClassInfo в StudentMapper, который используется для запроса класса студента, который включает запрос к нескольким таблицам. Добавлено updateClassName в ClassMapper для обновления имени класса в соответствии с идентификатором класса.
Когда studentmapper из sqlsession1 запрашивает данные, вступает в силу вторичный кэш. Сохранено в кеше под пространством имен StudentMapper. Когда метод updateClassName classMapper из sqlSession3 обновляет таблицу классов, updateClassName не принадлежит пространству имен StudentMapper, поэтому кеш в StudentMapper не воспринимает изменения и не обновляет кеш. Когда тот же запрос в StudentMapper выдается снова, грязные данные считываются из кеша.
Эксперимент 5
Чтобы решить проблему эксперимента 4, вы можете использовать Cache ref, чтобы позволить ClassMapper ссылаться на пространство имен StudentMapper, чтобы операции Sql, соответствующие двум файлам сопоставления, использовали один и тот же кеш.
Результаты:
Однако следствием этого является то, что гранулярность кеша становится более грубой, и все операции в нескольких пространствах имен Mapper будут влиять на использование кеша.На самом деле существование этого кеша не имеет большого значения.
Анализ исходного кода кэша L2
Рабочий процесс кеша второго уровня Mybatis аналогичен упомянутому выше кешу первого уровня, но перед обработкой кеша первого уровня подкласс BaseExecutor украшается CachingExecutor для реализации функций запроса и записи кеша, поэтому кеш второго уровня напрямую из кеша.Исходный код начинает анализировать.
Анализ исходного кода
Анализ исходного кода осуществляется из метода запроса CachingExecutor.В процессе обхода исходного кода задействовано много знаний, которые невозможно объяснить подробно.Вы можете оставить сообщение после статьи, и я выскажу его в подробнее в сеансе связи.
Метод запроса CachingExecutor сначала получает кэш, указанный во время инициализации конфигурации, из MappedStatement.
Cache cache = ms.getCache();
По сути, использование шаблона декоратора, конкретная цепочка выполнения
SynchronizedCache -> LoggingCache -> SerializedCache -> LruCache -> PerpetualCache.
Ниже приводится введение в конкретные классы реализации Cache Их комбинация дает Cache различные возможности.
- SynchronizedCache: синхронизированный кэш, реализация относительно проста, напрямую используйте метод синхронизированной модификации.
- LoggingCache: функция ведения журнала, класс оформления, используемый для записи частоты попаданий в кэш.Если включен режим DEBUG, будет выводиться журнал частоты попаданий.
- SerializedCache: функция сериализации, которая сериализует значение и сохраняет его в кеше. Эта функция используется для кэширования копии возвращенного экземпляра для сохранения безопасности потоков.
- LruCache: реализация кэширования с использованием алгоритма Lru для удаления наименее использовавшегося ключа/значения.
- PerpetualCache: как самый простой класс кэширования, базовая реализация относительно проста, напрямую используя HashMap.
Затем следует определить, нужно ли обновлять кэш.Код выглядит следующим образом.
flushCacheIfRequired(ms);
В настройках по умолчанию оператор SELECT не очищает кеш, а вставка/обновление/удаление очищает кеш. в метод. Код показан ниже.
private void flushCacheIfRequired(MappedStatement ms) {
Cache cache = ms.getCache();
if (cache != null && ms.isFlushCacheRequired()) {
tcm.clear(cache);
}
}
CachingExecutor Mybatis содержит TransactionalCacheManager, который в приведенном выше коде равен tcm.
Карта хранится в TransactionalCacheManager, и код выглядит следующим образом.
private Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();
Эта карта сохраняет отношение сопоставления между Cache и Cache, обернутым с помощью TransactionalCache.
TransactionalCache реализует интерфейс Cache, а CachingExecutor по умолчанию будет использовать его для обертывания первоначально сгенерированного Cache.Функция заключается в том, что если транзакция зафиксирована, операция над кешем вступит в силу.Если транзакция откатывается или транзакция не зафиксировано, это не повлияет на кеш.
В ясности TransactionalCache есть следующие два предложения. Список, который нужно добавить в кеш при отправке очищается, а кеш ставится на очистку при отправке.Код следующий.
@Override
public void clear() {
clearOnCommit = true;
entriesToAddOnCommit.clear();
}
CachingExecutor продолжает выходить из строя, гарантируя, что NoOutParams в основном используется для обработки хранимых процедур, поэтому пока не нужно его рассматривать.
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, parameterObject, boundSql);
Затем он попытается получить кэшированный список от tcm.
List<E> list = (List<E>) tcm.getObject(cache, key);
В методе getObject ответственность за получение значения полностью передается PerpetualCache. Если он не найден, ключ будет добавлен в набор «Мисс», который в основном предназначен для подсчета количества попаданий.
Object object = delegate.getObject(key);
if (object == null) {
entriesMissedInCache.add(key);
}
CachingExecutor продолжает работать.Если запрашиваются данные, он вызывает метод tcm.putObject для помещения значения в кэш.
if (list == null) {
list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
}
Метод put tcm напрямую не работает с кешем, а только помещает данные и ключ в карту для отправки.
@Override
public void putObject(Object key, Object object) {
entriesToAddOnCommit.put(key, object);
}
Из приведенного выше анализа кода мы можем понять, что если метод фиксации не вызывается из-за роли TranscationalCache, это не окажет прямого влияния на вторичный кеш. Итак, давайте посмотрим, что делается в методе фиксации Sqlsession. Код показан ниже.
@Override
public void commit(boolean force) {
try {
executor.commit(isCommitOrRollbackRequired(force));
Поскольку мы используем CachingExecutor, мы сначала введем метод фиксации, реализованный CachingExecutor.
@Override
public void commit(boolean required) throws SQLException {
delegate.commit(required);
tcm.commit();
}
Ответственность за конкретный коммит будет делегирована обернутому исполнителю. В основном посмотрите на tcm.commit(), tcm в конечном итоге снова вызовет TrancationalCache.
public void commit() {
if (clearOnCommit) {
delegate.clear();
}
flushPendingEntries();
reset();
}
Видение здесь clearOnCommit напоминает мне о флаге, установленном только что методом clear в TrancationalCache.Настоящая очистка кэша выполняется здесь. Ответственность за конкретную очистку делегирована обернутому классу Cache. Затем введите метод flushPendingEntries. Код показан ниже.
private void flushPendingEntries() {
for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
delegate.putObject(entry.getKey(), entry.getValue());
}
................
}
В flushPendingEntries отправляемая карта зацикливается, а затем делегируется обернутому классу Cache для выполнения операции putObject.
Последующие операции запроса будут повторять этот процесс. Если это вставка|обновление|удаление, то он единообразно войдет в метод обновления CachingExecutor, который вызывает эту функцию, и код выглядит следующим образом, поэтому я не буду его повторять.
private void flushCacheIfRequired(MappedStatement ms)
Суммировать
- По сравнению с кешем первого уровня, кеш второго уровня Mybatis реализует совместное использование кэшированных данных между SqlSessions. В то же время гранулярность более тонкая. Он может достигать уровня Mapper и реализовывать различные комбинации классов через Cache. интерфейс сильнее.
- Когда Mybatis запрашивает несколько таблиц, очень вероятно, что появятся грязные данные, будут недостатки дизайна, а условия для безопасного использования будут жесткими.
- В распределенной среде, поскольку реализация Mybatis Cache по умолчанию основана на локальной среде, грязные данные неизбежно будут считаны в распределенной среде.Для реализации интерфейса Mybatis Cache необходимо использовать централизованный кеш, что требует определенных затрат на разработку. используйте Redis и Memcache напрямую для реализации кэширования бизнеса.
Полный текст резюме
В этой статье представлены основные концепции Mybatis, использование кэшей первого и второго уровня Mybatis и анализ исходного кода, а также в определенной степени обобщены кэши первого и второго уровня.
Окончательный вывод заключается в том, что механизм кэширования Mybatis не очень хорошо разработан, и при использовании легко вызвать проблемы с грязными данными.Лично рекомендуется не использовать кэш Mybatis, а использовать другие механизмы на бизнес-уровне для достижения требуется функция кеша, так что Mybatis может честно это сделать Фреймворк ORM просто прекрасен, ха-ха.