Кеширование данных — Java Spring +9


Многократно вычитывая одни и те же данные, встает вопрос оптимизации, данные не меняются или редко меняются, это различные справочники и др. информация, т.е. функция получения данных по ключу — детерминирована. Тут наверно все понимают — нужен Кеш! Зачем всякий раз повторно выполнять поиск данных или вычисление?

Так вот здесь я покажу как делать кеш в 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. OlehR
    /#19261309

    RELIES_ON нужен только в 11R1. В следующих версиях он уже считается устаревшим.
    И согласитесь решение на PL/SQL Простое и елегантное. Практически не требует дополнительного кода.

  2. gibsonen
    /#19262801

    Спасибо за отличную статью. Все доступно и понятно:)
    Пишите еще;)

  3. AntonioXXX
    /#19263655

    Про оракловый кэш еще можно отметить явно его одно преимущество — то, что результат функций всегда актуален, если данные поменялись и закоммичены, то эти данные и вернутся. Не нужно закладываться на то, что могут быть в некоторых случаях устаревшие данные.

  4. Berkof
    /#19266143

    Кэшировать можно дофига на чём, Java реализация конечно очень удобная, но жрёт дико много памяти (сами эти mapEntry, объекты кучами), можно юзать memcache, но вопрос — как делать кэш распределённым на несколько машин? Там тоже свои решения, тот же Apache Ignite умеет работать сквозным кэшем с SQL базе (или можно отдельно в нём хранить строчки), плюс такого решения — его прогревать не нужно, он сам персиститься умеет (по желанию, опять же). Минут — нужно тащить в свой проект ещё одного крокодила.
    А ещё — ничего не сказано про мониторинг использования кэшей… спринговые вроде в JMX светятся и можно посмотреть hitRation с числом запросов, а как эффективность ораклового кэша узнать? как узнать, сколько ему памяти выделено (сколько значений кэширует БД? это вообще как-то конфигурируется?)

  5. OlehR
    /#19266177 / +1

    как узнать, сколько ему памяти выделено (сколько значений кэширует БД? это вообще как-то конфигурируется?)

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