Блог Анатолия Корсакова https://akorsa.ru Tue, 28 Feb 2017 04:21:09 +0000 ru-RU hourly 1 https://wordpress.org/?v=4.7.22 114712680 Советы и ошибки в Spring Transactional https://akorsa.ru/2017/01/sovety-i-oshibki-v-spring-transactions/ https://akorsa.ru/2017/01/sovety-i-oshibki-v-spring-transactions/#respond Tue, 31 Jan 2017 04:33:33 +0000 http://akorsa.ru/?p=5523 В этом посте я соберу самые лучшие практики использования Spring Transactional Service или DAO? Устанавливайте аннотацию @Transactional в слое сервиса (Service layer), а не в слое DAO. Сервисные бины могут использовать несколько DAO, соблюдая ACID под одной и той же транзакцией. Иначе если только в DAO определен механизм транзакций, то сервисные бины увеличат расходы на создание множественных […]

Запись Советы и ошибки в Spring Transactional впервые появилась Блог Анатолия Корсакова.

]]>
В этом посте я соберу самые лучшие практики использования Spring Transactional

Service или DAO?

  • Устанавливайте аннотацию @Transactional в слое сервиса (Service layer), а не в слое DAO. Сервисные бины могут использовать несколько DAO, соблюдая ACID под одной и той же транзакцией. Иначе если только в DAO определен механизм транзакций, то сервисные бины увеличат расходы на создание множественных транзакций для концептуально сгруппированных операций, не говоря уже о неконсистентном состоянии данных, которое мы рискуем получить.

  • В этом пункте, продолжаем мысль из предыдущего, мы можем задать @Transactional(propagation = Propagation.MANDATORY) на уровне класса в наших DAO, таким образом заставляя потребителей наших DAO инициировать управление транзакциями.
  • Необходимо знать дефолтное поведение аннотации @Transactional и не использовать ее вслепую. А именно, если не указано явно, уровень распространения (propagation) установится в Propagation.REQUIRED, что означает использование  текущей транзакции, иначе создать новую; изоляция установится в значение Isolation.DEFAULT, это значение определяется нижележащей БД (по умолчанию у многих БД это значение равно Isolation.READ_COMMITED); readOnly флаг выключен по умолчанию; rollbackFor может быть задано с Throwable классом, но будьте осторожны: по умолчанию откат (rollback) происходит только когда выбрасывается RuntimeException (если не установлен этот параметр).
  • Будьте осторожны с флагом readOnly. Хотя @Transactional(readOnly = true, propagation=Propagation.REQUIRED) выбросит исключение  при попытке выполниться команде вставки/обновления через  JDBC в транзакции, она может иметь неожиданное поведение при попытке вставки/обновления через ORM, где операция скрытно выполнится и успешно выполнится коммит транзакции. В ORM  окружении необходимо использовать этот флаг вместе с Propagation.SUPPORTS. В этом случае мы не будем платить стоимость создания новой транзакции в простой операции выборки. Или даже рассмотрите возможность избавиться от управления транзакциями для операций SELECT.
  • Будьте осторожны при использовании Propagation.REQUIRES_NEW не на высшем уровне транзакций. Это может вызвать многочисленные проблемы. Каждый раз как новая нижележащая транзакция оборачивается этим аспектом, в случаях когда множественные транзакции с REQUIRES_NEWs включены в пределах одного транзакционного сервисного метода могут возникнуть проблемы с потерей данных. С другой стороны, не возбраняется использовать аннтацию на транзакционном методе высшего уровня, что на самом деле аналогично поведению дефолтного Propagation.REQUIRED

Spring автоматически откатывает транзакции для unchecked (Runtime) исключений

Когда возникает Runtime exception, то Spring помечает текущую транзакцию для отката.

Возможно самой запутанной частью таких откатов явлется наблюдение такого сообщения «UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only».

Это ожидаемое поведение, но может быть запутанным, если не понимать что произошло на самом деле. В случаем если один транзакционный метод вызывает другой транзакционный метод в другом классе и этот внутренний вызов выбрасывает Runtime exception, то вся транзакция целиком будет отменена. Это будет происходить, если Вы не ловите Exception в вызывающем классе. Если откат внешнего класса не желателен, Вы можете либо выполнить внутреннюю транзакцию в новой транзакции (используя другие propagation) или использовать атрибут noRollBackFor аннотации @Transactional.

Не используйте аннотацию @Transactional на приватных, protected или default методах.

Этот пункт происходит из первого пункта о том как Spring управляет транзакциями. Добавление аннотации к методами с private, protected или default модификаторами доступа не выбросит исключение.Однако аннотация будет проигнорирована

Логирование транзакций

Очень важно в случае возникновения непредвиденных ситуаций смотреть и понимать логи. Для включения логов необходимо добавить в log4j.properties

org.springframework.transaction=TRACE

Это позволит Вам отследить активности происходящие внутри транзакционных процессов. Логи обеспечивают детальную информацию о новых, переиспользованных и активных транзакциях, режимах транзакций, коммитов транзакций. Это наилучший способ понять как транзакции управляют Вашим приложением и решить проблему.

Небольшая справка по атрибуту Transactional Propagation

@Transactional(propagation=Propagation.REQUIRED)

Если не указано, то стратегия распространения по дефолту это REQUIRED.

Другие опции это REQUIRES_NEW, MANDATORY, SUPPORTS, NOT_SUPPORTED, NEVER, и NESTED.

REQUIRED

  • Означает что целевой метод не может быть запущен без активной транзакции. Если транзакция уже стартовала до вызова  этого метода, тогда методы выполнится в ней или иначе при вызове метода новая транзакция будет создана.

REQUIRES_NEW

  • Означает что новая транзакция должна начинаться всякий раз, как целевой метод будет вызываться. Если уже есть начатая транзакция, то она будет приостановлена, до начала новой.

MANDATORY

  • Означает что целевой метод требует активной транзакции для старта. Если ее нет, то выполнение не будет произведено, и выбросится исключение.

SUPPORTS

  • Означает что целевой метод может быть исполнен вне транзакции. Если есть начатая транзакция, то метод запустится в ней. Если нет запущенной транзакции, то метод выполнится все равно, только не в транзакционом контексте.
  • Методы, которые выполняют выборку данных наилучшие кандидаты для этой опции.

NOT_SUPPORTED

  • Означает что целевой метод не требует транзакционного контекста для выполнения. Если есть начатая транзакция, то она будет приостановлена.

NEVER

  • Означает что целевой метод выбросит исключение, если выоплнится в транзакционном процессе.
  • Не советую использовать эту опцию.

@Transactional (rollbackFor=Exception.class)

  • По умолчанию откат происходит при rollbackFor=RunTimeException.class
  • В спринге, все API классы выбрасывают RuntimeException, что означает что если любой метод упал, то контейнер всегда произведет откат текущей транзакции.
  • Проблема только с checked exceptions. Так что данная опция может использоваться для декларативного отката транзакции если выбросится Checked Exception.

@Transactional (noRollbackFor=IllegalStateException.class)

  • Означает что откат не должен происходить если целевой метод выбросил это исключение.

Запись Советы и ошибки в Spring Transactional впервые появилась Блог Анатолия Корсакова.

]]>
https://akorsa.ru/2017/01/sovety-i-oshibki-v-spring-transactions/feed/ 0 5523
Введение в key-value NoSQL БД Aerospike https://akorsa.ru/2017/01/vvedenie-v-key-value-nosql-bd-aerospike/ https://akorsa.ru/2017/01/vvedenie-v-key-value-nosql-bd-aerospike/#respond Thu, 19 Jan 2017 06:17:26 +0000 http://akorsa.ru/?p=5499 Aerospike Aerospike — это хранилище пар вида ключ-значение с моделью данных без использования схемы. Данные организованы в контейнеры с собственными политиками, называемыми «namespaces» (пространства имен), семантически близкими к понятию «databases» (базы данных) в реляционных БД. В переделах namespace, данные разделены на «sets» (наборы — схоже с таблицей в RDMS) и «records» (записи — схоже со строками […]

Запись Введение в key-value NoSQL БД Aerospike впервые появилась Блог Анатолия Корсакова.

]]>
Aerospike

Aerospike — это хранилище пар вида ключ-значение с моделью данных без использования схемы. Данные организованы в контейнеры с собственными политиками, называемыми «namespaces» (пространства имен), семантически близкими к понятию «databases» (базы данных) в реляционных БД. В переделах namespace, данные разделены на «sets» (наборы — схоже с таблицей в RDMS) и «records» (записи — схоже со строками в таблице). Каждая запись имеет индексный «key» который уникален в наборе, и один или более именованных «bins» (схоже с колонками), которые содержат значения, ассоциированные с record.

Концепция Aerospike в терминах MySQL

Aerospike MySQL
namespace db
set table
bin column
key primary key
record row
 Рассмотрим далее как установить и сконфигурировать Aerospike, а так же как использовать Java клиент.

Скачивание, установка и запуск Aerospike

http://www.aerospike.com/download/ — на Windows

На Linux
wget -O aerospike.tgz 'http://aerospike.com/download/server/latest/artifact/ubuntu12' | tar -xvf aerospike.tgz| sudo ./asinstall| sudo service aerospike start

Уникальный тип для Aerospike — Large Data Types (LDTs)

Что это такое эти LDTs? Уникальные для Aerospike, Large Data Types позволяют отдельным бинам одной записи (record Bins) (столбцам) содержать коллекции сотен тысяч объектов (или документов), и эти типы данных позволяют эффективно хранить эти коллекции и обрабатывать в БД. Данная особенность Aerospike — LDT использует механизм Aerospike User-Defined Functions и новый тип контейнера записей, который указан как «sub-records» (подзаписи).

Sub-records (подзаписи) очень похожи на обычные Aerospike records (записи),с главным отличием, что они связаны с родительской записью. Они делят одно и то же адресное пространство и внутреннюю  блокировку, как и родительская запись, так что они переносятся вместе с родительской записью во время миграций и они защищены одним и тем же механизмом изоляции.

Большие объекты в Aerospike не хранятся рядом с ассоциированной записью, вместо этого они делятся на подзаписи (sub-records — с размерами между 2kb и 32kb), как показано на рисунке. Подзаписи индексированы, связаны вместе, и обслуживаются внутри БД через User Defined Functions (UDFs).

 

Конфигурирование Aerospike

http://www.aerospike.com/docs/operations/configure/

# Конфигурационный файл Aerospike



service {
user root
group root
paxos-single-replica-limit 1 # Количество нод при котором количество реплик автоматически уменьшится до единицы
pidfile /var/run/aerospike/asd.pid
service-threads 4
transaction-queues 4
transaction-threads-per-queue 4
proto-fd-max 15000
}

logging {
# Log file must be an absolute path.
file /var/log/aerospike/aerospike.log {
context any info
}
}

network {
service {
address any #
port 3000
}

heartbeat {
mode multicast # Может быть или |multicast| или |mesh|. Multicast поддерживает только UDP протокол для передачи даных о состоянии кластера, mesh работает по TCP
# address 239.1.99.222 # только для multicast
# port 9918 # только для multicast

# Для использования unicast-mesh режима, удалите три строчки сверху и посмотрите
# aerospike_mesh.conf.
mode mesh
address 265.312.999.555 # IP address
# mesh-port 3002
interval 150 # Интервал в милисекундах, через который посылаются данные о состоянии.
timeout 40 # Количество пропущенных heartbeats, после которого удаленная нода будет объявлена мертвой. (150ms x 40 = 6 seconds)

}

fabric {
port 3001
}

info {
port 3003
}
}

namespace akorsa {
replication-factor 2 # Количество копий записи (включаю мастер копию) содержащихся в целом кластере.
default-ttl 30d # Time-To-Live 30 дней, используйте 0 для бесконечного времени жизни.

enable-xdr false # Позволяет включить репликацию на уровне дата-центров see http://www.aerospike.com/docs/architecture/xdr.html
set-disable-eviction false # сервер будет удалять старые записи, если память или диск станет полным
high-water-memory-pct 60 # Данные будут удалены, если утилизация памяти достигнет более указанного процента
high-water-disk-pct 50 # Данные будут удалены, если утилизация диска достигнет более указанного процента</pre><pre>stop-writes-pct 90 # Запретить запись (кроме удалений) когда утилизация памяти выше указанного процента.
} 
namespace test { 
replication-factor 2 
memory-size 4G 
default-ttl 30d 
storage-engine memory 
load-at-startup true # on startup load data from storage and not start with empty db 
data-in-memory true # хранить данные в памяти, иначе только индекс будет храниться в памяти. Рекомендуется использовать если диск не SSD 
} 
namespace bar { 
replication-factor 2 
memory-size 4G 
default-ttl 30d 
storage-engine device { 
file /opt/aerospike/data/bar.dat 
filesize 16G 
data-in-memory true # Хранить данные в памяти в добавок к файлу
} 
} 

Размер записи (record)

Дефолтный максимальный размер record составляет  128 KB

 com.aerospike.client.AerospikeException: Error Code 13: Record too big

Можно увеличить следующим способом:

namespace mynamespace {
storage-engine device {
write-block-size 1M # Max size in bytes for each record (LDT entries might be larger)
}
}

Максимум это 1MB

attempted to configure write block size in excess of 1048576 

Клиент AQL Client

aql это SQL подобный клиент для aerospike

aql Management

Базы данных

 aql> show namespaces 

Таблицы

aql> show sets

Столбцы

 aql> show bins
+-------+-------------+-------+-----------+
| quota | bin | count | namespace |
+-------+-------------+-------+-----------+
| 32768 | "firstname" | 3 | "test" |
| 32768 | "height" | 3 | "test" |
| 32768 | "id" | 3 | "test" |
+-------+-------------+-------+-----------+

Query


aql> select * from test.people

aql> select firstname, height, id from test.people

Для фильтрации нужен индекс


aql> select firstname, height, id from test.people where id=42
0 rows in set (0.001 secs)
Error: (201) AEROSPIKE_ERR_INDEX_NOT_FOUND

aql> CREATE INDEX user_age_idx ON test.people (height) NUMERIC

aql> select firstname, height, id from test.people where height=187
aql> select firstname, height, id from test.people where height between 187 and 190

Получить запись через первичный ключ:


select * from mynamespace.myset where PK='foo'

http://www.aerospike.com/docs/guide/query.html

http://www.aerospike.com/docs/guide/aggregation.html

Используем Java клиент

http://www.aerospike.com/docs/client/java/install/


import com.aerospike.client.AerospikeClient;
import com.aerospike.client.Bin;
import com.aerospike.client.Host;
import com.aerospike.client.Key;
import com.aerospike.client.Record;
import com.aerospike.client.policy.ClientPolicy;

...

Host[] hosts = new Host[] {
new Host("127.0.0.1", 3000),
};

try(AerospikeClient client = new AerospikeClient(new ClientPolicy(), hosts)) {

// this namespace needs to exist in the aerospike configuration
String namespaceName="test";
String setName="people";
String bin1Name="firstname";
String bin2Name="height";
String bin3Name="id";

Integer id=1;
Key key = new Key(namespaceName, setName, id);
Bin bin1 = new Bin(bin1Name, "John");
Bin bin2 = new Bin(bin2Name, 182);
Bin bin3 = new Bin(bin3Name, id);

// Write a record
client.put(null, key, bin1, bin2, bin3);

// Read a record
Record record = client.get(null, key);
System.err.println(record); // (gen:4),(exp:182531601),(bins:(firstname:John),(id:1),(height:182))
String fstName=record.getString(bin1Name);
System.err.println(fstName); // John
}

Размещаем списки или maps в бин и читаем и записываем их через AeroSpike

https://github.com/aerospike/aerospike-client-java/blob/master/examples/src/com/aerospike/examples/LargeMap.java

 Для использования LDT необходимо подключить опцию в необходимом namespace

namespace mynamespace {
ldt-enabled true # large data types, require to put lists and maps as values into bins
}


final LargeMap lmap = client.getLargeMap(writePolicy, new Key(namespace, trackingId, recordKey), "myBin", null);
final Map<?, ?> filteredValues = lmap.get(Value.get(fieldKey));
final Object result = filteredValues.get(fieldKey);

И записываем в него:


final LargeMap lmap = client.getLargeMap(writePolicy, new Key(namespace, trackingId, recordKey), "myBin", null);
lmap.put(Value.get(fieldKey), Value.get(yourValue));

Удаляем


final LargeMap lmap = client.getLargeMap(writePolicy, new Key(namespace, trackingId, recordKey), "myBin", null);
lmap.remove(Value.get(field));

Запись Введение в key-value NoSQL БД Aerospike впервые появилась Блог Анатолия Корсакова.

]]>
https://akorsa.ru/2017/01/vvedenie-v-key-value-nosql-bd-aerospike/feed/ 0 5499
Создание рекомендательной системы, используя Aerospike и Spring Boot (Часть 1) https://akorsa.ru/2017/01/sozdanie-rekomendatelnoj-sistemy-ispolzuya-aerospike-i-spring-boot/ https://akorsa.ru/2017/01/sozdanie-rekomendatelnoj-sistemy-ispolzuya-aerospike-i-spring-boot/#respond Sun, 15 Jan 2017 07:50:52 +0000 http://akorsa.ru/?p=5489 Введение Рекомендательные системы используются в приложениях для улучшения взаимодействия с пользователями. Например, приложения электронной коммерции рекомендуют продукты покупателям, которые другие покупатели — с похожим поведением — просмотрели, лайкнули или приобрели. Новостные приложения могут также использовать рекомендательные системы в реальном времени. Эти дополнения к стандартным функциям улучшат интерфейс пользователя, увеличат продажи и помогут улучшить лояльность клиентов. […]

Запись Создание рекомендательной системы, используя Aerospike и Spring Boot (Часть 1) впервые появилась Блог Анатолия Корсакова.

]]>
Введение

Рекомендательные системы используются в приложениях для улучшения взаимодействия с пользователями. Например, приложения электронной коммерции рекомендуют продукты покупателям, которые другие покупатели — с похожим поведением — просмотрели, лайкнули или приобрели. Новостные приложения могут также использовать рекомендательные системы в реальном времени. Эти дополнения к стандартным функциям улучшат интерфейс пользователя, увеличат продажи и помогут улучшить лояльность клиентов. Эта статья содержит примеры кода для построения рекомендательной системы реального времени, основанной на косинусном сходстве.

Этот пример использует фреймворк Spring Boot, с помощью которого можно быстро развернуть бэкенд системы на Java. В качестве базы данных используется Aerospike.

Aerospike — высоконадежная NoSQL база данных и низкими задержками, которая масштабируется линейно — таким образом легко ее использовать в качестве хранилища для онлайн приложений. Она хорошо подходит к этому примеру, потому что она масштабируется как горизонтально (увеличивая количество нод на кластере), так и вертикально (поддерживает многопоточность). Aerospike — это in-memory база данных, оптимизированная для использования и DRAM и нативной Flash памяти. Aerospike может похвастаться задержками менее чем в 1 миллисекунду на более чем 100 000 запросов в секунду на один сервер с большим уровнем доступности и немедленной согласованностью данных. Это все автоматизировано и доступно «из коробки».

Что будем разрабатывать?

В этой статье я создам простой рекомендательный сервис. Предлагаемый движок будет использовать векторы сходимости для рекомендации продукта — в нашем случае, фильмов — клиенту. Алгоритм элементарный, и предоставляет стартовую площадку для реальных исследований рекомендаций.

Для обеспечения эффективной работы рекомендательного сервиса Вам необходима БД, которая может выгружать данные очень быстро, настолько что, понадобится лишь несколько запросов для выявления полноценной рекомендации. Если Ваша база данных работает очень медленно, то и процесс выявления рекомендаций затянется надолго.

Вы можете использовать любую БД, в этом примере я буду использовать Aerospike NoSQL БД.

Вы построите сервис, который принимает HTTP GET запрос вида:

http://localhost:8080/aerospike/recommmendation/{user}

Сервис ответит следующим JSON массивом рекомендаций:

[
  {"expiration":130019315,
   "bins":
     {"title":"Classic Albums: Meat Loaf: Bat Out of Hell",  "yearOfRelease":"1999"},
   "generation":4},
  {"expiration":130019337,
   "bins":
     {"title":"Rudolph the Red-Nosed Reindeer",
"yearOfRelease":"1964"},
   "generation":4},
  {"expiration":130019338,
   "bins":{"title":"The Bad and the Beautiful",
    "yearOfRelease":"1952"},
   "generation":4},
  {"expiration":130019384,
   "bins":
    {"title":"Jingle All the Way","yearOfRelease":"1996"},
   "generation":4},
  {"expiration":130019386,
   "bins":
     {"title":"The Killing","yearOfRelease":"1956"},
  "generation":4},
  {"expiration":130019400,
 "bins":
  {"title":"Silkwood","yearOfRelease":"1983"},"generation":4},
  {"expiration":130019404,"bins":{"title":"Chain of Command","yearOfRelease":"2000"},
   "generation":4}]

Каждый элемент JSON массива содержит 3 поля: generation, expiration и bins. Generation — это число используемое Aerospike для многопоточности, которое изменяется каждый раз когда происходит изменение записи. Expiration — это дата (time to live) в секундах, при наступлении которой запись автоматически удалится из кластера. Bins — это список пар имя/значение, в котором содержится данные записи. Bins похожи на столбцы в реляционных БД. Каждый объект или запись могут иметь множество бинов.

Алгоритм выявления рекомендаций

Будем разрабатывать поведенческий рекомендательный движок. Есть три категории объектов: Клиенты, Рейтинги и Фильмы. Клиенты идентифицируются случайными идентификаторами. Есть несколько доступных операций — просмотр фильма или выставление рейтинга фильму. Идентификатор фильма также абстрактное число.

В нашей системе, клиенты смотрят и оценивают фильмы. На основании их оценок, сервис определяет для других клиентов интересны ли им для просмотра некоторые фильмы.

Профиль клиента содержит историю его просмотров и рейтинги, и Фильм содержит список людей оценивших его.

Простая задача нахождения рекомендаций — это найти другого клиента, который похож на заданного клиента и рекомендовать продукты, которые понравились другому клиенту. Хорошей идеей является  исключить дубликаты, так что список рекомендованных фильмов будет состоять из фильмов, которые заданный клиент еще не смотрел..

Данные, которые мы будем использовать в этом упражнении это  моделируемый набор данных, который состоит из Фильмов просмотренных Клиентами, похожий на данные любого медиа сайта как NetFlix, Hulu, или Tivo. В этой симуляции есть только около 25 фильмов в датасете.

Схема данных

Aerospike имеет гибкую NoSQL модель данных. Set может быть использован для группировки записей, как таблица в реляционной БД , но без ограничений, применяемых к записям в Set.

Данные в этом примере хранятся в двух Aerospike сетах (Sets):

  • MOVIE_TITLES
  • MOVIE_CUSTOMERS

Рейтинги

Рейтинг это подзапись (sub-record), которая хранит мап (map) значений. Он хранится в Large List. Large List хранится в Bin в обоих записях: Сustomer record и Movie record. Значения в мапе это:

customer-id movie-id rating date
String String Integer String

Фильмы

Запись Movie содержит некоторые детали  о фильме, как Title (Название) и Year of release (Год выпуска),но более важен список тех кто просмотрел его, пользовательский рейтинг и когда он был выставлен. Этот список важен при определении того кто наиболее похожий клиент.

Move ID (primary key) YearOfRelease Title WATCHED_BY Rating
String String String Large Stack of Ratings (Aerospike), List (MongoDB) Integer

Клиенты

Запись Customer содержит ID клиента и список просмотренных и оцененных фильмов. Можно было бы разместить больше дополнительных атрибутов клиента, но для примера запись просто содержит список рейтингов.

Customer ID (primary key) MOVIES_WATCHED
String Large Stack of Ratings (Aerospike), List (MongoDB)

Как будем искать схожесть?

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

Сценарий

  1. Jane Doe входит в приложение
  2. Получаем профиль Jane
  3. Получаем запись Movie для каждого фильма, который Jane просмотрела. Если число фильмов мало, Вы можете использовать батчевую операцию в Aerospike, которая вытащит список записей за один проход. Если оно большое, лучше получать их частями.
  4. Для каждого фильма:
    • Получаем профиль каждого просмотревшего данный фильм пользователя
    • Смотрим насколько данный профиль схож с профилем Jane, задав ему оценку схожести
  5. Используя профиль пользователя с самой большой оценкой схожести, рекомендуем фильмы из профиля этого пользователя, которые Jane еще не смотрела.

Это очень элементарная техника и она полезна только как иллюстрация, и в ней есть несколько недостатков:

  • Представьте, что Jane просмотрела Гарри Поттера. Было бы глупо вычислять схожесть, используя профили пользователей, которые просмотрели этот фильмы, потому что очень большое количество людей посмотрели Гарри Поттера. Если обобщить эту идею, то фильмы, с количеством просмотров превышающего некоторые порог должны быть исключены.
  • Косинусная схожесть предполагает, что каждый элемент в векторе имеет одинаковый вес. Элементы в наших векторах это movie ID и рейтинг фильма также.

Продолжение во второй части…

Запись Создание рекомендательной системы, используя Aerospike и Spring Boot (Часть 1) впервые появилась Блог Анатолия Корсакова.

]]>
https://akorsa.ru/2017/01/sozdanie-rekomendatelnoj-sistemy-ispolzuya-aerospike-i-spring-boot/feed/ 0 5489
Векторная модель и косинусное сходство (Cosine similarity) https://akorsa.ru/2017/01/vektornaya-model-i-kosinusnoe-shodstvo-cosine-similarity/ https://akorsa.ru/2017/01/vektornaya-model-i-kosinusnoe-shodstvo-cosine-similarity/#respond Sat, 07 Jan 2017 15:30:22 +0000 http://akorsa.ru/?p=5483 В это статье я хочу ознакомить вас с примером использования векторной модели и рассказать как используется косинусное сходство —  Cosine similarity в информационном поиске. Документ в векторной модели рассматривается как неупорядоченное множество термов. Термами в информационном поиске называют слова, из которых состоит текст, а также такие элементы текста, как, например, 2010, II-5 или Тянь-Шань. Различными […]

Запись Векторная модель и косинусное сходство (Cosine similarity) впервые появилась Блог Анатолия Корсакова.

]]>
В это статье я хочу ознакомить вас с примером использования векторной модели и рассказать как используется косинусное сходство —  Cosine similarity в информационном поиске.

Документ в векторной модели рассматривается как неупорядоченное множество термов. Термами в информационном поиске называют слова, из которых состоит текст, а также такие элементы текста, как, например, 2010, II-5 или Тянь-Шань.

Различными способами можно определить вес терма в документе — «важность» слова для идентификации данного текста. Например, можно просто подсчитать количество употреблений терма в документе, так называемую частоту терма, — чем чаще слово встречается в документе, тем больший у него будет вес. Если терм не встречается в документе, то его вес в этом документе равен нулю.

Хорошее описание представлено в wiki (на русском, на английском).

Как рассчитать косинусное сходство? (Cosine similarity)

Рассмотрим небольшую коллекцию C, которая содержит следующие три документа:

Document 1 new york times
Document 2 new york post
Document 3 los angeles times

Некоторые термы встречаются в двух документах, некоторые только в одном. Общее количество документов N=3.

Рассчитаем значения idf (функции от величины, обратной количеству документов коллекции, в которых встречается этот терм) для термов:

angeles  log2(3/1)=1.584
los log2(3/1)=1.584
new log2(3/2)=0.584
post log2(3/1)=1.584
times log2(3/2)=0.584
york log2(3/2)=0.584

Для всех документов, мы вычислим значения tf  (отношение числа вхождений некоторого слова к общему числу слов документа) для всех термов из коллекции. Предположим, что слова в векторе упорядочены по алфавиту.

angeles los new post times york
Document 1 0 0 1 0 1 1
Document 2 0 0 1 1 0 1
Document 3 1 1 0 0 1 0

Теперь мы умножим значения tf на значения idf для каждого терма, получаем следующую матрицу: (Все термы встречаются только один раз в каждом документе в нашей небольшой коллекции, так что максимальное значение для нормализации будет 1.)

angeles los new post times york
Document 1 0 0 0.584 0 0.584 0.584
Document 2 0 0 0.584 1.584 0 0.584
Document 3 1.584 1.584 0 0 0.584 0

Для заданного поискового запроса: “new new times”, мы вычислим tf-idf вектор для запроса, и вычислим схожесть каждого документа из коллекции с заданным запросом, используя измерение косинусной схожести (cosine similarity). При вычислении tf-idf значений для термов из запроса мы разделим частоту на максимальную частоту (2) и умножим на значения idf.

query 0 0 (2/2)*0.584=0.584 0 (1/2)*0.584=0.292 0

Рассчитаем длину каждого документа и запроса:

Length of d1 = sqrt(0.584^2+0.584^2+0.584^2)=1.011

Length of d2 = sqrt(0.584^2+1.584^2+0.584^2)=1.786

Length of d3 = sqrt(1.584^2+1.584^2+0.584^2)=2.316

Length of q = sqrt(0.584^2+0.292^2)=0.652

Затем рассчитаем схожесть пл формуле

:

cosSim(d1,q) = (0*0+0*0+0.584*0.584+0*0+0.584*0.292+0.584*0) / (1.011*0.652) = 0.776

cosSim(d2,q) = (0*0+0*0+0.584*0.584+1.584*0+0*0.292+0.584*0) / (1.786*0.652) = 0.292

cosSim(d3,q) = (1.584*0+1.584*0+0*0.584+0*0+0.584*0.292+0*0) / (2.316*0.652) = 0.112

Согласно полученным значениям схожести, конечный порядок в котором документы будут представлены как результат запроса будет: d1, d2, d3.

 

Запись Векторная модель и косинусное сходство (Cosine similarity) впервые появилась Блог Анатолия Корсакова.

]]>
https://akorsa.ru/2017/01/vektornaya-model-i-kosinusnoe-shodstvo-cosine-similarity/feed/ 0 5483
Используем TDD в Java 8 Stream https://akorsa.ru/2016/12/ispolzuem-tdd-v-java-8-stream/ https://akorsa.ru/2016/12/ispolzuem-tdd-v-java-8-stream/#respond Wed, 28 Dec 2016 07:17:43 +0000 http://akorsa.ru/?p=5463 Введение Идея, которая лежит в упражнениях, приведенных ниже, это изучить Streaming Java 8, используя подход test-driven development (пишем имплементацию для первого теста, убеждаемся, что она работает и двигаемся к следующему). Каждая секция начинается с темы в форме тестов, которые докажут, что имплементация, которую мы напишем, будет корректно работать. Каждый из тестов сопровождается имплементацией на Java […]

Запись Используем TDD в Java 8 Stream впервые появилась Блог Анатолия Корсакова.

]]>
Введение

Идея, которая лежит в упражнениях, приведенных ниже, это изучить Streaming Java 8, используя подход test-driven development (пишем имплементацию для первого теста, убеждаемся, что она работает и двигаемся к следующему).

Каждая секция начинается с темы в форме тестов, которые докажут, что имплементация, которую мы напишем, будет корректно работать. Каждый из тестов сопровождается имплементацией на Java 7 и на Java 8, используя Stream API. Таким способом мы сравним некоторые особенности Java 8 с эквивалентом из Java 7.

Java 8 map

Приведите элементы коллекции в вид с верхним регистром.

Тест

public class ToUpperCaseSpec {

    @Test
    public void transformShouldConvertCollectionElementsToUpperCase() {
        List<String> collection = asList("My", "name", "is", "John", "Doe");
        List<String> expected = asList("MY", "NAME", "IS", "JOHN", "DOE");
        assertThat(transform(collection)).hasSameElementsAs(expected);
        assertThat(transform7(collection)).hasSameElementsAs(expected);
    }
}

Имплементации на Java 7 и Java 8

public class ToUpperCase {
    public static List<String> transform7(List<String> collection) {
        List<String> coll = new ArrayList<>();
        for (String element : collection) {
            coll.add(element.toUpperCase());
        }
        return coll;
    }

    public static List<String> transform(List<String> collection) {
        return collection.stream() // Convert collection to Stream
                .map(String::toUpperCase) // Convert each element to upper case
                .collect(toList()); // Collect results to a new list
    }
}

Java 8 filter

Отфильтруйте коллекцию так что вернуться только элементы с менее 4 символами.

Тест

public class FilterCollectionSpec {
    @Test
    public void transformShouldFilterCollection() {
        List<String> collection = asList("My", "name", "is", "John", "Doe");
        List<String> expected = asList("My", "is", "Doe");
        assertThat(transform(collection)).hasSameElementsAs(expected);
        assertThat(transform7(collection)).hasSameElementsAs(expected);
    }
}

Имплементации на Java 7 и Java 8

public class FilterCollection {
    public static List<String> transform7(List<String> collection) {
        List<String> newCollection = new ArrayList<>();
        for (String element : collection) {
            if (element.length() < 4) {
                newCollection.add(element);
            }
        }
        return newCollection;
    }

    public static List<String> transform(List<String> collection) {
        return collection.stream() // Convert collection to Stream
                .filter(value -> value.length() < 4) // Filter elements with length smaller than 4 characters
                .collect(toList()); // Collect results to a new list
    }
}

Java 8 flatMap

Делаем плоской многоуровневую колекцию

Тест

public class FlatMapSpec {

    @Test
    public void transformShouldFlattenCollection() {
        List<List<String>> collection = asList(asList("Viktor", "Farcic"), asList("John", "Doe", "Third"));
        List<String> expected = asList("Viktor", "Farcic", "John", "Doe", "Third");
        assertThat(FlatCollection.transform(collection)).hasSameElementsAs(expected);
        assertThat(FlatCollection.transform7(collection)).hasSameElementsAs(expected);
    }


}

Имплементации на Java 7 и Java 8

public class FlatCollection {

    public static List<String> transform7(List<List<String>> collection) {
        List<String> newCollection = new ArrayList<>();
        for (List<String> subCollection : collection) {
            for (String value : subCollection) {
                newCollection.add(value);
            }
        }
        return newCollection;
    }

    public static List<String> transform(List<List<String>> collection) {
        return collection.stream() // Convert collection to Stream
                .flatMap(value -> value.stream()) // Replace list with stream
                .collect(Collectors.toList());  // Collect results to a new list
    }

}

Java 8 max и comparator

Получить самого старшего человека из коллекции

Тест

public class OldestPersonSpec {

    @Test
    public void getOldestPersonShouldReturnOldestPerson() {
        Person sara = new Person("Sara", 4);
        Person viktor = new Person("Viktor", 40);
        Person eva = new Person("Eva", 42);
        List<Person> collection = asList(sara, eva, viktor);
        assertThat(getOldestPerson(collection)).isEqualToComparingFieldByField(eva);
        assertThat(getOldestPerson7(collection)).isEqualToComparingFieldByField(eva);
    }
}

Имплементации на Java 7 и Java 8

public class OldestPerson {

    public static Person getOldestPerson7(List<Person> people) {
        Person oldestPerson = new Person("", 0);
        for (Person person : people) {
            if (person.getAge() > oldestPerson.getAge()) {
                oldestPerson = person;
            }
        }
        return oldestPerson;
    }

    public static Person getOldestPerson(List<Person> people) {
        return people.stream() // Convert collection to Stream
                .max(Comparator.comparing(Person::getAge)) // Compares people ages
                .get(); // Gets stream result
    }
}

Java 8 sum и reduce

Получить сумму всех элементов коллекции

Тест

public class SumSpec {

    @Test
    public void transformShouldConvertCollectionElementsToUpperCase() {
        List<Integer> numbers = asList(1, 2, 3, 4, 5);
        assertThat(calculate(numbers)).isEqualTo(1 + 2 + 3 + 4 + 5);
    }

}

Имплементации на Java 7 и Java 8

public class Sum {

    public static int calculate7(List<Integer> numbers) {
        int total = 0;
        for (int number : numbers) {
            total += number;
        }
        return total;
    }

    public static int calculate(List<Integer> people) {
        return people.stream() // Convert collection to Stream
                .reduce(0, (total, number) -> total + number); // Sum elements with 0 as starting value
    }

}

Java 8 filter и map

Получить имена всех детей (до 18 лет)

Тест

public class KidsSpec {

    @Test
    public void getKidNameShouldReturnNamesOfAllKidsFromList() {
        Person sara = new Person("Sara", 4);
        Person viktor = new Person("Viktor", 40);
        Person eva = new Person("Eva", 42);
        Person anna = new Person("Anna", 5);
        List<Person> collection = asList(sara, eva, viktor, anna);
        assertThat(getKidNames(collection))
                .contains("Sara", "Anna")
                .doesNotContain("Viktor", "Eva");
        assertThat(getKidNames7(collection))
                .contains("Sara", "Anna")
                .doesNotContain("Viktor", "Eva");
    }

}

Имплементации на Java 7 и Java 8

public class Kids {

    public static Set<String> getKidNames7(List<Person> people) {
        Set<String> kids = new HashSet<>();
        for (Person person : people) {
            if (person.getAge() < 18) {
                kids.add(person.getName());
            }
        }
        return kids;
    }

    public static Set<String> getKidNames(List<Person> people) {
        return people.stream()
                .filter(p -> p.getAge() < 18) // Filter kids (under age of 18)
                .map(Person::getName) // Map Person elements to names
                .collect(toSet()); // Collect values to a Set
    }
}

Java 8 summaryStatisctics

Получить стастику по людям: средний возраст, количество, максимальный возраст, минимальный возраст и сумма всех возрастов.

Тест

public class PeopleStatsSpec {

    Person sara = new Person("Sara", 4);
    Person viktor = new Person("Viktor", 40);
    Person eva = new Person("Eva", 42);
    List<Person> collection = asList(sara, eva, viktor);

    @Test
    public void getStatsShouldReturnAverageAge() {
        assertThat(getStats(collection).getAverage())
                .isEqualTo((double)(4 + 40 + 42) / 3);
        assertThat(getStats7(collection).getAverage())
                .isEqualTo((double)(4 + 40 + 42) / 3);
    }

    @Test
    public void getStatsShouldReturnNumberOfPeople() {
        assertThat(getStats(collection).getCount())
                .isEqualTo(3);
        assertThat(getStats7(collection).getCount())
                .isEqualTo(3);
    }

    @Test
    public void getStatsShouldReturnMaximumAge() {
        assertThat(getStats(collection).getMax())
                .isEqualTo(42);
        assertThat(getStats7(collection).getMax())
                .isEqualTo(42);
    }

    @Test
    public void getStatsShouldReturnMinimumAge() {
        assertThat(getStats(collection).getMin())
                .isEqualTo(4);
        assertThat(getStats7(collection).getMin())
                .isEqualTo(4);
    }

    @Test
    public void getStatsShouldReturnSumOfAllAges() {
        assertThat(getStats(collection).getSum())
                .isEqualTo(40 + 42 + 4);
        assertThat(getStats7(collection).getSum())
                .isEqualTo(40 + 42 + 4);
    }
}

Имплементации на Java 7 и Java 8

public class PeopleStats {

    public static Stats getStats7(List<Person> people) {
        long sum = 0;
        int min = people.get(0).getAge();
        int max = 0;
        for (Person person : people) {
            int age = person.getAge();
            sum += age;
            min = Math.min(min, age);
            max = Math.max(max, age);
        }
        return new Stats(people.size(), max, min, sum);
    }

    public static IntSummaryStatistics getStats(List<Person> people) {
        return people.stream()
                .mapToInt(Person::getAge)
                .summaryStatistics();
    }
}

Java 8 partitioningBy

Необходимо разделить взрослых и детей

Тест

public class PartitioningSpec {

    @Test
    public void partitionAdultsShouldSeparateKidsFromAdults() {
        Person sara = new Person("Sara", 4);
        Person viktor = new Person("Viktor", 40);
        Person eva = new Person("Eva", 42);
        List<Person> collection = asList(sara, eva, viktor);

        Map<Boolean, List<Person>> result = partitionAdults(collection);

        Map<Boolean, List<Person>> result2 = partitionAdults7(collection);

        assertThat(result.get(true)).hasSameElementsAs(asList(viktor, eva));
        assertThat(result.get(false)).hasSameElementsAs(asList(sara));
        assertThat(result2.get(true)).hasSameElementsAs(asList(viktor, eva));
        assertThat(result2.get(false)).hasSameElementsAs(asList(sara));
    }
    
}

Имплементации на Java 7 и Java 8

public class Partitioning {

    public static Map<Boolean, List<Person>> partitionAdults7(List<Person> people) {
        Map<Boolean, List<Person>> map = new HashMap<>();
        map.put(true, new ArrayList<>());
        map.put(false, new ArrayList<>());
        for (Person person : people) {
            map.get(person.getAge() >= 18).add(person);
        }
        return map;
    }

    // Partition stream of people into adults (age => 18) and kids
    public static Map<Boolean, List<Person>> partitionAdults(List<Person> people) {
        return people.stream()
                .collect(partitioningBy(p -> p.getAge() >= 18));
    }
}

Java 8 groupingBy

Нужно сгруппировать людей по национальности

Тест

public class GroupungSpec {

    @Test
    public void groupByNationatilityShouldSeparatePeopleByNationality() {
        Person sara = new Person("Sara", 4, "Norwegian");
        Person viktor = new Person("Viktor", 40, "Serbian");
        Person eva = new Person("Eva", 42, "Norwegian");
        List<Person> collection = asList(sara, eva, viktor);
        Map<String, List<Person>> result = groupByNationality(collection);
        assertThat(result.get("Norwegian")).hasSameElementsAs(asList(sara, eva));
        assertThat(result.get("Serbian")).hasSameElementsAs(asList(viktor));

        result = groupByNationality7(collection);
        assertThat(result.get("Norwegian")).hasSameElementsAs(asList(sara, eva));
        assertThat(result.get("Serbian")).hasSameElementsAs(asList(viktor));
    }
}

Имплементации на Java 7 и Java 8

public class Grouping {

    public static Map<String, List<Person>> groupByNationality7(List<Person> people) {
        Map<String, List<Person>> map = new HashMap<>();
        for (Person person : people) {
            if (!map.containsKey(person.getNationality())) {
                map.put(person.getNationality(), new ArrayList<>());
            }
            map.get(person.getNationality()).add(person);
        }
        return map;
    }

    public static Map<String, List<Person>> groupByNationality(List<Person> people) {
        return people.stream() // Convert collection to Stream
                .collect(groupingBy(Person::getNationality)); // Group people by nationality
    }
}

Java 8 joining

Необходимо вернуть имена людей, разделенные запятой

Тест

public class JoiningSpec {

    @Test
    public void toStringShouldReturnPeopleNamesSeparatedByComma() {
        Person sara = new Person("Sara", 4);
        Person viktor = new Person("Viktor", 40);
        Person eva = new Person("Eva", 42);
        List<Person> collection = asList(sara, viktor, eva);
        assertThat(namesToString7(collection))
                .isEqualTo("Names: Sara, Viktor, Eva.");
        assertThat(namesToString(collection))
                .isEqualTo("Names: Sara, Viktor, Eva.");
    }
}

Имплементации на Java 7 и Java 8

public class Joining {
    public static String namesToString7(List<Person> people) {
        String label = "Names: ";
        StringBuilder sb = new StringBuilder(label);
        for (Person person : people) {
            if (sb.length() > label.length()) {
                sb.append(", ");
            }
            sb.append(person.getName());
        }
        sb.append(".");
        return sb.toString();
    }

    public static String namesToString(List<Person> people) {
        return people.stream() // Convert collection to Stream
                .map(Person::getName) // Map Person to name
                .collect(joining(", ", "Names: ", ".")); // Join names
    }
}

Исходный код

Весь исходный код размещен на GitHub. Помимо тестов и имплементаций, репозиторий включает в себя pom.xml, который может быть полезен для скачивания зависимости Assertj и запуска тестов.

Запись Используем TDD в Java 8 Stream впервые появилась Блог Анатолия Корсакова.

]]>
https://akorsa.ru/2016/12/ispolzuem-tdd-v-java-8-stream/feed/ 0 5463
Как работает кэш второго уровня Hibernate? Часть 2 https://akorsa.ru/2016/12/kak-rabotaet-kesh-vtorogo-urovnya-hibernate-chast-2/ https://akorsa.ru/2016/12/kak-rabotaet-kesh-vtorogo-urovnya-hibernate-chast-2/#respond Thu, 15 Dec 2016 04:10:02 +0000 http://akorsa.ru/?p=5446 В первой части мы рассмотрели концепцию кэша второго уровня и способы его конфигурации. Во второй части рассмотрим стратегии кэширования и конфигурацию EhCache. Пример проекта в котором Вы можете посмотреть работу кэша и поэксперементировать находится на GitHub. Стратегии кэширования Под стратегией понимается, что можно делать над объектом кэша – нашей сущностью: изменять, удалять, вставлять, читать, давайте […]

Запись Как работает кэш второго уровня Hibernate? Часть 2 впервые появилась Блог Анатолия Корсакова.

]]>
В первой части мы рассмотрели концепцию кэша второго уровня и способы его конфигурации. Во второй части рассмотрим стратегии кэширования и конфигурацию EhCache.

Пример проекта в котором Вы можете посмотреть работу кэша и поэксперементировать находится на GitHub.

Стратегии кэширования

Под стратегией понимается, что можно делать над объектом кэша – нашей сущностью: изменять, удалять, вставлять, читать, давайте рассмотрим их:

  1. read-only — самая простая стратегия, кэш может только читаться, операции обновления (update) и удаления (delete) не разрешены, однако можно вставлять новые данные (insert) отлично подходит к кэшированию различных справочников,  например наименование регионов, городов, улиц … и т. д.
  2. nonstrict read-write — данные этого кэша могут меняться, возможен конкурентный доступ к одному и тому же объекту. Может произойти ситуация когда в кэше содержатся не последняя измененная сущность, т. е. данные сущности в кэше могут быть не равны данным в базе данных. Отсюда следует, что нужно избегать конкурентного доступа, точнее одни и те же записи не должны  редактировать 2 пользователя. Приведу пример: идеальный случаи это когда кадровики редактируют только свои данные по работникам:  1-й кадровик с 1 по 200 таб. номер, 2-й кадровик с 201 по 400 таб. номер и т. д. Но все же если есть конкурентный доступ к сущности,  то изменения сущности должны  происходить мгновенно. Не имеет никаких блокировок, поэтому есть шанс возвратить устаревшие данные из кэша.
  3. read-write — в целом похож на nonstrict read-write, позволяет более гибко настроить конкурентный доступ, поведение кэша зависит от настройки transaction isolation уровня базы данных, т. е. поведение изменения данных в кэше копирует поведение транзакции.  Использует блокировки, но асинхронно. Максимум, чего можно выжать — это «repeatable read transaction isolation» уровень базы данных. Применяется для не кластерного кэша, используется в основном для предотвращения считывания устаревших  данных с кэша.
  4. transactional — изменения в кэше и изменения в базе данных, полностью записываются в одной транзакции. У предыдущих двух стратегий была отложенная запись кэша, т. е. при изменении сущности, с начала, происходит блокировка в кэше (soft locked), после применения транзакции с некоторым опозданием выполняется замена старого значения в кэше, новым.  Уровень этого кэша — это «serializable transaction isolation»  уровень базы данных. Применяется только для распределенных кэшей, в управляемых транзакциях JTA. Полностью исключается чтение устаревших данных из кэша.

Обратите внимание что, чем менее строгую стратегию для кэша вы выбираете, тем больше затраты ресурсов  у кэша второго уровня.  Hibernate имеет стратегию кэша по умолчанию, для этого нужно использовать свойство hibernate.cache.default_cache_concurrency_strategy  в файле настроек hibernate.cfg.xml, для примера сделаем  стратегию кэша по умолчанию  read-write:

<property name="hibernate.cache.default_cache_concurrency_strategy">read-write</property>

Как настроить Ehcache?

Приведу пример конфигурационного файла ehcache.xml:


<?xml version="1.0" encoding="utf-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="true" monitoring="autodetect" dynamicConfig="true">
    <diskStore path="java.io.tmpdir"/>
    <defaultCache maxElementsInMemory="1" 
eternal="false" 
timeToIdleSeconds="120" 
timeToLiveSeconds="120" 
overflowToDisk="true" />
        
    <cache name="Article" 
maxElementsInMemory="500" 
eternal="true" 
timeToIdleSeconds="0" 
timeToLiveSeconds="100" 
overflowToDisk="false" />
</ehcache>

В этом файле, Вы можете индивидуально изменить конфигурации кэша для отдельных сущностей. Например, мы изменили конфигурацию кэша для сущности Article явно задав настройки.

Название атрибута Описание
diskStore Используется для определения пути, по которому создастся файловое хранилище для кэша на диске
name Имя региона кэша. Обязателен для элемента cache.
maxElementsInMemory Максимальное количество объектов сущностей, которые должны храниться всегда в памяти кэша.
eternal Могут ли храниться  объекты кэша в памяти ”бесконечно”  долго. – true да, false нет.
timeToIdleSeconds Промежуток времени обращения к объекту кэша, измеряется в секундах, если в течение этого времени не было обращения к объекту кэша, то сущность выгружается из кэша.  Если поставить значение в 0, то  объект кэша будет находиться в памяти  “бесконечно” долго.
timeToLiveSeconds Промежуток времени в секундах, в течение которого объект кэша может находиться в кэше, после истечения этого промежутка, объект выгружается из памяти кэша. Если поставить значение в 0, то  объект кэша будет находиться в памяти  “бесконечно” долго.
overflowToDisk Если превысило количество объектов кэша в памяти больше значения maxElementsInMemory, то выгружать ли данные объектов кэша, на диск в “файлы подкачки”. true да, false нет.
diskExpiryThreadIntervalSeconds Сколько секунд по времени после помещения  объекта кэша в “файл подкачки”, он может там находиться.
memoryStoreEvictionPolicy Параметр отвечает за процесс вытеснения объектов кэша, из кэша. Очередь, первым пришел, первым ушел — FIFO. По максимальному времени не использования объекта кэша в кэше – LRU.  По количеству использования объектов кэша, т. е. как часто используется объект в кэше,  и наименьше используемый объект кэша, выгружается из него – LFU.
maxElementsOnDisk Максимальное количество объектов кэша, которое может содержаться  в “файле подкачки”.
diskPersistent Допустим, вы перезапускаете свое приложение, нужно ли сохранять кэш на диске и после  перезапуска восстанавливать кэш, true да, false нет.

Очищение кэша второго уровня (Evict)

Для того что удалить полностью кэш второго уровня, мы можем использовать следующий код:


@Autowired
private SessionFactory sessionFactory;
public void evictAll() {
 SessionFactory sf = currentSession().getSessionFactory();
 Cache cache = sf.getCache();
 cache.evictQueryRegions();
 cache.evictDefaultQueryRegion();
 cache.evictCollectionRegions();
 cache.evictEntityRegions();
}
protected Session currentSession() {
 return sessionFactory.getCurrentSession();
}

Если мы хотим удалить одну сущность, все сущности или коллекцию из кэша, мы можем сделать это так:


SessionFactory sf = currentSession().getSessionFactory();
cache.evictEntity(Cat.class, catId); //evict a particular Cat
cache.evictEntityRegion(Cat.class); //evict all Cats
cache.evictCollection("Cat.kittens", catId); //evict a particular collection of kittens
cache.evictCollectionRegion("Cat.kittens"); //evict all kitten collections

Заключение

Разные типы данных требуют разных политик кэширования: Различия в соотношении операции чтения и записи, различия в размерах таблиц в БД, и некоторые таблицы пошарены с другими внешними приложениями. Кэш второго уровня конфигурируется на уровне класса или коллекции. Это позволит Вам, например, включить кэширование для классов, для которых операции чтения преобладают и выключить, для данных, которые представляют финансовые показатели.

Политики кэширования определяются следующими шагами:

■ На каких классах включить кэширование

■ Определиться со стратегией кэширования

■ Политики в отношении времени жизни кэша (смотри настройки EhCache)

■ Физический формат кэша (память, файлы-индексы, кластеры и репликации)

Кэширование полезно если:

  • Вы пишите в БД только через Hibernate (это необходимо для единого пути обнаружения и инвалидации данных)
  • Вы часто читаете объекты
  • Вы имеете одну ноду, и не имеете репликацию. Иначе, Вам необходимо будет реплицирвоать сам кэш (например, с помощью JGroups) что добавит сложности и затруднит масштабирование.

Когда работает кэш?

  • Когда вы вызываете методы session.get() или session.load() для объекта, который до этого размещен в кэше. Кэш это хранилище, в котором ID это ключ и свойства сущности являются значением. Так что получить сущность из кэша возможно только по его ID.
  • Когда Ваши ассоциации загружаются lazy (или eager с селектами вместо joins)

Не работает когда:

  • Если Вы делаете выборку не по ID. Если Вы делаете запрос как: from Authors where name = :name, то тогда запрос не попадет в кэш.
  • Когда вы используете HQL (даже если задавать where id = ?).
  • Если в маппинге проставлено fetch="join", это означает что надо выгружать ассоциацию с помощью джойнов везде, вместо отдельных запросов селекта. Кэш работает, только если используется fetch="select".

Пример проекта в котором Вы можете посмотреть работу кэша и поэксперементировать находится на GitHub.

 

 

Запись Как работает кэш второго уровня Hibernate? Часть 2 впервые появилась Блог Анатолия Корсакова.

]]>
https://akorsa.ru/2016/12/kak-rabotaet-kesh-vtorogo-urovnya-hibernate-chast-2/feed/ 0 5446
Как работает кэш второго уровня Hibernate? Примеры https://akorsa.ru/2016/12/kak-rabotaet-kesh-vtorogo-urovnya-hibernate-primery/ https://akorsa.ru/2016/12/kak-rabotaet-kesh-vtorogo-urovnya-hibernate-primery/#respond Fri, 09 Dec 2016 18:25:28 +0000 http://akorsa.ru/?p=5412 Введение Кэширование это возможность предоставляемая ORM фреймворками, которая помогает пользователям достичь высокую скорость, и в то же время помогает самим фреймворкам уменьшить количество обращений к БД. Hibernate предоставляет эту функциональность на двух уровнях. Кэш первого уровня в Hibernate включен по умолчанию и работает на уровне сессии. Прочтите здесь подробнее о нем. Кэш второго уровня не включен […]

Запись Как работает кэш второго уровня Hibernate? Примеры впервые появилась Блог Анатолия Корсакова.

]]>
Введение

Кэширование это возможность предоставляемая ORM фреймворками, которая помогает пользователям достичь высокую скорость, и в то же время помогает самим фреймворкам уменьшить количество обращений к БД. Hibernate предоставляет эту функциональность на двух уровнях.

  • Кэш первого уровня в Hibernate включен по умолчанию и работает на уровне сессии. Прочтите здесь подробнее о нем.
  • Кэш второго уровня не включен по умолчанию и доступен глобально на уровне Session Factory.

Один раз, попав в кэш второго уровня объект-сущность, используется на всем протяжении жизни объекта sessionFactory, т.е. область действия кэша  — вся наша программа, а если быть точнее, то, как вы настроите свой кэш, так он себя и поведет. Это также означает, что если фабрика сессий закроется, весь кэш ассоциированный с ней «умрет» и кэш менеджер тоже выключится.

В отличие от кэша первого уровня кэш второго уровня нужно включать непосредственно в настройках Hibernate, и он реализуется провайдером,  сторонней библиотекой кэшем.

Так же это означает что если у Вас есть два инстанса фабрики сессий (нормальное приложение конечно не будет так разрабатываться), Вы будете иметь два кэш менеджера в Вашем приложении и можно получить непредсказуемые результаты.

flush1

В этом туториале я расскажу концепцию кэша второго уровня и приведу примеры использования. Вторую часть вы можете прочитать здесь.

Но перед этим разъясню понятие «инвалидация«.  В документации часто встречается понятие инвалидация (invalidated) кэша,  это устаревание данных в кэше,  после того как кэш перешел в это состояние он должен быть очищен от старых данных методом удаления и заново обновлен новыми значениями, после обновления, кэш переходит в  валидное(validate) состояние.

Как работает кэш второго уровня

Давайте напишем все факты:

  1. Кэш первого уровня в первую очередь кэширует сущности, но поддерживает опционально кэш запросов.
  2. Всякий раз когда сессия Hibernate пытается загрузить сущность, самым первом местом где она будет ее искать это кэшированная копия сущности из кэша первого уровня (ассоциированный с частичной сессией).
  3. Если кэшированная копия сущности присутствует в кэше первого уровня, она вернется как результат метода load().
  4. Если не найдена в кэше первого уровня, то ищется в кэше второго уровня (если он включен).
  5. Если в кэше второго уровня имеется кэшированная сущность, она вернется как результат метода load(). Но, прежде чем вернуть сущность, она сохранится в кэше первого уровня так же, поэтому следующий вызов метода загрузки сущности вернет сущность из кэша первого уровня и больше не понадобится искать ее в кэше второго уровня опять.
  6. Если же сущность не была найдена ни в кэше первого уровня, ни в кэше второго уровня, то выполнится запрос в БД и сущность сохранится в обоих кэшах, прежде чем вернется как результат вызова метода load().
  7. Кэш второго уровня  сам следит за обновлениями сущностей из кэша, если модификации были выполнены через Session API.
  8. Если некоторый пользователь или процесс делает изменения прямо в БД, то кэш второго уровня нет имеет возможности обновиться до времени «timeToLiveSeconds» действующего для данного региона кэша. В это случае, хорошей идеей является инвалидировать весь кэш и позволить Hibernate построить его кэш заново. Вы можете использовать следующий код для инвалидации всего кэша второго уровня:

/**
* Evicts all second level cache hibernate entites. This is generally only
* needed when an external application modifies the database.
*/
public void evict2ndLevelCache() {
try {
    Map<String, ClassMetadata> classesMetadata = sessionFactory.getAllClassMetadata();
        for (String entityName : classesMetadata.keySet()) {
            logger.info("Evicting Entity from 2nd level cache: " + entityName);
            sessionFactory.evictEntity(entityName);
        }
    } catch (Exception e) {
           logger.logp(Level.SEVERE, "SessionController", "evict2ndLevelCache", "Error evicting 2nd level hibernate cache entities: ", e);
     }
}

Внутренности кэша второго уровня

Каждая сущность хранится как CacheEntry и каждая сущность преобразуется в hydrated состояние для создания записи в кэше. В терминах Hibernate, гидратация это когда JDBC ResultSet преобразуется в массив сырых значений:

 
final Object[] values = persister.hydrate( rs, id, object, rootPersister, cols, eagerPropertyFetch, session ); 

То есть он хранит информацию в виде массивов строк, чисел и т. д. И идентификатор объекта выступает указателем на эту информацию. Концептуально это нечто вроде Map, в которой id объекта — ключ, а массивы данных — значение. Приблизительно можно представить себе это так:

 *-----------------------------------------*
 | Person Data Cache |
 |-----------------------------------------|
 | 1 -> [ "John" , "Q" , "Public" , null ] |
 | 2 -> [ "Joey" , "D" , "Public" , 1 ]  |
 | 3 -> [ "Sara" , "N" , "Public" , 1 ]  |
 *-----------------------------------------*

«Гидратированное» состояние сохраняется в текущем запущенном Persistence Context как объект EntityEntry, который инкапсулирует снэпшот сущности, снятую во время загрузки. Затем это состояние используется для:

  • дефолтного механизма «грязной» проверки, в котором сравниваются текущие данные сущности против снэпшота снятого во время загрузки
  • кэша второго уровня, в котором записи в кэше строятся из этого гидратированного состояния.

Обратная операция называется дегидратация и она копирует состояние сущности в выражения INSERT и UPDATE.

Как работает кэш запросов (Query cache)?

Кэш запросов выглядит концептуально как хэш мапа, где ключом выступает композиция из текста запроса и значений параметров, и значение это список Id сущностей, которые подходят запросу:

 *----------------------------------------------------------*
 | Query Cache | 
 |----------------------------------------------------------|
 | ["from Person where firstName=?", ["Joey"] ] -> [1, 2] ] |
 *----------------------------------------------------------*

Некоторые запросы не возвращают сущности, вместо них они возвращают только примитивные значения. В этом случае сами значения будут сохранены в кэше запросов. Кэш запросов заполняется когда выполняется кэшируемый JPQL/HQL запрос.

Как связаны кэш второго уровня и кэш запросов?

Если выполняемый запрос имеет предыдущие закэшированные результаты, то SQL запрос к БД не будет послан. Вместо этого результаты запроса стянуться из кэша запросов, и затем закэшированный идентификатор сущности будет использован для получения из кэша второго уровня.

Если кэш второго уровня содержит данные для данного ID, он дегидрирует сущность и вернет ее. Если кэш второго уровня не содержит результаты для данного ID, то тогда SQL запрос будет выполнен для загрузки сущности из БД.

Как включить и настроить кэш второго уровня?

Первый шаг это включить зависимость hibernate-ehcache в вашем pom файле.


<dependenc>
<groupId>org.hibernate</groupId>
 <artifactId>hibernate-ehcache</artifactId>
 <version>SOME-HIBERNATE-VERSION</version>
</dependency>

По умолчанию, кэш второго уровня выключен и для активации его, мы должны выставить следующие свойства Hibernate:


properties.put("hibernate.cache.use_second_level_cache",
      Boolean.TRUE.toString());
properties.put("hibernate.cache.region.factory_class",
      "org.hibernate.cache.ehcache.EhCacheRegionFactory");

или в xml конфигурации:


<prop key="hibernate.cache.use_second_level_cache">true</prop>
<prop key="hibernate.cache.use_query_cache">true</prop>
<prop key="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</prop>
<prop key="net.sf.ehcache.configurationResourceName>your-cache-config.xml</prop>

RegionFactory задает провайдера кэша второго уровня и  свойство hibernate.cache.region.factory_class обязательно к заполнению при выставленном в значение true свойстве hibernate.cache.use_second_level_cache.

Следующим шагом будет сконфигурировать настройки кэша регионов в файле your-cache-config.xml, описание настроек приведено во второй части:


<xml version="1.0">
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" updateCheck="false" xsi:noNamespaceSchemaLocation="ehcache.xsd" name="yourCacheManager">

<diskStore path="java.io.tmpdir"/>

<cache name="yourEntityCache" maxEntriesLocalHeap="10000" eternal="false" overflowToDisk="false" timeToLiveSeconds="86400" />

<cache name="org.hibernate.cache.internal.StandardQueryCache" maxElementsInMemory="10000" eternal="false timeToLiveSeconds="86400" overflowToDisk="false" memoryStoreEvictionPolicy="LRU" />

<defaultCache maxElementsInMemory="10000" eternal="false" timeToLiveSeconds="86400" overflowToDisk="false" memoryStoreEvictionPolicy="LRU" />
</ehcache>

Для включения кэширования на уровне сущностей, мы должны проставить аннотацию над кэшируемой сущностью:


@Entity
@org.hibernate.annotations.Cache(usage =
      CacheConcurrencyStrategy.NONSTRICT_READ_WRITE, region = "yourEntityCache")
public class SomeEntity {
}

Ассоциации так же могут быть закэшированы кэшем второго уровня, но по умолчанию это не включено. Для кэширования ассоциаций нам нужно установить аннотацию @Cache к самой ассоциации:


@Entity
public class SomeEntity {
@OneToMany
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY, region="yourCollectionRegion")
private Set<OtherEntity> other;
}

JPA также предоставляет аннотацию @Cacheable, но она не поддерживает установку свойства concurrency strategy на уровне сущности.

Как использовать кэш запросов?

По умолчанию при кэшировании запросов hibernate  использует два внутренних кэша:

  • org.hibernate.cache.StandardQueryCache, кэш используется, если вы явно не указали в запросе, какой регион/имя кэша использовать, содержит в себе результаты кэшированных запросов. По умолчанию hibernate  не инвалидирует кэш StandardQueryCache(SQC), т. е. не удаляет его, когда он устаревает или когда были произведены манипуляции с сущностями в кэше, допустим обновление или удаление сущностей.
  • org.hibernate.cache.UpdateTimestampsCache, содержит временные метки (timestamps) самых последних изменений кэшируемых таблиц.  Необходим для валидации результатов, которые он получил из кэша запросов.

 

После конфигурирования кэша запросов, по умолчанию ни один запрос не закэшируется. Запросы нужно явно отметить, например:


@NamedQuery(name="account.queryName",
     query="select acct from Account ...",
     hints={
        @QueryHint(name="org.hibernate.cacheable",
        value="true")
      }
})

 

И пример как отметить Criteria запрос как кэшируемый:

List cats = session.createCriteria(Cat.class)
 .setCacheable(true)
 .list();

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

Запись Как работает кэш второго уровня Hibernate? Примеры впервые появилась Блог Анатолия Корсакова.

]]>
https://akorsa.ru/2016/12/kak-rabotaet-kesh-vtorogo-urovnya-hibernate-primery/feed/ 0 5412
Как работает кэш первого уровня в Hibernate? Примеры https://akorsa.ru/2016/11/kak-rabotaet-kesh-pervogo-urovnya-v-hibernate-primery/ https://akorsa.ru/2016/11/kak-rabotaet-kesh-pervogo-urovnya-v-hibernate-primery/#respond Fri, 25 Nov 2016 04:40:33 +0000 http://akorsa.ru/?p=5403 Введение Кэширование это возможность предоставляемая ORM фреймворками, которая помогает пользователям достичь высокую скорость, и в то же время помогает самим фреймворкам уменьшить количество обращений к БД. Hibernate достигает вторую цель вводя кэш первого уровня. Кэш первого уровня в Hibernate включен по умолчанию и Вам не нужно ничего предпринимать, чтобы включить его. Даже выключить его не […]

Запись Как работает кэш первого уровня в Hibernate? Примеры впервые появилась Блог Анатолия Корсакова.

]]>
Введение

Кэширование это возможность предоставляемая ORM фреймворками, которая помогает пользователям достичь высокую скорость, и в то же время помогает самим фреймворкам уменьшить количество обращений к БД. Hibernate достигает вторую цель вводя кэш первого уровня.

Кэш первого уровня в Hibernate включен по умолчанию и Вам не нужно ничего предпринимать, чтобы включить его. Даже выключить его не удастся. На самом деле, в роли кэша в сессии выступает Persistence Context. В приложении Hibernate, мы говорим что одна сессия имеет один внутренний persistence context. В JPA приложении, EntityManager имеет так же свой persistence context.

Persistence контекст полезен по нескольким причинам:

  • Hibernate может делать автоматические dirty checking и транзакции
  • Hibernate может использовать persistence контекст как кэш первого уровня
  • Hibernate может гарантировать уникальность Java-объекта в масштабе сессии
  • Слой persistence не уязвим к переполнениям стека в случае зацикленных ссылок в графе объектов
  • Никогда не может быть противоречивых представлений одной и той же строки в БД в конце единицы работы
  • Изменения совершенные в едином persistence контексте всегда видны сразу для всего остального исполняемого кода внутри persistence контекста и его единицы работы (гарантируются повторные чтения).

Легче понимать кэш первого уровня, если мы поймем тот факт, что он ассоциирован с объектом Session. Как мы знаем объект сессии создается по запросу от SessionFactory и кэш уничтожается в тот момент, когда сессия закрывается. Кэш первого уровня также называется Кэшем уровня сессии, означая что он работает для сессии только, в случае если один и тот же запрос будет выполнен два или более раз в одной сессии он выдаст данные из БД только для первого запроса и вернет те же самые данные из кэша для всех входящих подобных запросов.

Как работает кэширование первого уровня?

Кэш первого уровня связан с сессией только, когда запрос выполняется первый раз он выдает данные из БД и сохраняет результат запроса в кэш. Данные сохраненные в кэше при первом запросе будут выдываться и для всех входящих подобных запросов, пока сессия жива.

hibernate-first-level-caching

Как только сессия закроется весь кэш первого уровня будет очищен. Мы можем удалить частично загруженную сущность из кэша при помощи метода evict() и весь кэш может быть очищен используя метод clear(). Как только кэш очистится при вызове одного из двух методов свыше, он будет делать запрос к БД снова для нового вызова даже если сессия еще не закрыта.

Пример:


package ru.akorsa.sample;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

import ru.akorsa.models.Employee;
import ru.akorsa.utils.HibernateUtils;
public class CachingImpl {
public static void main(String[] args) {
SessionFactory sessionFactory = HibernateUtils.getSessionFactory();

// create an employee object to persist in DB
Employee employee = new Employee("eddy", "smith",
"eddy@akorsa.ru", "9898787676");

// lets save few data in Employee table
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
session.save(employee);
transaction.commit();
session.close();

System.out.println("****Data creation completed****");

Session session1 = sessionFactory.openSession();
System.out.println("****Created Session 1****");
transaction = session1.beginTransaction();

employee = (Employee) session1.load(Employee.class, 1l);
System.out.println(employee.getFirstName());

employee = (Employee) session1.load(Employee.class, 1l);
System.out.println(employee.getFirstName());

employee = (Employee) session1.load(Employee.class, 1l);
System.out.println(employee.getFirstName());

transaction.commit();
session1.close();
System.out.println("****Closed Session 1****");

Session session2 = sessionFactory.openSession();
System.out.println("****Created Session 2****");
transaction = session2.beginTransaction();

employee = (Employee) session2.load(Employee.class, 1l);
System.out.println(employee.getFirstName());

transaction.commit();
session2.close();
System.out.println("****Closed Session 2****");

}
}

Результат:

first-level-cache-example-1

Вы можете видеть, что в сессии под номер 1 мы запросили сущность три раза, но запрос в БД был выполнен только один раз и для второго и третьего запроса были получены данные сохраненные в кэше. Мы закрываем сессию 1 и открываем вторую сессию, в этот раз мы запросили эту же сущность из БД, но был сделан запрос прямо в БД, потому что кэшированные данные очистились, когда сессия 1 закрылась.

Как работают методы evict() и clear() в Hibernate

Мы не можем выключить кэш первого уровня в Hibernate, но мы можем удалить некоторую конкретную сущность из кэша с помощью evict() и весь кэш с помощью clear(). Пример ниже:


package ru.akorsa.sample;

import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

import ru.akorsa.models.Employee;
import ru.akorsa.utils.HibernateUtils;
public class EvictImpl {
public static void main(String[] args) {
SessionFactory sessionFactory = HibernateUtils.getSessionFactory();

// create an employee object to persist in DB
Employee employee = new Employee("eddy", "smith",
"eddy@beingjavaguys.com", "9898787676");

// lets save few data in Employee table
Session session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
session.save(employee);
transaction.commit();
session.close();

System.out.println("****Data creation completed****");

Session session1 = sessionFactory.openSession();
System.out.println("****Created Session****");
transaction = session1.beginTransaction();

employee = (Employee) session1.load(Employee.class, 1l);
System.out.println(employee.getFirstName());

employee = (Employee) session1.load(Employee.class, 1l);
System.out.println(employee.getFirstName());

// removed employee entity from the cache
session1.evict(employee);
System.out.println("****removed employee entity from the cache****");

employee = (Employee) session1.load(Employee.class, 1l);
System.out.println(employee.getFirstName());

transaction.commit();
session1.close();
System.out.println("****Closed Session****");

}
}

Результат:

first-level-cache-example-2

Запись Как работает кэш первого уровня в Hibernate? Примеры впервые появилась Блог Анатолия Корсакова.

]]>
https://akorsa.ru/2016/11/kak-rabotaet-kesh-pervogo-urovnya-v-hibernate-primery/feed/ 0 5403
Как работает кэш в Hibernate? https://akorsa.ru/2016/11/kak-rabotaet-kesh-v-hibernate/ https://akorsa.ru/2016/11/kak-rabotaet-kesh-v-hibernate/#respond Fri, 18 Nov 2016 04:49:21 +0000 http://akorsa.ru/?p=5397 Введение При разработке приложений, связанных с ORM, я уже сталкивался с ситуациями, когда производительность приложения падала по причине большого траффика к БД. Hibernate единственный ORM фреймворк, которые помогает преодолеть такие сложные ситуации с помощью кэширования. В этой статье я попытаюсь дать детальное описание этой функции. Как работает кэш Hibernate? В веб-приложениях , используя Hibernate, кэш […]

Запись Как работает кэш в Hibernate? впервые появилась Блог Анатолия Корсакова.

]]>
Введение

При разработке приложений, связанных с ORM, я уже сталкивался с ситуациями, когда производительность приложения падала по причине большого траффика к БД. Hibernate единственный ORM фреймворк, которые помогает преодолеть такие сложные ситуации с помощью кэширования. В этой статье я попытаюсь дать детальное описание этой функции.

Как работает кэш Hibernate?

В веб-приложениях , используя Hibernate, кэш на самом деле хранит данные уже загруженные из БД. Это помогает уменьшить трафик между нашим приложением и БД. Большинство случаев извлечения данных попадает на кэш. Всякий раз когда нужны новые данные, будет сделан запрос в БД. Поскольку время, необходимое для доступа к БД, больше по сравнению с временем доступа к кэшу, трафик будет уменьшен между приложением и БД. В кэше хранятся только данные, связанные с текущим запущенным приложением. Следовательно, кэш должен быть очищен всякий раз когда приложение изменяется. Давайте рассмотрим как это все делается ниже.

Hibernate использует следующие кэши для объектов:

hibernatecaching02

  • First-level cache (Кэш первого уровня): Он всегда связан с объектом Session. Это обязательный кэш, через который проходят все запросы. Hibernate использует этот кэш по умолчанию. Если у Вас возникла необходимость много раз обновлять объект, Hibernate пытается задержать выполенение этих обновлений насколько это возможно для уменьшения количества вызовов SQL UPDATE. Он завершает эту транзакцию только в конце транзакции.
  • Second-level cache (Кэш второго уровня): Он всегда связан с объектом Session Factory. Это опциональный кэш. Кэш второго уровня создается на уровне session factory и доступен для использования во всех сессиях, которые создаются используя конкретную данную фабрику сессий. Кэш вторго уровня может быть сконфигурирован отдельно для каждого класса и коллекции и в основном отвечает за кэширование между сеансами. Это означает, что в момент когда session factory закроется, весь кэш связанный с ним очиститься и cache менеджер также закроется.
  • Query Cache (Кэш запросов): Кэш запросов используется для кэширования результатов запроса. Когда кэш запроса включен, результаты запроса сохраняются  вместе с комбинацией запросов и параметров вызова. Каждый раз запрос вызовет проверку на наличие результата у кэш менеджера. Если результаты найдены в кэше, они возвращаются, иначе инициализируется транзакция в БД.
  • Любой сторонний кэш может быть использован в Hibernate. Предоставляется интерфейс org.hibernate.cache.CacheProvider, который должен быть имплементирован для использования стороннего провайдера кэша.

Стратегии конкурентности

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

Read-only (Только чтение)

  • Полезно для данных, которые часто читаются, но никогда не обновляются.
  • Простая в использовании
  • Лучшая производительность среди всех
  • Безопасно для использования в кластере.

Пример для использования стратегии read-only.

<hibernate-mapping>
<class name="Student" table="STUDENT">
<meta attribute="class-description"> 
This class contains the STUDENT detail. 
</meta>
<cache usage="read-only"/>
....
</class>
..........
</hibernate-mapping>

Использование атрибута «read-only» сообщает Hibernate что нужно использовать стратегию read-only для заданного кэша.

Read-Write (Запись-чтение)

  • Используется в случае, когда наши данные необходимо обновлять.
  • Есть больше оверхеда чем при применении стратегии read-only
  • Не используется если требуется уровнь изоляции Serializable
  • В среде JTA, для получения JTA Transaction Manager мы должны заполнить свойство hibernate.transaction.manager_lookup_class
  • Для использования в кластере имплементация кэша должна поддерживать блокировку.

Пример для использования данной стратегии:


<hibernate-mapping>
<class name="Student" table="STUDENT">
<meta attribute="class-description"> 
This class contains the STUDENT detail. 
</meta>
<cache usage="read-write"/>
....
</class>
..........
</hibernate-mapping>

Nonstrict read-write (Нестрогое чтение-запись)

  • Необходимо использовать, если приложение редко обновляет данные
  • Мы должны заполнить hibernate.transaction.manager_lookup_class для использования в JTA
  • Транзакция завершается, когда вызывается Session.close() или Session.disconnect() в других средах (кроме JTA)

Пример:


<class name="abc.xyz" .... >

<cache usage=" nonstrict-read-write"/>

….

</class>

Transactional

Используйте эту стратегию для данных, которые в большинстве случаев читаются и также где является критичным предотвращение протухания данных в конкурирующих транзакциях, в редком случае обновления.

Имплементации кэша

Теперь мы поняли как выбрать стратегии, и надо выбрать провайдера кэша. Hibernate принуждает Вас выбрать единственного провайдера кэша на все приложение. Каждый провайдер кэша не совметсим с каждой стратегией. Hibernate подерживает следующие опенсорсные имлементации кэша: EHCache (Easy Hibernate Cache), OSCache (Open Symphony Cache), Swarm Cache, JBoss Tree Cache.

Давайте рассмотрим каждого:

EHCache (Easy Hibernate Cache)

  • Быстрый, легковесный и легкий в использовании
  • Поддерживает только стратегии read-only и read-write
  • Поддерживает кэширование на диске и в памяти
  • Не поддерживает кластеризацию

OSCache (Open Symphony Cache) (org.hibernate.cache.OSCacheProvider)

  • Мощный и гибкий
  • Поддерживает только стратегии read-only и read-write
  • Поддерживает кэширование на диске и в памяти
  • Обеспечивает базовую поддержку для кластеризации через JavaGroups или JMS

SwarmCache (org.hibernate.cache.SwarmCacheProvider)

  • Кэширование основанное на кластеризации
  • Поддерживает read-only стратегию или нетстрогое чтение-запись
  • Подходит к приложениям, у которых количество операций чтения больше чем операций записи

TreeCache (org.hibernate.cache.TreeCacheProvider) из JBoss

  • мощный, реплицируемый (синхронно или асинхронно) и транзакционный кэш
  • полезный, когда нам нужно правильная транзакционная архитектура кэша

Следующая таблица совместимостей поможет Вам выбрать подходящую комбинацию.

Cache Provider Read-Only Nonstrict Read-Only Read-Write Transactional
EHCache Yes Yes Yes
OSCache Yes Yes Yes
SwarmCache Yes Yes
TreeCache Yes Yes

Вывод

В этой статье мы рассмотрели что такое кэширование Hibernate, типы конкурентных стратегий. Мы также рассмотрели доступных провайдеров кэша и как их выбрать. Спасибо за внимание!

Запись Как работает кэш в Hibernate? впервые появилась Блог Анатолия Корсакова.

]]>
https://akorsa.ru/2016/11/kak-rabotaet-kesh-v-hibernate/feed/ 0 5397
Стратегии загрузки графа объектов в JPA (Часть 2 и вывод) https://akorsa.ru/2016/11/strategii-zagruzki-grafa-obektov-v-jpa-chast-2-i-vyvod/ https://akorsa.ru/2016/11/strategii-zagruzki-grafa-obektov-v-jpa-chast-2-i-vyvod/#respond Wed, 02 Nov 2016 17:00:51 +0000 http://akorsa.ru/?p=5383 В первой части  мы рассмотрели статические стратегии загрузки графа объектов. Теперь рассмотрим динамические и сделаем выводы. Примеры и тесты доступны на гитхаб. Стратегии динамической загрузки Стратегии динамической загрузки различаются тем, насколько они динамичны на самом деле. Вообще говоря, все эти стратегии позволяют Вам решить в рантайме какие из частей графа нужно выгрузить, в то время как […]

Запись Стратегии загрузки графа объектов в JPA (Часть 2 и вывод) впервые появилась Блог Анатолия Корсакова.

]]>
В первой части  мы рассмотрели статические стратегии загрузки графа объектов. Теперь рассмотрим динамические и сделаем выводы.

Примеры и тесты доступны на гитхаб.

Стратегии динамической загрузки

Стратегии динамической загрузки различаются тем, насколько они динамичны на самом деле. Вообще говоря, все эти стратегии позволяют Вам решить в рантайме какие из частей графа нужно выгрузить, в то время как вышеупомянутые стратегии не поддерживают этого, потому что они сконфигурированы через маппинг во время компиляции или через константные запросы в виде строк.

Именованные графы сущностей

Единственный способ для определения нескольких стратегий динамической загрузки это использовать NamedEntityGraph, который может быть сконфигурирован как аннотация над сущностью, например:


@NamedEntityGraph(name = "user.cars", attributeNodes = @NamedAttributeNode("cars")
@Entity
@Table(name = "usr")
public class User {
}

Граф сущности имеет два важных атрибута: уникальное имя и attributeNodes, который описывает части графа объектов, которые нужно загрузить. Данный граф задает то, что всякий раз когда загружается сущность user, его машины должны быть тоже выгружены. Почему это динамически происходит? Я должен задать граф как аннотацию и он не может измениться в рантайме. Теперь я покажу, как использовать этот граф. Вы можете использовать его на каждой операции по разному:


@Test
public void testInitCarsByEntityGraph() {
    User user = entityManager.find(User.class, 1L,
    Collections.singletonMap("javax.persistence.fetchgraph",
        entityManager.getEntityGraph("user.cars")));
    user.getCars().stream().forEach(System.out::println);

}

Есть свойство javax.persistence.fetchgraph позволяющая Вам передать данный граф сущностей для использования в операции find. В этом примере, мы использовали метод find класса EntityManager. Вы так же можете установить граф, когда используете запрос JPQL:


@Test
public void testInitByNamedEntityGraphInJPQL() {
    TypedQuery<User> loadUserQuery = entityManager.createQuery("select usr from User usr
       where usr.id = :id", User.class);
    loadUserQuery.setParameter("id", 1L);
    loadUserQuery.setHint("javax.persistence.fetchgraph",
         entityManager.getEntityGraph("user.cars"));
    User user = loadUserQuery.getSingleResult();
    user.getCars().stream().forEach(System.out::println);

}

Граф в этом примере передается через метод setHint, который задан в классе TypedQuery.

Вы так же можете иметь несколько разных графов и выбрать нужный граф, в зависимости от некоторых условий, или Вы можете передать имя графа как параметр для репозитория или DAO. Именно поэтому я категоризирую эти графы как динамические.

Однако, именованные графы все еще имеют лимит, потому что я могу использовать только один граф на одну операцию одновременно.

Не позволяется смешивать их. Следовательно, Вы потенциально можете столкнуться с огромным количеством разных графов, представляющих все комбинации ребер графа, которые должны быть загружены. Если у Вас именно такой случай, то Вы должны использовать динамические графы сущностей.

Динамические графы сущностей

Динамические графы объектов могут быть проинициализированы в рантайме. Если Ваше приложение должно быть очень гибким при загрузке разных графов, Вам нужно выбрать эту концепцию. Следующий тест покажет, как использовать их:


@Test
public void testInitDynamicEntityGraph() {
    EntityGraph<User> graph = entityManager.createEntityGraph(User.class);
        graph.addAttributeNodes("cars");
    User user = entityManager.find(User.class, 1L,
    Collections.singletonMap("javax.persistence.fetchgraph", graph));
    user.getCars().stream().forEach(System.out::println);

}

В этом примере мы создаем новый EntityGraph через EntityManager.createEntityGraph. Для этого графа мы добавляем attributeNodes  = cars. Инстанс затем передается в javax.persistence.fetchgraph свойство опять, как и в примере именованного графа сущностей.

Criteria API

Самый мощный и гибкий способ задавать запросы с JPA это использовать JPA Criteria API. Criteria API позволяет Вам задавать целый запрос динамически в рантайме. Типичный пример использования — фильтр для поиска. Представьте, что у Вас есть разные атрибуты фильтра и всякий раз когда Вы записываете какое-то значение в форму для фильтра Вы хотите добавить тест, в котором этот атрибут проверяется на равенство заданному значению из части «where» запроса. Самый очевидный подход связан  с конкатенации строк. Но это не безопасно и вероятно неэффективно. Следовательно, Вы можете использовать JPA Criteria API, как показано в примере:


@Test
public void testInitByExplicitFetchJoinInJPACriteria() {
    CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> query = builder.createQuery(User.class);
    Root<User> root = query.from(User.class);
    root.fetch("cars", JoinType.LEFT);
    CriteriaQuery<User> criteriaQuery =
        query.select(root).where(builder.and(builder.equal(root.get("id"), 1L)));
    User user = entityManager.createQuery(criteriaQuery).getSingleResult();
    user.getCars().stream().forEach(System.out::println);

}

В этом примере, Вы сперва создаете объект CriteriaBuilder. Этот объект позволяет Вам создавать новый CriteriaQuery объект. Далее, создаем источник запроса (query root). На этом объекте вызываем fetch для машин. Затем Вы задаете что нужно выбрать и как должно задаваться where. Для того чтобы сделать запрос безопасным Вы можете использовать также каноническую мета модель. Это позволит заменить строковые «cars» и «id» на безопасные выражения как root.get(User_.id) или root.get(User_.cars). Для того чтобы динамически загружать объекты из графа Вы можете добавлять дальнейшие части графа, например, root.fetch("cars", JoinType.LEFT).fetch("axis", JoinType.LEFT) соединяя вызовы метода fetch.

C другой стороны, Вы можете даже комбинировать именованные графы или динамически графы с criteria API:


@Test
public void testInitByNamedEntityGraphInJPACriteria() {
    CriteriaBuilder builder = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> query = builder.createQuery(User.class);
    Root<User> root = query.from(User.class);
    CriteriaQuery<User> criteriaQuery =
        query.select(root).where(builder.and(builder.equal(root.get("id"), 1L)));
    User user = entityManager.createQuery(criteriaQuery)
        .setHint("javax.persistence.fetchgraph",
    entityManager.getEntityGraph("user.cars")).getSingleResult();
    user.getCars().stream().forEach(System.out::println);

}

Как выбрать стратегию?

Резюмируя, я хочу обсудить преимущества и недостатки представленных стратегий и гайд для выбора подходящей стратегии.

Lazy Loading

Преимущества:

  • Вы выгружаете только те объекты, которые прямо запросили
  • Вы не должны думать о конкретной стратегии выгрузки
  • Ваш корневой узел загружается быстро

Недостатки:

  • Можно получить большие задержки при выгрузки объектов, которые имеют длинный путь в графе
  • Нельзя использовать джойны SQL для загрузки частей графа, Вы производите несколько выражений select
  • Вы не гибки, потому что Ваша стратегия задана глобальна на уровне маппинга

Нужно использовать если:

  • Ваш фронтенд работает на той же самой JVM, что и бэкенд с Hibernate
  • Вы не можете предвидеть какие из частей графа нужны в каждой ситуации
  • Вы хотите, чтоб приложение стартовало быстро и распределяете нагрузку во времени между последующими действиями пользователя

Вы не должны использовать если:

  • Ваш фронтенд вызывает бэкенд Hibernate удаленно
  • очевидно какие именно части графа должны быть загружены
  • Вы можете пренебречь временем запуска приложения и предзагрузить большинство данных

Eager Loading

Преимущества:

  • Вы явно загружаете все данные, которые необходимы
  • Вы можете использовать JPA для оптимальной стратегии выгрузки (батчи, джойны, селекты)
  • Вы не имеете дело с закрытыми сессиями

Недостатки:

  • Вы должны думать, какие части должны быть заранее выгружены и какие нет
  • Вы будете иметь высокую задержку для загрузки корневого объекта из графа, потому что также другие части графа должны быть загружены
  • Вероятно Вы выгрузите ненужные части графа
  • Вы не гибки из-за того что стратегия глобальна и задается на уровне маппинга

Нужно использовать если:

  • ясно какие объекты из графа всегда выгружаются рядом
  • Вам нужно сделать предзагрузку для быстрого доступа позже

Вы не должны использовать если:

  • не ясно какие части графа требуются выгружать
  • граф выгрузки очень большой

Explicit join

Преимущества:

  • Вы решаете на уровне операций что нужно выгружать, без выгрузки ненужных объектов
  • Вы можете предложить разные варианты Вашей операции (например, и lazy и eager одновременно) используя разные стратегии join для одной и той же операции

Недостатки:

  • Более сложно определить что выгружать на уровне запроса
  • Опреление что нужно загружать на уровне запроса делает запрос менее переиспользуемым

Нужно использовать если:

  • для определенной операции, всегда одни и теже данные должны быть выгружены

Вы не должны использовать если:

  • Методы с джойном немного менее эффективны, потому что Вы получаете декартово произведение

Именованные графы

Преимущества:

  • Вы решаете на уровне операции что нужно выгрузить
  • Вы можете предложить разные варианты операции, позволяя использовать разные графы загрузки
  • Увеличивается переиспользование, потому что Вы используете тот же самый граф для разных операций

Недостатки:

  • Синтаксис многословный, если граф становится сложней
  • Именованные графы не могут быть совмещены

Нужно использовать если:

  • у Вас есть несколько стратегий загрузки, которые должны поддерживаться и могут переиспользованы для разных операций в репозитории/DAO

Вы не должны использовать если:

  • Есть очень много стратегий или комбинаций стратегий

Динамические графы

Преимущества:

  • Вы решаете на уровне операции что нужно выгрузить
  • Потребитель операции репозитория/DAO может сам решить что выгружать, это может уменьшить количество операций предлагаемых Вашими репозиториями/DAO

Недостатки:

  • Сложно задавать
  • Сложно переиспользовать, потому что они действительны только для конкретного случая

Нужно использовать если:

  • есть множество разных стратегий загрузки или комбинаций

Вы не должны использовать если:

  • Вам нужно малое количество стратегий в Вашем DAO

Criteria API

Преимущества:

  • Позволяет писать полностью динамические запросы, включая динамические стратегии
  • Может быть комбинирован с графами

Недостатки:

  • Более сложен, труден для чтения и сложно задавать

Нужно использовать если:

  • у Вас есть множество вариантов одного и того же за