Spring - EhCache

Overview

Spring Boot integration with EhCache 3

Github: https://github.com/gitorko/project98

EhCache

EhCache is an open-source cache library. It supports cache in memory and disk, It supports eviction policies such as LRU, LFU, FIFO. Ehcache uses Last Recently Used (LRU) eviction strategy for memory & Last Frequently Used (LFU) as the eviction strategy for disk store.

Ehcache Caching Tiers - Caching layer can consist of more than one memory area. When using more than one memory area, the areas are arranged as hierarchical tiers. The lowest tier is called the Authority Tier and the other tiers are called the Near Cache. Most frequently used data is stored in the fastest caching tier (top layer)

Types of store

  1. On-Heap Store - stores cache entries in Java heap memory
  2. Off-Heap Store - primary memory (RAM) to store cache entries, cache entries will be moved to the on-heap memory automatically before they can be used.
  3. Disk Store - uses a hard disk to store cache entries. SSD type disk would perform better.
  4. Clustered Store - stores cache entries on the remote server

Types of caching

  1. Read-Cache-aside - Application queries the cache. If the data is found, it returns the data directly. If not it fetches the data from the SoR, stores it into the cache, and then returns.
  2. Read-Through - Application queries the cache, cache service queries the SoR if not present and updates the cache and returns.
  3. Write-Around - Application writes to db and to the cache.
  4. Write-Behind / Write-Back - Application writes to cache. Cache is pushed to SoR after some delay periodically.
  5. Write-through - Application writes to cache, cache service immediately writes to SoR.

Code

 1package com.demo.project98.service;
 2
 3import com.demo.project98.domain.Country;
 4import lombok.extern.slf4j.Slf4j;
 5import org.springframework.cache.annotation.CacheEvict;
 6import org.springframework.cache.annotation.CachePut;
 7import org.springframework.cache.annotation.Cacheable;
 8import org.springframework.stereotype.Service;
 9
10@Service
11@Slf4j
12public class CountryCache {
13
14    /**
15     * NOTE: Never create a local HashMap to key those values, as it can cause memory overflow without eviction.
16     *
17     * @Cacheable - If key is present returns the object, no update happens.
18     * @CachePut - If key is present it will update and then return object.
19     */
20    @Cacheable(cacheNames = "countryCache", key = "#country.code")
21    public Country get(Country country) {
22        log.info("Getting country name: {}", country.getCode());
23        return country;
24    }
25
26    @CachePut(cacheNames = "countryCache", key = "#country.code")
27    public Country put(Country country) {
28        log.info("Adding country: {}", country);
29        return country;
30    }
31
32    @CacheEvict(cacheNames = "countryCache", key = "#country.code")
33    public void evictSingleCacheValue(Country country) {
34        log.info("Evicting from cache: {}", country);
35    }
36
37}
 1package com.demo.project98.service;
 2
 3import com.demo.project98.domain.Country;
 4import lombok.extern.slf4j.Slf4j;
 5import org.ehcache.event.CacheEvent;
 6import org.ehcache.event.CacheEventListener;
 7
 8@Slf4j
 9public class CountryCacheListener implements CacheEventListener<String, Country> {
10    @Override
11    public void onEvent(CacheEvent<? extends String, ? extends Country> event) {
12        log.info("Event '{}' fired for key '{}' with value {}", event.getType(), event.getKey(), event.getNewValue());
13    }
14}
 1package com.demo.project98;
 2
 3import com.demo.project98.domain.Country;
 4import com.demo.project98.domain.Customer;
 5import com.demo.project98.repo.CustomerRepository;
 6import org.springframework.beans.factory.annotation.Autowired;
 7import org.springframework.boot.CommandLineRunner;
 8import org.springframework.boot.SpringApplication;
 9import org.springframework.boot.autoconfigure.SpringBootApplication;
10import org.springframework.cache.Cache;
11import org.springframework.cache.CacheManager;
12import org.springframework.cache.annotation.EnableCaching;
13import org.springframework.context.annotation.Bean;
14
15@SpringBootApplication
16@EnableCaching
17public class Main {
18
19    @Autowired
20    CacheManager cacheManager;
21
22    @Autowired
23    CustomerRepository customerRepository;
24
25    public static void main(String[] args) {
26        SpringApplication.run(Main.class, args);
27    }
28
29    @Bean
30    public CommandLineRunner sendData() {
31        return args -> {
32            Cache cache = cacheManager.getCache("countryCache");
33            cache.put("FR", new Country("FR", "France", "Paris"));
34            cache.put("US", new Country("US", "United State Of America", "Washington DC"));
35            cache.put("IN", new Country("IN", "India", "Delhi"));
36            //Since cache is configure to hold only 2 values, one element is auto evicted!
37
38            for (int i = 0; i < 200; i++) {
39                customerRepository.save(Customer.builder().name("customer_" + i).phone("phone_" + i).build());
40            }
41            System.out.println("Seeded!");
42        };
43    }
44
45}
 1<?xml version="1.0" encoding="UTF-8"?>
 2<config xmlns='http://www.ehcache.org/v3'>
 3    <cache alias="countryCache">
 4        <key-type>java.lang.String</key-type>
 5        <value-type>com.demo.project98.domain.Country</value-type>
 6        <expiry>
 7            <ttl unit="minutes">1</ttl>
 8        </expiry>
 9        <listeners>
10            <listener>
11                <class>com.demo.project98.service.CountryCacheListener</class>
12                <event-firing-mode>ASYNCHRONOUS</event-firing-mode>
13                <event-ordering-mode>ORDERED</event-ordering-mode>
14                <events-to-fire-on>CREATED</events-to-fire-on>
15                <events-to-fire-on>REMOVED</events-to-fire-on>
16                <events-to-fire-on>EXPIRED</events-to-fire-on>
17            </listener>
18        </listeners>
19        <resources>
20            <heap unit="entries">2</heap>
21        </resources>
22        <heap-store-settings>
23            <max-object-graph-size>2</max-object-graph-size>
24            <max-object-size unit="kB">5</max-object-size>
25        </heap-store-settings>
26    </cache>
27</config>

Notice the SQL is printed each time a db call happens, if the data is cached no DB call is made.

Setup

Project 98

Spring Boot & Ehcache

https://gitorko.github.io/spring-ehcache/

Version

Check version

1$java --version
2openjdk 17.0.3 2022-04-19 LTS

Postgres DB

1docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
2docker ps
3docker exec -it pg-container psql -U postgres -W postgres
4CREATE USER test WITH PASSWORD 'test@123';
5CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
6grant all PRIVILEGES ON DATABASE "test-db" to test;
7
8docker stop pg-container
9docker start pg-container

Dev

To run the backend in dev mode.

1./gradlew clean build
2./gradlew bootRun

Postman

Import the postman collection to postman

Postman Collection

References

https://www.ehcache.org/documentation/3.0

https://docs.spring.io/spring-boot/docs/2.7.2/reference/htmlsingle/#io.caching

comments powered by Disqus