Кеширование данных — 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("Иван");
}
, первый раз происходит вызов, поиск, в второй раз результат берется уже из кеша. Это видно в консоли
Удобно, можно точечно оптимизировать существующий функционал. Если в функции более одного аргумента, то можно указать имя параметра, какой использовать в качестве ключа.
@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 конфигурации это указывается так:
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;
}
Структура проекта
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