Кеширование данных — 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("Иван"); } 

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

    @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("Иван"); 	} 

image

Другая аннотация это @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;     }  

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

Здесь 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.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


ссылка на оригинал статьи https://habr.com/post/427187/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *