Кешування даних — Java Spring

Багаторазово вичитуючи одні і ті ж дані, постає питання оптимізації, дані не змінюються або рідко змінюються, це різні довідники і ін. інформація, тобто функція отримання даних по ключу — детермінована. Тут напевно всі розуміють — потрібен Кеш! Навіщо всякий раз повторно виконувати пошук даних або обчислення?
Так ось тут я покажу як робити кеш в Java Spring і оскільки це тісно пов’язано швидше за все з Базою даних, і як зробити це в СУБД на прикладі однієї конкретної.

Зміст

  • Кеш в Spring
  • Кеш в Oracle PL-SQL функції

 

Кеш в Spring

Далі все роблять приблизно однаково, в Java використовують різні HasMap, ConcurrentMap та ін. В Spring теж це є рішення, просте, зручне, ефективне. Я думаю що в більшості випадків це допоможе у вирішенні завдання. І так, все що потрібно, це включити кеш і анотувати функцію.
Робимо кеш доступним

@SpringBootApplication
@EnableCaching
public class DemoCacheAbleApplication {

 public static void main(String[] args) {
 SpringApplication.run(DemoCacheAbleApplication.class, args);
}
}

Кешуємо дані функції пошуку

@Cacheable(cacheNames="person")
 public Person findCacheByName(String name) {
//...
}

В анотації вказується назва кеша і є ще інші параметри. Працює як і очікується так, перший раз код виконується, результат пошуку поміщається в кеш по ключу (в даному випадку name) і наступні виклики код не виконується, а дані беруться з кеша.
Приклад реалізації репозиторію «Person» з використанням кеша

@Component
public class PersonRepository {

 private static final Logger logger = LoggerFactory.getLogger(PersonRepository.class);
 private List<Person> persons = new ArrayList<>();

 public void initPersons(List<Person> persons) {
this.persons.addAll(persons);
}

 private Person findByName(String name) {
 Person person = persons.stream()
 .filter(p -> p.getName().equals(name))
.findFirst()
.orElse(null);
 return person;
}

@Cacheable(cacheNames="person")
 public Person findCacheByName(String name) {
 logger.info("find person ..." + name);
 final Person person = findByName(name);
 return person;
}
}

Перевіряю що вийшло

@RunWith(SpringRunner.class)
@SpringBootTest
public class DemoCacheAbleApplicationTests {


 private static final Logger logger = LoggerFactory.getLogger(DemoCacheAbleApplicationTests.class);

@Autowired
 private PersonRepository personRepository;

@Before
 public void before() {
 personRepository.initPersons(Arrays.asList(new Person("Іван", 22),
 new Person("Сергій", 34),
 new Person("Ігор", 41)));
}


 private Person findCacheByName(String name) {
 logger.info("begin find" + name);
 final Person person = personRepository.findCacheByName(name);
 logger.info("find result =" + person.toString());
 return person;
}

@Test
 public void findByName() {
findCacheByName("Іван");
findCacheByName("Іван");
}
}

У тесті викликаю два рази

@Test
public void findByName() {
findCacheByName("Іван");
findCacheByName("Іван");
}

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

Зручно, можна точково оптимізувати існуючий функціонал. Якщо у функції більше одного аргументу, то можна вказати ім’я параметра, який використовувати в якості ключа.

 @Cacheable(cacheNames="person", key="#name")
 public Person findByKeyField(String name, Integer age) {

Є й більш складні схеми отримання ключа, в документації.
Але звичайно постане питання, як оновити дані в кеші? Для цієї мети є дві анотації.
Це перша @CachePut
Функція з цієї анотацією буде завжди викликати код, а результат поміщати в кеш, таким чином вона зможе оновити кеш.
Додам в репозиторій два методу: видалення і додавання Person

 public boolean delete(String name) {
 final Person person = findByName(name);
 return persons.remove(person);
}

 public boolean add(Person person) {
 return persons.add(person);
}

Виконаю пошук Person, видалю, додам, знову пошук, але як і раніше буду отримувати одне і теж особа з кеша, поки не викликом «findByNameAndPut»

@CachePut(cacheNames="person")
 public Person findByNameAndPut(String name) {
 logger.info("findByName and put person ..." + name);
 final Person person = findByName(name);
 logger.info("put in cache person" + person);
 return person;
}

Тест

@Test
 public void findCacheByNameAndPut() {
 Person person = findCacheByName("Іван");

 logger.info("delete" + person);
personRepository.delete("Іван");

findCacheByName("Іван");

 logger.info("add new person");
 person = new Person("Іван", 35);
personRepository.add(person);

findCacheByName("Іван");

 logger.info("put new");
personRepository.findByNameAndPut("Іван");

findCacheByName("Іван");
}

Інша анотація це @CacheEvict
Дозволяє не просто відвідувати сховище кеша, але і виселяти. Цей процес корисний для видалення застарілих або невикористовуваних даних з кеша.

Читайте також  Схожі на ос дрони піднімають тяжкості, допомагаючи собі черевцем

За замовчуванням Spring для кеша використовує — ConcurrentMapCache, якщо є свій відмінний клас для організації кеша, то це можливо вказати в CacheManager

@SpringBootApplication
@EnableCaching
public class DemoCacheAbleApplication {

 public static void main(String[] args) {
 SpringApplication.run(DemoCacheAbleApplication.class, args);
}

@Bean
 public CacheManager cacheManager() {
 SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(
 new ConcurrentMapCache("person"),
 new ConcurrentMapCache("addresses")));
 return cacheManager;
}
}

Там же вказуються імена кешей, їх може бути кілька. У конфігурації xml це зазначається так:
Spring configuration.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
 http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

<cache:annotation-driven/>

 <bean id="cacheManager"
class="org.springframework.cache.support.SimpleCacheManager">
 <property name="caches">
<set>
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
name="person"/>
<bean
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
name="addresses"/>
</set>
</property>
</bean>

</beans>

Person клас

public class Person {

 private String name;
 private Integer age;

 public Person(String name, Integer age) {
 this.name = name;
 this.age = age;
}

 public String getName() {
 return name;
}

 public Integer getAge() {
 return age;
}

@Override
 public String toString() {
 return name + ":" + age;
}

Структура проекту

Тут pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>demoCacheAble</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>DemoCacheAble</name>
 <description>Demo project for Spring Boot</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
 <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.звітування.outputEncoding>UTF-8</project.звітування.outputEncoding>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

Кеш в Oracle PL-SQL функції

Ну і в кінці, тим хто не нехтує потужністю СУБД, а використовує її, можуть використовувати кешування на рівні БД, в доповнення або як альтернативу. Так, наприклад, у Oracle не менш елегантно можна перетворити звичайну функцію, функцію з кешування результату, додавши до неї RESULT_CACHE
Приклад:

CREATE OR REPLACE FUNCTION GET_COUNTRY_NAME(P_CODE IN VARCHAR2)
 RETURN VARCHAR2 RESULT_CACHE IS
 CODE_RESULT VARCHAR2(50);
BEGIN
 SELECT COUNTRY_NAME INTO CODE_RESULT COUNTRIES FROM
 WHERE COUNTRY_ID = P_CODE;
 -- імітація довгої роботи
 dbms_lock.sleep (1);

RETURN(CODE_RESULT);
END;

Після зміни даних в таблиці, кеш буде перебудований, можна тонко налаштувати правило кеша з допомогою RELIES_ON(…)

Матеріали
Cache Abstraction

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *