Многократно вычитывая одни и те же данные, встает вопрос оптимизации, данные не меняются или редко меняются, это различные справочники и др. информация, т.е. функция получения данных по ключу — детерминирована. Тут наверно все понимают — нужен Кеш! Зачем всякий раз повторно выполнять поиск данных или вычисление?
Так вот здесь я покажу как делать кеш в Java Spring и поскольку это тесно связанно скорее всего с Базой данных, то и как сделать это в СУБД на примере одной конкретной.
Содержание
Кеш в 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 конфигурации это указывается так:
<?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>
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;
}
Структура проекта
<?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.reporting.outputEncoding>UTF-8</project.reporting.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 FROM COUNTRIES
WHERE COUNTRY_ID = P_CODE;
-- имитация долгой работы
dbms_lock.sleep (1);
RETURN(CODE_RESULT);
END;
После изменения данных в таблице, кеш будет перестроен, можно тонко настроить правило кеша с помощью
RELIES_ON(...)
Материалы
Cache Abstraction
Автор: Александр Рыльков