MS Lesson16: Cache. L1 cache, Redis

1. L1 chace first level cache-dir, Hibernate-in en esas daxili cache-dir. 

Her Session/EntityManager ucun ayrica olur. Default olaraq hemish aktivdir. Session muddetinde yuklenen entity-leri yadda saxlayir. Session bitende  (transaction bitib context baglananda) bu cache-da bitir. 

Arxa fonda Map<EntityKey, Object> sheklinde saxlanilir. Bu map proxy uzerinden yaranmir. Bu map birbasha Session-un daxili strukturudur.

*** Eyni EntityManager/Session daxilinde icra olunan query entity instance qaytarirsa, Hibernate onu persistance context-e daxil edir. 


mes menim bele bir kodum varsa:

@Override
@Transactional
public void foo() {
Account parvin = accountRepository.findAccountByName("Parvin").get();
Account account = accountRepository.findAccountByName("Parvin").get();
System.out.println(parvin == account);
}

Burada addim addim ne bash verir:

1. Hibernate ilk select-i atir, ve oyekti getirir. Sonra map-a qoyur key = id value = entity ozu. 

2. Hibernate ikinci select-i atir, ve raw getiri, raw-in icinden primary key goturur. Sonra map-in icinde hemin primary key-e gore axtarish edir. Eger varsa o zaman L1 cache-in icinde hemin obyekti goturub istifade edir. Eger yoxdursa o zaman map-a put edir. 



novbeti example:

@Override
@Transactional
public void foo() {
Account parvin = accountRepository.findById(1L).get();
Account account = accountRepository.findById(1L).get();
System.out.println(parvin == account);
}


1. Hibernate ilk defe select atir. Sonra map-a qoyur

2. Hibernate ikinci defe gedib map-i yoxlayir, tapdisa query atmir.



novbeti example:

@Override
@Transactional
public void foo() {
List<Account> all = accountRepository.findAll();
Account account = accountRepository.findAccountByName("Parvin").get();
Account account1 = all.stream().filter(el -> el.getId().equals(1L)).findFirst().get();
System.out.println(account1 == account);
}

Burada findAll() olmasina baxmayaraq, getirilen entity-leri hamisini map-a yazir.



novbeti example:

@Override
@Transactional
public void foo() {
List<Account> all1 = accountRepository.findAll();
System.out.println(all1);
List<Account> all2 = accountRepository.findAll();
System.out.println(all2);
}

1. Hibernate birinci select all atir ve butun account-lari getirir ve map-a put edir.

2. Butun entitleri print etdiyimiz ucun n + 1 problemi aliriq ve bu zaman child entity-leri getirmek ucun elave query atilir. 

3. Hibernate iki select all atir bu zaman map-a put etmeye ehtiyac yoxdur.



novbeti example:

@Override
@Transactional
public void foo() {
List<Account> all1 = accountRepository.findAll();
System.out.println(all1);
AccountTransaction accountTransaction = accountTransactionRepository.findById(1L).get();
System.out.println(accountTransaction);
List<Account> all2 = accountRepository.findAll();
System.out.println(all2);
}

Bu zaman child-lari da map-a yazir.



novbeti example:

 @Override
@Transactional
public void foo() {
List<Account> all1 = accountRepository.findAll();
// System.out.println(all1);
AccountTransaction accountTransaction = accountTransactionRepository.findById(1L).get();
System.out.println(accountTransaction);
List<Account> all2 = accountRepository.findAll();
// System.out.println(all2);
}

Burada nece select atilacaq?




                                                                    Redis

dependency:

implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'


main class-a annotasiya elave etmek lazimdir:

@EnableCaching


docker-compose.yaml fayli:

redis:
image: redis:7-alpine
container_name: redis_crud
restart: always
ports:
- "6379:6379"
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redis_data:/data


application.yaml:

cache:
type: redis
redis:
time-to-live: 10m
cache-null-values: false
data:
redis:
host: localhost
port: 6379
timeout: 2s



Redis-e qoyacagimiz entity Serializable olmalidir. Cunki Redis obyekt saxlamir, byte saxlayir. Entity-ni Redis-e qoymaq ucun object -> byte cevrilmesi lazimdir (serialization). 


@Cacheable

id-ye gore cache-lamaq:

@Override
@Cacheable(value = "accounts", key = "#id")
public Account testRedis(Long id) {
return accountRepository.findById(id).orElseThrow();
}


@Cachable o demekdir ki, Spring bu bean ucun proxy yaradir ve xaricden gelen cagirish evvel proxy-ye dushur. 


Eger metod void olsa o zaman cache-leyecek obyekt olmadigi ucun cache hecne yazilmayacaq. @Cacheable metodun qaytardigi deyeri cache-leyir. 



@CacheEvict

@Caching(evict = {
@CacheEvict(value = "accounts", key = "#id"),
@CacheEvict(value = "accounts", key = "'all'")
})
public void deleteById(Long id) {
accountRepository.deleteById(id);
}



@CachePut

@CachePut(value = "accounts", key = "#result.id")
public Account updateScholarship(Long id) {
Account acc = accountRepository.findById(id).get();
acc.setAmount(5555);
return accountRepository.save(acc);
}






hamisini cache-lamaq:

@Override
@Cacheable(value = "accounts", key = "'all'")
public List<Account> findAllCached() {
return accountRepository.findAll();
}



Novbeti addim eger biz redis-de isteyirikse entity-ler json sheklinde yazilsin o zaman lazimi config elave edirik:


package guru.springframework.cruddemo.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import tools.jackson.databind.json.JsonMapper;

import java.time.Duration;

@Configuration
public class RedisCacheConfig {

@Bean
public JsonMapper redisJsonMapper() {
return JsonMapper.builder().build();
}

@Bean
public RedisSerializer<Object> redisValueSerializer() {
return RedisSerializer.json();
}

@Bean
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory connectionFactory,
RedisSerializer<Object> redisValueSerializer) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setValueSerializer(redisValueSerializer);
template.setHashValueSerializer(redisValueSerializer);
template.afterPropertiesSet();
return template;
}

@Bean
public CacheManager cacheManager(
RedisConnectionFactory connectionFactory,
RedisSerializer<Object> redisValueSerializer,
@Value("${spring.cache.redis.time-to-live:10m}") Duration entryTtl) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(entryTtl)
.disableCachingNullValues()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(redisValueSerializer));

return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}



Novbeti addim: eger biz Redis-i daha da flexible etmek isteyirikse:

package guru.springframework.cruddemo.service.impl;

import guru.springframework.cruddemo.service.FlexibleRedisStore;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.stereotype.Service;
import tools.jackson.databind.json.JsonMapper;

import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class FlexibleRedisStoreImpl implements FlexibleRedisStore {

private final RedisTemplate<String, Object> redisTemplate;
private final JsonMapper redisJsonMapper;

@Value("${app.redis.flexible.key-prefix:cruddemo}")
private String keyPrefix;

@Value("${app.redis.flexible.default-ttl:10m}")
private Duration defaultTtl;

private String compositeKey(String bucket, String key) {
return keyPrefix + ":" + bucket + ":" + key;
}

private String bucketPattern(String bucket) {
return keyPrefix + ":" + bucket + ":*";
}

@Override
public <T> void put(String bucket, String key, T value) {
put(bucket, key, value, defaultTtl);
}

@Override
public <T> void put(String bucket, String key, T value, Duration ttl) {
if (ttl == null || ttl.isZero() || ttl.isNegative()) {
putPersistent(bucket, key, value);
return;
}
redisTemplate.opsForValue().set(compositeKey(bucket, key), value, ttl);
}

@Override
public <T> void putPersistent(String bucket, String key, T value) {
redisTemplate.opsForValue().set(compositeKey(bucket, key), value);
}

@Override
public <T> Optional<T> get(String bucket, String key, Class<T> type) {
Object raw = redisTemplate.opsForValue().get(compositeKey(bucket, key));
if (raw == null) {
return Optional.empty();
}
if (type.isInstance(raw)) {
return Optional.of(type.cast(raw));
}
return Optional.of(redisJsonMapper.convertValue(raw, type));
}

@Override
public boolean delete(String bucket, String key) {
Boolean removed = redisTemplate.delete(compositeKey(bucket, key));
return Boolean.TRUE.equals(removed);
}

@Override
public long deleteBucket(String bucket) {
String pattern = bucketPattern(bucket);
ScanOptions options = ScanOptions.scanOptions().match(pattern).count(256).build();
List<String> keys = new ArrayList<>();
try (Cursor<String> cursor = redisTemplate.scan(options)) {
while (cursor.hasNext()) {
keys.add(cursor.next());
}
}
if (keys.isEmpty()) {
return 0L;
}
Long deleted = redisTemplate.delete(keys);
return deleted == null ? 0L : deleted;
}

@Override
public boolean hasKey(String bucket, String key) {
Boolean exists = redisTemplate.hasKey(compositeKey(bucket, key));
return Boolean.TRUE.equals(exists);
}

}



* Redis-i message queue kimi ishletmek.

Producer

package guru.springframework.cruddemo.controller;

import guru.springframework.cruddemo.dto.PersonQueuePayload;
import guru.springframework.cruddemo.service.RedisQueueProducerService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/queue")
@RequiredArgsConstructor
public class RedisQueueProducerController {

private final RedisQueueProducerService redisQueueProducerService;

@PostMapping("/persons/{id}")
public PersonQueuePayload enqueuePersonFromDb(@PathVariable Long id) {
return redisQueueProducerService.enqueuePersonById(id);
}

@PostMapping("/persons")
public PersonQueuePayload enqueuePersonBody(@RequestBody PersonQueuePayload payload) {
redisQueueProducerService.enqueuePersonPayload(payload);
return payload;
}

}


package guru.springframework.cruddemo.service.impl;

import guru.springframework.cruddemo.dto.PersonQueuePayload;
import guru.springframework.cruddemo.entity.Person;
import guru.springframework.cruddemo.repository.PersonRepository;
import guru.springframework.cruddemo.service.RedisQueueProducerService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.server.ResponseStatusException;

@Service
@RequiredArgsConstructor
public class RedisQueueProducerServiceImpl implements RedisQueueProducerService {

private final RedisTemplate<String, Object> redisTemplate;
private final PersonRepository personRepository;

@Value("${app.redis.queue.person-list-key}")
private String personQueueKey;

@Override
@Transactional(readOnly = true)
public PersonQueuePayload enqueuePersonById(Long id) {
Person person = personRepository.findById(id).get();
PersonQueuePayload payload = toPayload(person);
redisTemplate.opsForList().rightPush(personQueueKey, payload);
return payload;
}

@Override
public Long enqueuePersonPayload(PersonQueuePayload payload) {
Long size = redisTemplate.opsForList().rightPush(personQueueKey, payload);
return size;
}

private static PersonQueuePayload toPayload(Person person) {
return new PersonQueuePayload(
person.getId(),
person.getName(),
person.getEmail(),
person.getScholarship(),
person.getCreatedAt());
}
}


Consumer

package guru.springframework.cruddemo.controller;

import guru.springframework.cruddemo.dto.PersonQueuePayload;
import guru.springframework.cruddemo.service.RedisQueueConsumerService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.time.Duration;

@RestController
@RequestMapping("/queue")
@RequiredArgsConstructor
public class RedisQueueConsumerController {

private final RedisQueueConsumerService redisQueueConsumerService;

@Value("${app.redis.queue.consume-wait-seconds:10}")
private int configuredWaitSeconds;

@PostMapping("/persons/consume")
public ResponseEntity<PersonQueuePayload> consumeOne(
@RequestParam(required = false) Integer waitSeconds) {
int seconds = waitSeconds != null
? Math.min(Math.max(waitSeconds, 1), 120)
: Math.min(Math.max(configuredWaitSeconds, 1), 120);
return redisQueueConsumerService
.consumeOne(Duration.ofSeconds(seconds))
.map(ResponseEntity::ok)
.orElse(ResponseEntity.noContent().build());
}

}


package guru.springframework.cruddemo.service.impl;

import guru.springframework.cruddemo.dto.PersonQueuePayload;
import guru.springframework.cruddemo.service.RedisQueueConsumerService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import tools.jackson.databind.json.JsonMapper;

import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class RedisQueueConsumerServiceImpl implements RedisQueueConsumerService {

private final RedisTemplate<String, Object> redisTemplate;
private final JsonMapper redisJsonMapper;

@Value("${app.redis.queue.person-list-key}")
private String personQueueKey;

@Override
public Optional<PersonQueuePayload> consumeOne(Duration blockTimeout) {
long seconds = Math.max(0, blockTimeout.toSeconds());
Object raw = redisTemplate.opsForList().leftPop(personQueueKey, seconds, TimeUnit.SECONDS);
if (raw == null) {
return Optional.empty();
}
if (raw instanceof PersonQueuePayload payload) {
return Optional.of(payload);
}
return Optional.of(redisJsonMapper.convertValue(raw, PersonQueuePayload.class));
}

}














Комментарии

Популярные сообщения из этого блога

Interview questions

Lesson1: JDK, JVM, JRE

Lesson_2: Operations in Java