Кешування даних — 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