предисловие
MyBatis — это обычная структура уровня доступа к базе данных Java. В повседневной работе разработчики в большинстве случаев используют конфигурацию кэша MyBatis по умолчанию, но механизм кэширования MyBatis имеет некоторые недостатки, которые могут легко привести к загрязнению данных при использовании и формированию некоторых потенциальных скрытых опасностей. Лично я также имел дело с некоторыми проблемами разработки, вызванными кэшированием MyBatis в развитии бизнеса.С моим личным интересом я надеюсь разобраться с механизмом кэширования MyBatis для читателей с точки зрения приложения и исходного кода.
Код и таблицы базы данных, задействованные в этом анализе, размещены на GitHub по адресу:mybatis-cache-demo.
содержание
Эта статья разворачивается в следующем порядке.
- Введение в кэш первого уровня и связанная с ним конфигурация.
- Рабочий процесс кэширования уровня 1 и анализ исходного кода.
- Сводка кэша L1.
- Введение в кэш L2 и связанная с ним конфигурация.
- Анализ исходного кода кэша L2.
- Сводка кэша L2.
- Полный текст резюме.
Кэш L1
Введение в кэш L1
Во время работы приложения мы можем выполнять несколько запросов SQL с одинаковыми условиями в одном сеансе базы данных. MyBatis предоставляет решение кэширования первого уровня для оптимизации этой части сценария. Если это один и тот же оператор SQL, он попадет в первый уровень первый Кэш, избегайте прямого запроса к базе данных, повышайте производительность. Конкретный процесс выполнения показан на следующем рисунке.
Каждый SqlSession содержит Executor, а каждый Executor имеет LocalCache. Когда пользователь инициирует запрос, MyBatis генерирует MappedStatement на основе текущего выполняемого оператора, а Local
Кэш выполняет запрос. Если кеш попадает, он возвращает результат непосредственно пользователю. Если кеш не попадает, он запрашивает базу данных, записывает результат в локальный кеш и, наконец, возвращает результат пользователю. Диаграмма классов конкретного класса реализации показана на следующем рисунке.
Конфигурация кэша L1
Давайте посмотрим, как использовать кеш первого уровня MyBatis. Разработчикам нужно только добавить следующий оператор в файл конфигурации MyBatis, чтобы использовать кеш первого уровня. Есть два варианта: SESSION или STATEMENT, по умолчанию используется уровень SESSION, то есть все операторы, выполняемые в сеансе MyBatis, будут совместно использовать этот кэш. Одним из них является уровень STATEMENT, который можно понять, поскольку кеш действителен только для текущего выполняемого оператора.
<setting name="localCacheScope" value="SESSION"/>
Эксперимент с кешем L1
Затем, в ходе эксперимента, чтобы понять влияние кеша первого уровня MyBatis, восстановите измененные данные после каждого модульного теста.
Во-первых, создать пример таблицы student, создать соответствующий класс POJO и добавить и изменить методы, которые можно просмотреть в пакете сущностей и пакете сопоставления.
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 обновил имя студента с id 1, с Karen на Xiaocen, но в запросе после session1 имя студента с id 1 по-прежнему Karen, и появились грязные данные, что также подтвердило предыдущее предположение. совместно используются только в рамках сеансов базы данных.
Рабочий процесс кэширования уровня 1 и анализ исходного кода
Итак, каков рабочий процесс кэша L1? Давайте учиться на уровне исходного кода.
процесс работы
Временная диаграмма выполнения кэша L1 показана на следующем рисунке.
Анализ исходного кода
Далее я прочитаю исходный код базовых классов MyBatis, связанных с запросами, и кэша первого уровня. Это также полезно для дальнейшего изучения кэша второго уровня.
SqlSession: Предоставляет все методы, необходимые для взаимодействия между пользователями и базой данных для внешнего мира, и скрывает основные детали. Класс реализации по умолчанию — DefaultSqlSession.
Executor: SqlSession предоставляет пользователям методы для работы с базой данных, но обязанности, связанные с операциями с базой данных, делегируются Executor.
Как показано на рисунке ниже, у Executor есть несколько классов реализации, которые наделяют Executor различными возможностями.Основные функции каждого класса вы можете изучить самостоятельно по названию класса.
При анализе исходного кода кэша первого уровня мы в основном изучаем внутреннюю реализацию BaseExecutor.
BaseExecutor: BaseExecutor — это абстрактный класс, который реализует интерфейс Executor, определяет несколько абстрактных методов и делегирует определенные операции подклассам для выполнения во время выполнения.
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;
Во введении к кешу первого уровня упоминается, что запрос и запись локального кеша выполняются внутри Executor. Прочитав код BaseExecutor, я обнаружил, что Local Cache является переменной-членом внутри BaseExecutor, как показано в следующем коде.
public abstract class BaseExecutor implements Executor {
protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
protected PerpetualCache localCache;
Cache: Интерфейс Cache в MyBatis предоставляет самые основные операции, связанные с кэшированием, как показано на следующем рисунке.
Существует несколько классов реализации, которые собираются друг с другом с помощью шаблона декоратора, чтобы обеспечить широкие возможности для управления кешем.Некоторые из классов реализации показаны на следующем рисунке.
PerpetualCache, одна из переменных-членов BaseExecutor, является самой базовой реализацией интерфейса Cache. Его реализация очень проста. Он содержит внутри HashMap. Операция кэша первого уровня на самом деле является операцией HashMap. Как показано в коде ниже.
public class PerpetualCache implements Cache {
private String id;
private Map<Object, Object> cache = new HashMap<Object, Object>();
После прочтения соответствующего кода основного класса на уровне исходного кода соответствующий код, участвующий в работе кеша первого уровня, для экономии места исходный код соответствующим образом удаляется.Читатели и друзья могут объединить эту статью для более подробного изучения. позже.
Чтобы выполнить взаимодействие с базой данных, вам сначала нужно инициализировать SqlSession и открыть SqlSession через DefaultSqlSessionFactory:
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
............
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
}
При инициализации SqlSession будет создан новый Executor с использованием класса Configuration в качестве параметра конструктора DefaultSqlSession Код для создания Executor выглядит следующим образом:
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);
}
В то же время метод 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 проста, это просто HashMap без ограничения емкости, а функциональность кеша отсутствует.
- Максимальная область кэша первого уровня MyBatis находится внутри SqlSession. В нескольких сеансах SqlSession или в распределенной среде операция записи в базу данных вызовет грязные данные. Рекомендуется установить уровень кэша на оператор.
Кэш L2
Введение в кэш L2
В упомянутом выше кеше первого уровня самая большая общая область находится в пределах SqlSession. Если несколько сеансов SqlSession должны совместно использовать кеш, необходимо использовать кеш второго уровня. После включения кеша второго уровня CachingExecutor будет использоваться для оформления Executor.Перед входом в процесс запроса кеша первого уровня выполняется запрос кеша второго уровня в CachingExecutor.Конкретный рабочий процесс выглядит следующим образом.
После включения кэша L2 все операторы операций в одном и том же пространстве имен влияют на один и тот же кэш, то есть кэш L2 совместно используется несколькими сеансами SqlSessions и является глобальной переменной.
Когда кеш включен, процесс выполнения запроса данных — это кеш второго уровня -> кеш первого уровня -> база данных.
Конфигурация кэша L2
Чтобы правильно использовать кэш L2, вам необходимо выполнить следующую настройку.
- Включите кэш второго уровня в конфигурационном файле MyBatis.
<setting name="cacheEnabled" value="true"/>
- Настройте кэш или ссылку на кэш в XML-файле сопоставления MyBatis.
Тег cache используется для объявления того, что это пространство имен использует кэш второго уровня и может быть настроено.
<cache/>
- type: тип, используемый кешем, по умолчанию PerpetualCache, который упоминался в кеше первого уровня.
- выселение: определите стратегию утилизации, общие из них FIFO, LRU.
- flushInterval: настройте определенный период времени для автоматической очистки кеша в миллисекундах.
- размер: максимальное количество кэшированных объектов.
- readOnly: независимо от того, доступен ли он только для чтения, если конфигурация доступна для чтения и записи, соответствующий класс сущности должен быть сериализуемым.
- блокировка: если соответствующий ключ не может быть найден в кеше, будет ли он продолжать блокироваться до тех пор, пока соответствующие данные не поступят в кеш.
cache-ref представляет конфигурацию Cache, которая ссылается на другие пространства имен, и операции двух пространств имен используют один и тот же Cache.
<cache-ref namespace="mapper.StudentMapper"/>
Эксперимент с кешем 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, и в то же время степень детализации является более тонкой.
- Когда MyBatis запрашивает несколько таблиц, очень вероятно, что появятся грязные данные, есть недостатки дизайна, а условия для безопасного использования кеша второго уровня жесткие.
- В распределенной среде, поскольку реализация MyBatis Cache по умолчанию основана на локальной среде, грязные данные неизбежно будут считаны в распределенной среде.Для реализации интерфейса MyBatis Cache необходимо использовать централизованный кеш, что требует определенных затрат на разработку. распределенные кеши, такие как Redis и Memcached, могут быть дешевле и безопаснее.
Полный текст резюме
В этой статье представлены основные понятия кэша первого и второго уровня MyBatis и анализируется механизм кэширования MyBatis с точки зрения приложения и исходного кода. Наконец, делается краткое изложение механизма кэширования MyBatis.Лично рекомендуется, чтобы функция кэширования MyBatis была отключена в производственной среде, и, возможно, более целесообразно использовать ее в качестве ORM-фреймворка.
об авторе
Карен, инженер отдела исследований и разработок Meituan Dianping, окончила Шанхайский морской университет в 2016 году и в настоящее время занимается разработкой платформы общественного питания Meituan Dianping. Публичный идентификатор: KailunTalk, добро пожаловать, чтобы подписаться и обсудить больше технических знаний вместе.
Предложения о работе
Meituan Dianping с нетерпением ждет вашего присоединения
Шанхай нанимает: серверная часть Java, разработка данных, внешний интерфейс, контроль качества, продукт, эксплуатация продукта, бизнес-анализ и т. д.
Электронная почта для внутреннего push-резюме: weiyanping#meituan.com
Если вы обнаружите какие-либо ошибки в статье или у вас возникнут вопросы по содержанию, вы можете подписаться на официальный аккаунт Meituantech технической команды Meituan и оставить нам сообщение в фоновом режиме. Каждую неделю мы будем выбирать сердечного друга и отправлять красивый маленький подарок. Приходите отсканировать код, чтобы следовать за нами!