Как загрузить @Cache при запуске весной?

Я использую spring-cache для улучшения запросов к базе данных, который отлично работает следующим образом:

@Bean
public CacheManager cacheManager() {
    return new ConcurrentMapCacheManager("books");
}

@Cacheable("books")
public Book getByIsbn(String isbn) {
    return dao.findByIsbn(isbn);
}

Но теперь я хочу предварительно заполнить полный книжный кеш при запуске. Это означает, что я хочу вызвать dao.findAll() и поместить все значения в кеш. Эта процедура должна быть запланирована только периодически.

Но как я могу явно заполнить кеш при использовании @Cacheable?


person membersound    schedule 14.01.2015    source источник
comment
stackoverflow.com/questions/53030289/   -  person Mr Nobody    schedule 29.10.2018


Ответы (7)


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

@Service
public class CacheScheduler {
    @Autowired
    BookDao bookDao;
    @Autowired
    CacheManager cacheManager;

    @PostConstruct
    public void init() {
        update();
        scheduleUpdateAsync();
    }

    public void update() {
        for (Book book : bookDao.findAll()) {
            cacheManager.getCache("books").put(book.getIsbn(), book);
        }
    }
}

Убедитесь, что ваш KeyGenerator вернет объект для одного параметра (по умолчанию). Или же выставьте метод putToCache в BookService, чтобы избежать прямого использования cacheManager.

@CachePut(value = "books", key = "#book.isbn")
public Book putToCache(Book book) {
    return book;
}
person Loki    schedule 04.02.2015
comment
Что такое BookService, я не вижу упоминания об этом в вопросе ОП. Кто звонит putToCache? - person Abhijit Sarkar; 06.08.2018

Я столкнулся со следующей проблемой при использовании @PostConstruct: - хотя метод, который я хотел кэшировать, был вызван, после его вызова из swagger он все еще не использовал кэшированное значение. Только после того, как позвонил еще раз.

Это было потому, что @PostConstruct слишком рано для кэширования чего-либо. (По крайней мере, я думаю, что это было проблемой)

Теперь я использую его более поздно в процессе запуска, и он работает без проблем:

@Component
public class CacheInit implements ApplicationListener<ApplicationReadyEvent> {

    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
       //call service method
    }

}
person Andrea Calin    schedule 12.11.2018
comment
Использовал аналогичный подход. @EventListener с ContextRefreshedEvent помог нам. - person Erikson; 28.01.2019

Если вам требуется иметь все экземпляры Book в памяти при запуске, вам следует хранить их в каком-то буфере самостоятельно. Помещение их в кэш с помощью метода findAll() означает, что вы должны аннотировать findAll() с помощью @Cacheable. Тогда вам придется вызывать findAll() при запуске. Но это не означает, что вызов getByIsbn(String isbn) будет обращаться к кешу, даже если соответствующий экземпляр был помещен в кеш при вызове findAll(). На самом деле этого не произойдет, потому что ehcache будет кэшировать возвращаемое значение метода как пару ключ/значение, где ключ вычисляется при вызове метода. Поэтому я не понимаю, как вы могли бы сопоставить возвращаемое значение findAll() и возвращаемое значение getByIsbn(String), потому что возвращаемые типы не совпадают, и, кроме того, ключ никогда не будет совпадать для всех ваших экземпляров.

person Olivier Meurice    schedule 14.01.2015

Можно было бы использовать CommandLineRunner для заполнения кеша при запуске.

Из официальной документации CommandLineRunner это:

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

Следовательно, нам просто нужно получить список всех доступных книг, а затем, используя CacheManager, заполнить кэш книг.

@Component
public class ApplicationRunner implements CommandLineRunner {
    @Autowired
    private BookDao dao;

    @Autowired
    private CacheManager cacheManager;

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("books");
    }

    @Override
    public void run(String... args) throws Exception {

        List<Book> results = dao.findAll();

        results.forEach(book -> 
            cacheManager.getCache("books").put(book.getId(), book));
    }
}
person mladzo    schedule 30.11.2017

Как указал Оливье, поскольку Spring кэширует вывод функции как один объект, использование нотации @cacheable с findAll не позволит вам загружать все объекты в кэш, чтобы впоследствии к ним можно было получить доступ по отдельности.

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

person Sameer Shah    schedule 25.01.2015

Одним из способов обойти @PostConstruct отсутствие привязки параметров является следующий код с тем преимуществом, что он будет выполняться после инициализации параметров:

@Bean
public Void preload(MyDAO dao) {
    dao.findAll();

    return null;
}
person ATorras    schedule 25.06.2020
comment
не могли бы вы улучшить свой пример, потому что, как таковой, я не понимаю смысла вызова sched.list() (что делает что...?), а также вызова dao.findAll() без фактического использования его результата...?! - person maxxyme; 16.09.2020
comment
Конечно! Этот ответ является незначительным улучшением по сравнению с принятым ответом Loki, поскольку вам не потребуется служебный класс класса (CacheScheduler) для выполнения задачи заполнения кэша. - person ATorras; 17.09.2020

Добавить еще один компонент BookCacheInitialzer

Автоподключение текущего bean-компонента BookService в BookCacheInitialzer

в методе PostConstruct псевдокода BookCacheInitialzer

Затем можно сделать что-то вроде

class BookService {
    @Cacheable("books")
    public Book getByIsbn(String isbn) {
        return dao.findByIsbn(isbn);
    }
    
    public List<Book> books;

    @Cacheable("books")
    public Book getByIsbnFromExistngBooks(String isbn) {
        return searchBook(isbn, books);
    }
}

class BookCacheInitialzer {

    @Autowired
    BookService  service;

    @PostConstruct
    public void initialize() {
        books = dao.findAll();
        service.books = books;
        for(Book book:books) {
            service.getByIsbnFromExistngBooks(book.getIsbn());
        }
    }
}
person Dheerendra Kulkarni    schedule 14.01.2015
comment
Да, это был бы вариант, НО очень плохой для производительности, так как я попадаю в БД n раз во время запуска для каждой записи. И более того, это как-то лишнее, так как у меня уже есть все мои книги findAll(). Поэтому я ищу способ поместить эти книги в кеш без повторного обращения к БД. - person membersound; 14.01.2015
comment
Затем можно сделать что-то вроде - person Dheerendra Kulkarni; 14.01.2015
comment
Соответственно отредактировал soln. - person Dheerendra Kulkarni; 14.01.2015
comment
на самом деле, я действительно не понимаю цели сохранения book в service.books , кроме непонимания полезности метода searchBook... пожалуйста, будьте более точны в своем примере. - person maxxyme; 16.09.2020