1. Ніколи не пишіть своє рішення щодо кешування

Ще один спосіб прискорити роботу з базою даних — кешувати об'єкти, на які ми вже робили запит раніше.

Важливо! Ніколи не пиши своє рішення щодо кешування. Це завдання має стільки підводного каміння, що тобі й не снилося.

Проблема 1 — скидання кеша. Іноді відбуваються події, коли потрібно видалити об'єкт із кеша або оновити його в ньому. Єдиний спосіб зробити це грамотно — пропускати всі запити до бази через двигун кеша. В іншому разі тобі щоразу доведеться явно вказувати кешу, які об'єкти варто видалити або оновити.

Проблема 2 — нестача пам'яті. Кешування здається чудовою ідеєю, поки ти не зіткнешся з тим, що об'єкти в пам'яті займають багато місця. Тобі потрібні додатково десятки гігабайт пам'яті для ефективної роботи кеша серверної програми.

А оскільки пам'яті завжди не вистачає, потрібна ефективна стратегія видалення об'єктів із кеша. Це чимось нагадує збирач сміття у Java. І як ти пам'ятаєш, вже десятки років найкращі розуми винаходять різні способи маркування об'єктів за поколіннями тощо.

Проблема 3 — різні стратегії. Як показує практика, для різних об'єктів ефективні різні стратегії зберігання та оновлення у кеші. Ефективна система кешування не може обійтися стратегією для всіх об'єктів.

Проблема 4 — ефективне зберігання об'єктів. Не можна просто зберігати об'єкти в кеші. Об'єкти занадто часто містять посилання на інші об'єкти тощо. Такими темпами тобі не знадобиться збирач сміття: йому просто нема що видаляти.

Тому замість того, щоб зберігати самі об'єкти, іноді набагато ефективніше зберігати значення їхніх полів-примітивів. І системи швидкого конструювання об'єктів за ними.

На виході ти отримаєш цілу віртуальну СУБД у пам'яті, яка має швидко працювати та споживати мало пам'яті.

2. Кешування в базі даних

Крім кешування прямо в Java-програмі ще часто організовують кешування прямо в базі даних.

Там є чотири великі підходи:

Підхід перший — денормалізація бази даних. SQL-сервер у пам'яті зберігає дані негаразд, як вони зберігатися у таблицях.

Коли дані зберігаються на диску в таблицях, то дуже часто розробники намагаються максимально уникнути дублювання даних — такий процес називається нормалізацією бази даних. Для прискорення роботи з даними в пам'яті виконується зворотний процес — денормалізація бази даних. Купа зв'язаних таблиць може зберігатися вже в об'єднаному вигляді — у вигляді великих таблиць тощо.

Другий підхід — кешування запитів. І результатів запитів.

СУБД бачить, що часто до неї приходять однакові або схожі запити. Тоді вона починає просто кешувати ці запити та відповіді на них. Але при цьому потрібно чітко стежити за тим, щоб з кешу вчасно видалялися рядки, що змінилися у базі.

Цей підхід може бути дуже ефективним за участю людини, яка може проаналізувати запити та допомогти СУБД зрозуміти, як їх краще кешувати.

Третій підхід — база даних у пам'яті.

Ще один підхід, що часто використовується. Між сервером та СУБД ставиться ще одна база, яка зберігає всі свої дані лише у пам'яті. Її ще називають In-Memory-DB.

Якщо у тебе багато різних серверів звертаються до однієї бази даних, то за допомогою In-Memory-DB можна організувати кешування, орієнтоване на тип конкретного сервера.

Приклад:

Підхід 4 — кластер баз даних. Декілька read-only баз.

Ще одне рішення — використання кластера: кілька СУБД одного типу містять ідентичні дані. Водночас читати дані можна з усіх баз, а писати лише в одну, яка потім синхронізується з рештою баз.

Це дуже добре рішення, тому що його легко конфігурувати, і воно працює на практиці. Зазвичай на один запит до бази на зміну даних до неї надходить 10-100 запитів на читання даних.

3. Види кешування в Hibernate

Hibernate підтримує три рівні кешування:

  • Кешування на рівні сесії (Session)
  • Кешування на рівні SessionFactory
  • Кешування запитів (та їх результатів)

Цю систему можна уявити у вигляді такого малюнка:

Найпростіший вид кешування (його ще називають кешем першого рівня) реалізований на рівні Hibernate-сесії. Hibernate завжди за замовчуванням використовує цей кеш і його не можна вимкнути.

Давай відразу розглянемо наступний приклад:


Employee director1 = session.get(Employee.class, 4);
Employee director2 = session.get(Employee.class, 4);
 
assertTrue(director1 == director2);

Може здатися, що тут виконається два запити до бази, проте це не так. Після першого запиту до бази об'єкт Employee буде закешований. І якщо ти знову виконаєш запит об'єкта в тій самій сесії, то Hibernate поверне той самий Java-об'єкт.

Той самий об'єкт — це означає, що навіть посилання на об'єкти будуть ідентичними. Це реально той самий об'єкт.

Під час використання методів save(), update(), saveOrUpdate(), load(), get() , list(), iterate() та scroll() завжди буде задіяний кеш першого рівня. Власне, тут нема чого більше додати.

4. Кешування другого рівня

Якщо кеш першого рівня прив'язаний до об'єкта сесії, кеш другого рівня прив'язаний до об'єкта SessionFactory. Що означає, що видимість об'єктів у цьому кеші набагато ширша, ніж у кеші першого рівня.

Приклад:


Session session = factory.openSession();
Employee director1 = session.get(Employee.class, 4);
session.close();
 
Session session = factory.openSession();
Employee director2 = session.get(Employee.class, 4);
session.close();
 
assertTrue(director1! = director2);
assertTrue(director1.equals(director2));

У цьому прикладі буде виконано два запити до бази. Hibernate поверне ідентичні об'єкти, але це буде не той самий об'єкт — вони матимуть різні посилання.

Кешування другого рівня за промовчанням вимкнено. Тому ми маємо два запити до бази замість одного.

Щоб його увімкнути, потрібно у файлі hibernate.cfg.xml написати такі рядки:


<property name="hibernate.cache.provider_class" value="net.sf.ehcache.hibernate.SingletEhCacheProvider"/>
<property name="hibernate.cache.use_second_level_cache" value="true"/>

Після включення кешування другого рівня поведінка Hibernate трохи зміниться:


Session session = factory.openSession();
Employee director1 = session.get(Employee.class, 4);
session.close();
 
Session session = factory.openSession();
Employee director2 = session.get(Employee.class, 4);
session.close();
 
assertTrue(director1 == director2);

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