Spring - EhCache

Overview

Spring Boot 3 with EhCache 3

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

EhCache

EhCache is an open-source cache library. Ehcache version 3 provides an implementation of a JSR-107 cache manager. 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.

Caching

HashMap vs Cache

Disadvantage of using hashmap over cache is that hashmap can cause memory overflow without eviction & doesn't support write to disk.

Ehcache will only evict elements when putting elements and your cache is above threshold. Otherwise, accessing those expired elements will result in them being expired (and removed from the Cache). There is no thread that collects and removes expired elements from the Cache in the background.

Types of store

Cache 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

Memory areas supported by Ehcache

  1. On-Heap Store: Uses the Java heap memory to store cache entries and shares the memory with the application. The cache is also scanned by the garbage collection. This memory is very fast, but also very limited.
  2. Off-Heap Store: Uses the RAM to store cache entries. This memory is not subject to garbage collection. Still quite fast memory, but slower than the on-heap memory, because the cache entries have to be moved to the on-heap memory before they can be used.
  3. Disk Store: Uses the hard disk to store cache entries. Much slower than RAM. It is recommended to use a dedicated SSD that is only used for caching.

Caching Strategies

Read heavy caching strategies

  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.

Write heavy caching strategies

  1. Write-Around - Application writes to db and to the cache.
  2. Write-Behind / Write-Back - Application writes to cache. Cache is pushed to SoR after some delay periodically.
  3. Write-through - Application writes to cache, cache service immediately writes to SoR.

Caching Strategy

Spring Caching

@Cacheable vs @CachePut

@Cacheable will skip running the method, whereas @CachePut will actually run the method and then put its results in the cache.

You can also use CacheEventListener to track events like CREATED, UPDATED, EXPIRED, REMOVED.

Ehcache uses Last Recently Used (LRU) as the default eviction strategy for the memory stores when the cache is full. If a disk store is used and this is full it uses Last Frequently Used (LFU) as the eviction strategy.

You can enable spring actuator and look at the cache metrics

The @CacheConfig annotation allows us to define certain cache configurations at the class level. This is useful if certain cache settings are common for all methods.

1@CacheConfig(cacheNames = "customerCache")

Code

  1package com.demo.project98.config;
  2
  3import static org.ehcache.config.builders.CacheEventListenerConfigurationBuilder.newEventListenerConfiguration;
  4import static org.ehcache.event.EventType.CREATED;
  5import static org.ehcache.event.EventType.EXPIRED;
  6import static org.ehcache.event.EventType.REMOVED;
  7import static org.ehcache.event.EventType.UPDATED;
  8
  9import java.math.BigDecimal;
 10import java.time.Duration;
 11import java.util.Arrays;
 12import javax.cache.CacheManager;
 13import javax.cache.Caching;
 14import javax.cache.spi.CachingProvider;
 15
 16import com.demo.project98.domain.Country;
 17import com.demo.project98.domain.Customer;
 18import com.demo.project98.listener.CountryCacheListener;
 19import lombok.RequiredArgsConstructor;
 20import org.ehcache.config.CacheConfiguration;
 21import org.ehcache.config.builders.CacheConfigurationBuilder;
 22import org.ehcache.config.builders.ExpiryPolicyBuilder;
 23import org.ehcache.config.builders.ResourcePoolsBuilder;
 24import org.ehcache.config.units.MemoryUnit;
 25import org.ehcache.impl.config.event.DefaultCacheEventListenerConfiguration;
 26import org.ehcache.jsr107.Eh107Configuration;
 27import org.springframework.cache.annotation.EnableCaching;
 28import org.springframework.cache.concurrent.ConcurrentMapCache;
 29import org.springframework.cache.support.SimpleCacheManager;
 30import org.springframework.context.annotation.Bean;
 31import org.springframework.context.annotation.Configuration;
 32
 33@Configuration
 34@EnableCaching
 35@RequiredArgsConstructor
 36public class CacheConfig {
 37
 38    private final CountryCacheListener listener;
 39
 40    @Bean
 41    public CacheManager echCacheManager() {
 42        CachingProvider cachingProvider = Caching.getCachingProvider();
 43        CacheManager cacheManager = cachingProvider.getCacheManager();
 44        cacheManager.createCache("customerCache", customerCacheConfig());
 45        cacheManager.createCache("countryCache", countryCacheConfig());
 46        cacheManager.createCache("squareCache", squareCacheConfig());
 47        return cacheManager;
 48    }
 49
 50    private javax.cache.configuration.Configuration<Long, Customer> customerCacheConfig() {
 51        CacheConfiguration<Long, Customer> cacheConfig = CacheConfigurationBuilder
 52                .newCacheConfigurationBuilder(Long.class, Customer.class,
 53                        ResourcePoolsBuilder.newResourcePoolsBuilder()
 54                                .heap(10)
 55                                .offheap(10, MemoryUnit.MB)
 56                                .build())
 57                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(10)))
 58                .build();
 59        javax.cache.configuration.Configuration<Long, Customer> configuration = Eh107Configuration.fromEhcacheCacheConfiguration(cacheConfig);
 60        return configuration;
 61    }
 62
 63    private javax.cache.configuration.Configuration<String, Country> countryCacheConfig() {
 64        CacheConfiguration<String, Country> cacheConfig = CacheConfigurationBuilder
 65                .newCacheConfigurationBuilder(String.class, Country.class,
 66                        ResourcePoolsBuilder.newResourcePoolsBuilder()
 67                                .heap(10)
 68                                .offheap(10, MemoryUnit.MB)
 69                                .build())
 70                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(10)))
 71                .withService(getCacheEventListener())
 72                .build();
 73        javax.cache.configuration.Configuration<String, Country> configuration = Eh107Configuration.fromEhcacheCacheConfiguration(cacheConfig);
 74        return configuration;
 75    }
 76
 77    private javax.cache.configuration.Configuration<Long, BigDecimal> squareCacheConfig() {
 78        CacheConfiguration<Long, BigDecimal> cacheConfig = CacheConfigurationBuilder
 79                .newCacheConfigurationBuilder(Long.class, BigDecimal.class,
 80                        ResourcePoolsBuilder.newResourcePoolsBuilder()
 81                                .heap(10)
 82                                .offheap(10, MemoryUnit.MB)
 83                                .build())
 84                .build();
 85        javax.cache.configuration.Configuration<Long, BigDecimal> configuration = Eh107Configuration.fromEhcacheCacheConfiguration(cacheConfig);
 86        return configuration;
 87    }
 88
 89    private DefaultCacheEventListenerConfiguration getCacheEventListener() {
 90        return newEventListenerConfiguration(listener, CREATED, UPDATED, EXPIRED, REMOVED)
 91                .asynchronous()
 92                .unordered()
 93                .build();
 94    }
 95
 96    /**
 97     * Use when no configurations.
 98     */
 99    public SimpleCacheManager simpleEhCacheManager() {
100        SimpleCacheManager cacheManager = new SimpleCacheManager();
101        cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("customerCache"), new ConcurrentMapCache("countryCache")));
102        return cacheManager;
103    }
104
105}
 1package com.demo.project98.listener;
 2
 3import com.demo.project98.domain.Country;
 4import lombok.extern.slf4j.Slf4j;
 5import org.ehcache.event.CacheEvent;
 6import org.ehcache.event.CacheEventListener;
 7import org.springframework.stereotype.Component;
 8
 9@Slf4j
10@Component
11public class CountryCacheListener implements CacheEventListener<String, Country> {
12    @Override
13    public void onEvent(CacheEvent<? extends String, ? extends Country> cacheEvent) {
14        log.info("Cache event = {}, Key = {},  Old value = {}, New value = {}", cacheEvent.getType(), cacheEvent.getKey(), cacheEvent.getOldValue(), cacheEvent.getNewValue());
15    }
16}
 1package com.demo.project98.service;
 2
 3import javax.cache.Cache;
 4import javax.cache.CacheManager;
 5
 6import com.demo.project98.domain.Country;
 7import jakarta.annotation.PostConstruct;
 8import lombok.RequiredArgsConstructor;
 9import lombok.extern.slf4j.Slf4j;
10import org.springframework.stereotype.Service;
11
12@Service
13@Slf4j
14@RequiredArgsConstructor
15public class CountryService {
16
17    /**
18     * Interact directly with CacheManager
19     */
20    private final CacheManager cacheManager;
21    private Cache<String, Country> cache;
22
23    @PostConstruct
24    public void postInit() {
25        cache = cacheManager.getCache("countryCache");
26    }
27
28    public Country get(String code) {
29        log.info("Getting country code: {}", code);
30        return cache.get(code);
31    }
32
33    public void put(Country country) {
34        log.info("Adding country: {}", country);
35        cache.put(country.getCode(), country);
36    }
37
38    public void evict(String code) {
39        log.info("Evicting country code: {}", code);
40        cache.remove(code);
41    }
42
43}
 1package com.demo.project98.service;
 2
 3import java.util.Optional;
 4
 5import com.demo.project98.domain.Customer;
 6import com.demo.project98.repo.CustomerRepository;
 7import lombok.RequiredArgsConstructor;
 8import lombok.extern.slf4j.Slf4j;
 9import org.springframework.cache.annotation.CacheEvict;
10import org.springframework.cache.annotation.CachePut;
11import org.springframework.cache.annotation.Cacheable;
12import org.springframework.stereotype.Service;
13
14@Service
15@RequiredArgsConstructor
16@Slf4j
17public class CustomerService {
18
19    final CustomerRepository customerRepository;
20
21    /**
22     * If return type is Optional<Customer> it will hit the db each time. As the cache is not configured for Optional<Customer>
23     */
24    @Cacheable(cacheNames = "customerCache", key = "#id", unless = "#result == null")
25    public Customer getCustomerById(Long id) {
26        log.info("Getting customer {} from db!", id);
27        Optional<Customer> customer = customerRepository.findById(id);
28        if (customer.isPresent()) {
29            return customer.get();
30        } else {
31            return null;
32        }
33    }
34
35    /**
36     * Don't put @Cacheable here as it will load everything into cache
37     */
38    public Iterable<Customer> getCustomers() {
39        log.info("Getting all customers from db!");
40        return customerRepository.findAll();
41    }
42
43    @CachePut(cacheNames = "customerCache", key = "#result.id")
44    public Customer save(Customer customer) {
45        log.info("Saving customer {} to db!", customer);
46        return customerRepository.save(customer);
47    }
48
49    @CacheEvict(cacheNames = "customerCache", key = "#id")
50    public void deleteById(Long id) {
51        log.info("Deleting customer {} from db!", id);
52        customerRepository.deleteById(id);
53    }
54
55    /**
56     * Will evict all entries in cache
57     */
58    @CacheEvict(cacheNames = "customerCache", allEntries = true)
59    public void evictAll() {
60        log.info("evicting all customers from cache");
61    }
62}
 1package com.demo.project98.service;
 2
 3import java.math.BigDecimal;
 4
 5import lombok.extern.slf4j.Slf4j;
 6import org.springframework.cache.annotation.Cacheable;
 7import org.springframework.stereotype.Service;
 8
 9@Service
10@Slf4j
11public class NumberService {
12
13    /**
14     * @Cacheable results would be stored
15     * Condition is that result will be stored only for numbers > 10
16     */
17    @Cacheable(value = "squareCache", key = "#number", condition = "#number>10")
18    public BigDecimal square(Long number) {
19        BigDecimal square = BigDecimal.valueOf(number).multiply(BigDecimal.valueOf(number));
20        log.info("Square of {} is {}", number, square);
21        return square;
22    }
23}
24

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

Postman

Import the postman collection to postman

Postman Collection

Setup

 1# Project 98
 2
 3Spring Boot & Ehcache
 4
 5[https://gitorko.github.io/spring-ehcache/](https://gitorko.github.io/spring-ehcache/)
 6
 7### Version
 8
 9Check version
10
11```bash
12$java --version
13openjdk 21.0.3 2024-04-16 LTS
14```
15
16### Postgres DB
17
18```
19docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:14
20docker ps
21docker exec -it pg-container psql -U postgres -W postgres
22CREATE USER test WITH PASSWORD 'test@123';
23CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
24grant all PRIVILEGES ON DATABASE "test-db" to test;
25
26docker stop pg-container
27docker start pg-container
28```
29
30### Dev
31
32To run the backend in dev mode.
33
34```bash
35./gradlew clean build
36./gradlew bootRun
37```

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