Кеширование данных — 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 persons  = new ArrayList<>();

    public void initPersons(List 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



    

    
        
            
                
                
            
        
    




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


        4.0.0

        com.example
        demoCacheAble
        0.0.1-SNAPSHOT
        jar

        DemoCacheAble
        Demo project for Spring Boot

        
                org.springframework.boot
                spring-boot-starter-parent
                2.0.6.RELEASE
                 
        

        
                UTF-8
                UTF-8
                1.8
        

        
                
                        org.springframework.boot
                        spring-boot-starter-cache
                

                
                        org.springframework.boot
                        spring-boot-starter-test
                        test
                
        

        
                
                        
                                org.springframework.boot
                                spring-boot-maven-plugin
                        
                
        




Кеш в 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

© Habrahabr.ru