Кеширование данных — 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
    Поделиться публикацией

    Комментарии 5

      +1
      RELIES_ON нужен только в 11R1. В следующих версиях он уже считается устаревшим.
      И согласитесь решение на PL/SQL Простое и елегантное. Практически не требует дополнительного кода.
        0
        Спасибо за отличную статью. Все доступно и понятно:)
        Пишите еще;)
          0
          Про оракловый кэш еще можно отметить явно его одно преимущество — то, что результат функций всегда актуален, если данные поменялись и закоммичены, то эти данные и вернутся. Не нужно закладываться на то, что могут быть в некоторых случаях устаревшие данные.
            –1
            Кэшировать можно дофига на чём, Java реализация конечно очень удобная, но жрёт дико много памяти (сами эти mapEntry, объекты кучами), можно юзать memcache, но вопрос — как делать кэш распределённым на несколько машин? Там тоже свои решения, тот же Apache Ignite умеет работать сквозным кэшем с SQL базе (или можно отдельно в нём хранить строчки), плюс такого решения — его прогревать не нужно, он сам персиститься умеет (по желанию, опять же). Минут — нужно тащить в свой проект ещё одного крокодила.
            А ещё — ничего не сказано про мониторинг использования кэшей… спринговые вроде в JMX светятся и можно посмотреть hitRation с числом запросов, а как эффективность ораклового кэша узнать? как узнать, сколько ему памяти выделено (сколько значений кэширует БД? это вообще как-то конфигурируется?)
              +1
              как узнать, сколько ему памяти выделено (сколько значений кэширует БД? это вообще как-то конфигурируется?)

              Конфигурируется на уровне БД (Задается сколько памяти виделить на кеш.). Там же есть статистика сколько попаданий, сколько раз пересчитало, и сколько витеснено с кеша хороших значений. При большом количестве последнего желательно увеличивать кеш(или уменшать количество кешируемих результатов)
              Можно смотреть сдесь
              docs.oracle.com/database/121/TGDBA/tune_result_cache.htm#TGDBA616

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое