Many times I have met a problem with the delay of reading data from the service. How to speed up the process? How to make reading data faster? There is solution: need caching data for reading. I ve found a lot of examples of data caching implementation, but all of them covered quite simple cases and there was not the most important one: caching the entire collection and invalidate cache data during writing to the database.
I propose to do this using the spring boot framework, Caffeine Cache, Spring Aop, I won’t specify ORM and database i will use, it will work with other data layer implementation. In my case it is jooq and postgres.
1.add dependencies
implementation 'org.springframework.boot:spring-boot-starter-aop'
implementation 'com.github.ben-manes.caffeine:caffeine:3.0.4'
implementation 'javax.cache:cache-api'
implementation 'org.springframework.boot:spring-boot-starter-cache'
Create cache configuration
package org.springframework.samples.petclinic.system;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
class CacheConfiguration {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("owner");
cacheManager.setCaffeine(caffeineCacheBuilder());
return cacheManager;
}
private Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(10)
.maximumSize(10);
}
}
create service that reading data from cache
package org.springframework.samples.petclinic.owner;
import com.github.benmanes.caffeine.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.concurrent.ConcurrentMap;
@Service
public class OwnerService {
private final CacheManager manager;
private final OwnerRepository repository;
private final ConcurrentMap ownerCache;
public OwnerService(CacheManager manager, OwnerRepository repository) {
this.manager = manager;
this.repository = repository;
Cache nativeCache = (Cache) manager.getCache("owner").getNativeCache();
ownerCache = nativeCache.asMap();
}
@PostConstruct
public void init() {
repository.findAll().forEach(elem -> {
ownerCache.put(elem.getId(), elem);
});
}
public Page<Owner>findByLastName(String lastName) {
List filteredOwners = ownerCache.values().stream().filter(item->{
Owner owner = (Owner) item;
return owner.getLastName().contains(lastName);
}).toList();
return new PageImpl<>(filteredOwners);
}
public Owner findById(Integer id){
return (Owner)ownerCache.get(id);
}
}
create aspect, that will invalidate cache during new entity creation or update.
package org.springframework.samples.petclinic.owner;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Optional;
@Aspect
@Component
public class CacheOwnerAspect {
private final Cache cache;
private final OwnerRepository repository;
public CacheOwnerAspect(CacheManager manager,
OwnerRepository repository) {
this.cache = manager.getCache("owner");
this.repository = repository;
}
@Pointcut(
"@annotation(org.springframework.samples.petclinic.owner.InvalidateOwnerCache) "
)
public void invalidate(){
}
@After("invalidate()")
public void invalidateOwner(JoinPoint jp){
Optional<Object> first = Arrays.stream(jp.getArgs()).findFirst();
if(!(first.orElseThrow(()->new RuntimeException("first arg was null")) instanceof Owner)) {
throw new RuntimeException("first arg have to be Owner type");
}
Owner owner = (Owner) first.get();
Owner ownerFromDb = repository.findById(owner.getId());
cache.put(ownerFromDb.getId(),ownerFromDb);
}
}
Create annotation, that will point to aspect when need to invalidate data in cache.
package org.springframework.samples.petclinic.owner;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface InvalidateOwnerCache {
}
Write the above annotation in repository before methods that will start cache invalidating after execution.
@InvalidateOwnerCache
public OwnersRecord save(Owner owner) {
owner.setId(context.nextval("owners_id_seq").intValue());
return context
.insertInto(OWNERS)
.set(context.newRecord(OWNERS, owner))
.returning().fetchOne();
}
@InvalidateOwnerCache
public void update(Owner owner) {
OwnersRecord record = context.newRecord(OWNERS, owner);
record.update();
}
Then we can replace source for all queries for reading from database to cache for better perfomance.