Как стать автором
Обновить

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

Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.