Pull to refresh

Cache invalidation spring boot

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'
  1. 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);
}

}
  1. 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 -&gt; {
		ownerCache.put(elem.getId(), elem);
	});
}

public Page<Owner>findByLastName(String lastName) {
	List filteredOwners = ownerCache.values().stream().filter(item-&gt;{
		Owner owner = (Owner) item;
		return owner.getLastName().contains(lastName);
	}).toList();
	return new PageImpl<>(filteredOwners);
}
public Owner findById(Integer id){
	return (Owner)ownerCache.get(id);
}

}
  1. 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);
}

}
  1. 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 {
}
  1. 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.

code example

Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.