Spring Boot integracija sa Redis kešom
Kao developeri, svi verovatno dobro znate važnost Redisa. Redis je visokoperformansna baza podataka tipa ključ-vrednost, napisana u C-u, i najšire korišćenje skladišni middleware u polju internet tehnologija. Ime je akronim za „Remote Dictionary Service“, odnosno „udaljenu rečničku uslugu“.
Redis je poznat po izuzetnoj performansi, besprekornoj dokumentaciji i preglednom izvornom kodu. Koriste ga mnoge velike internet kompanije kako u Kini tako i u svetu — na primer Alibaba, Tencent, GitHub, Stack Overflow itd. Naravno, koriste ga i male i srednje kompanije.
Instalacija Redisa
Zvanični sajt Redisa nudi instalacione pakete za sve relevantne platforme: Linux, macOS i Windows.

Zvanična adresa: https://redis.io/docs/getting-started/
Ja lično koristim macOS, pa je dovoljno da pokrenem brew install redis i instalacija je završena.

Nakon instalacije, komandom redis-server pokrećete Redis servis.

Korisnici Windowsa mogu iskoristiti chocolatey, alat čarobnjak za upravljanje softverom iz komandne linije (kliknite na link za detalje) — dovoljna je jedna komanda choco install redis i instalacija je gotova, što je veoma zgodno.

U produkcionom okruženju Redis se najčešće instalira na Linuxu. Na mom serveru je instaliran BaoTa panel, tako da mogu direktno u prodavnici softvera da potražim ključnu reč „Redis“ i da ga instaliram (već sam ga instalirao).

Uz to, instalirajte i klijentski alat za Redis. Preporučujem AnotherRedisDesktopManager, koji na GitHub-u ima preko 20k zvezdica — 🚀🚀🚀 brži, bolji i stabilniji desktop (GUI) klijent za upravljanje Redisom, podržava Windows, macOS i Linux, ima odlične performanse i lako učitava ogroman broj ključeva.
Nakon instalacije, povežite se na Redis servis:

Redis tipovi podataka
Redis podržava pet tipova podataka: string (niska), hash (heš), list (lista), set (skup) i zset (sorted set — uređeni skup).
Redis tutorijal: https://www.redis.net.cn/tutorial/3508.html
1) string
string je najosnovniji tip podatka u Redisu — jedan ključ mapira na jednu vrednost.
Možemo vežbati osnovne komande set i get kroz klijent AnotherRedisDesktopManager (uz pomoć Redis dokumentacije — klijent nudi sugestije, pa komande ne treba učiti napamet).

Odgovarajuće tekstualne komande:
## Dodaj ključ "name" sa vrednošću Chenmo Wang Er
> set name 'Chenmo Wang Er'
OK
## Čitanje
> get name
Chenmo Wang Er
> set name 'Chenmo Wang San'
OK
> get name
Chenmo Wang San
## Brisanje
> del name
1
> get name
null
## Provera da li "name" postoji
> EXISTS key
0
> EXISTS name
02) hash
Redis hash je kolekcija parova ključ-vrednost; samu vrednost možemo posmatrati kao jednu Mapu.
## Brisanje baze
> flushdb
OK
## Kreiraj hash, ključ user_hset, polje user1, vrednost Chenmo Wang Er
> hset user_hset user1 Chenmo Wang Er
1
> hset user_hset user2 Chenmo Wang San
1
## Broj polja
> hlen user_hset
2
## Sva polja
> HKEYS user_hset
user1
user2
## Sve vrednosti
> hvals user_hset
Chenmo Wang Er
Chenmo Wang San
## Vrednost polja user1
> hget user_hset user1
Chenmo Wang Er
## Dohvati sva polja i vrednosti za ključ user_hset
> hgetall user_hset
user1
Chenmo Wang Er
user2
Chenmo Wang San
## Ažuriranje polja
> hset user_hset user1 Chenmo Wang Si
0
> hgetall user_hset
user1
Chenmo Wang Si
user2
Chenmo Wang San
> hdel user_hset user1
1
> hgetall user_hset
user2
Chenmo Wang San3) list
list je jednostavna lista niski, sortirana prema redosledu umetanja.
## Dodaj ključ user_list sa vrednostima Chenmo Wang Er i Chenmo Wang San
> lpush user_list Chenmo Wang Er Chenmo Wang San
2
## Upit
> lrange user_list 0 -1
Chenmo Wang San
Chenmo Wang Er
## Dodaj na kraj
> rpush user_list Chenmo Wang Er je glup
3
## Dodaj na početak
> lpush user_list Chenmo Wang Er je budala
4
> lrange user_list 0 -1
Chenmo Wang Er je budala
Chenmo Wang San
Chenmo Wang Er
Chenmo Wang Er je glup
## Ažuriraj vrednost na indeksu 0
> lset user_list 0 Chenmo Wang Si
OK
> lrange user_list 0 -1
Chenmo Wang Si
Chenmo Wang San
Chenmo Wang Er
Chenmo Wang Er je glup
## Obriši vrednost na indeksu 0
> lrem user_list 0 Chenmo Wang Si
1
> lrange user_list 0 -1
Chenmo Wang San
Chenmo Wang Er
Chenmo Wang Er je glup4) set
set je neuređeni skup niski i ne dozvoljava duplikate.
## Dodaj ključ user_set sa vrednostima Chenmo Wang Er, Chenmo Wang San i Chenmov podanik
> sadd user_set Chenmo Wang Er Chenmo Wang San Chenmov podanik
3
## Upit
> smembers user_set
Chenmo Wang Er
Chenmov podanik
Chenmo Wang San
## Obriši element sa vrednošću Chenmo Wang Er
> srem user_set Chenmo Wang Er
1
> smembers user_set
Chenmov podanik
Chenmo Wang San
## Dodavanje
> sadd user_set Chenmo Wang Er
1
> smembers user_set
Chenmo Wang Er
Chenmov podanik
Chenmo Wang San5) sorted set
sorted set je uređeni skup niski i ne dozvoljava duplikate.
> FLUSHDB
OK
## Dodaj ključ user_zset: rezultat 1 → Chenmo Wang Er, rezultat 2 → Chenmo Wang San, rezultat 3 → Chenmov podanik
> zadd user_zset 1 Chenmo Wang Er 2 Chenmo Wang San 3 Chenmov podanik
3
## Upit
> zrange user_zset 0 -1
Chenmo Wang Er
Chenmo Wang San
Chenmov podanik
## Obrnuti redosled
> zrevrange user_zset 0 -1
Chenmov podanik
Chenmo Wang San
Chenmo Wang Er
## Dohvati rezultat elementa Chenmo Wang Er
> zscore user_zset Chenmo Wang Er
1Spring Boot integracija sa Redisom
Prvi korak: u fajlu pom.xml dodajte Redis starter.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>Drugi korak: u fajlu application.yml dodajte konfiguraciju za Redis.
spring:
redis:
host: xxx.xxx.99.232 # Adresa Redis servera
database: 0 # Indeks Redis baze (podrazumevano 0)
port: 6379 # Port za povezivanje sa Redis serverom
password: xxx # Lozinka za povezivanje sa Redis serverom (podrazumevano prazna)Treći korak: u testnu klasu dodajte sledeći kod.
@SpringBootTest
class CodingmoreRedisApplicationTests {
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
public void testRedis() {
// Dodavanje
redisTemplate.opsForValue().set("name","Chenmo Wang Er");
// Upit
System.out.println(redisTemplate.opsForValue().get("name"));
// Brisanje
redisTemplate.delete("name");
// Ažuriranje
redisTemplate.opsForValue().set("name","Chenmov podanik");
// Upit
System.out.println(redisTemplate.opsForValue().get("name"));
// Dodavanje
stringRedisTemplate.opsForValue().set("name","Chenmo Wang Er");
// Upit
System.out.println(stringRedisTemplate.opsForValue().get("name"));
// Brisanje
stringRedisTemplate.delete("name");
// Ažuriranje
stringRedisTemplate.opsForValue().set("name","Chenmov podanik");
// Upit
System.out.println(stringRedisTemplate.opsForValue().get("name"));
}
}I RedisTemplate i StringRedisTemplate su klase šablona koje pruža Spring Data Redis i kojima se operiše nad Redisom. Ova druga je namenjena podacima kod kojih su i ključ i vrednost tipa String, dok prva može raditi sa bilo kojom vrstom objekata.
Pored metode opsForValue za rad sa niskama, RedisTemplate i StringRedisTemplate nude i sledeće metode:
- opsForList: rad sa list
- opsForSet: rad sa set
- opsForZSet: rad sa uređenim set
- opsForHash: rad sa hash
Nakon pokretanja testne klase, u konzoli ćete videti sledeći izlaz:

Podatke u Redis bazi možete pogledati i kroz klijent AnotherRedisDesktopManager.

Codingmore integracija sa Redisom
Codingmore je projekat sa odvojenim front-endom i back-endom (Spring Boot + Vue). Najbolji način da se u njega integriše Redis jeste upotreba Spring Cachea — dovoljne su anotacije @Cacheable, @CachePut, @CacheEvict i @EnableCaching pa Redis lako možete koristiti kao keš.

1) @EnableCaching — uključuje funkciju keširanja.
2) @Cacheable — pre poziva metode traži u kešu; ako nađe, vraća vrednost, a ako ne nađe, izvršava metodu i povratnu vrednost smešta u keš.
3) @CachePut — pre poziva metode ne traži u kešu; metod se uvek izvrši, a zatim se povratna vrednost smešta u keš.
4) @CacheEvict — briše jedan ili više zapisa iz keša.
Spring Cache je celovito rešenje za keširanje koje pruža Spring. Iako sam po sebi ne implementira keš, nudi čitav skup interfejsa, pravila, konfiguracije i anotacija koji nam omogućavaju da neprimetno povežemo Redis, Ehcache i druge konkretne implementacije keša.
Anotacije Spring Cachea (one gore navedene četiri) se, nakon poziva metode, koriste da se u keš smesti konačan rezultat metode; ili, pre poziva metode, da se iz keša uzme rezultat; a takođe i da se iz keša obriše rezultat.
Te operacije čitanja i pisanja ne moramo ručno da implementiramo — prepustimo ih Spring Cacheu i to je to. Zar nije sjajno?

Prvi korak: u fajl pom.xml dodajte Redis starter.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>Drugi korak: u fajl application.yml dodajte konfiguraciju veze ka Redisu.
spring:
redis:
host: 118.xx.xx.xxx # Adresa Redis servera
database: 0 # Indeks Redis baze (podrazumevano 0)
port: 6379 # Port za povezivanje sa Redis serverom
password: xx # Lozinka za povezivanje sa Redis serverom (podrazumevano prazna)
timeout: 1000ms # Vreme čekanja na povezivanje (u milisekundama)Treći korak: dodajte novu klasu RedisConfig.java. Pomoću RedisTemplatea postavite JSON serijalizator — tako će podaci smešteni u Redis biti tipizirani JSON, na primer:
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// Serijalizacija pomoću Jackson komponente
RedisSerializer<Object> serializer = redisSerializer();
// ključ i vrednost
// Generalno, redis-key koristi string serijalizaciju;
// redis-value koristi JSON serijalizaciju — JSON je manjeg obima, čitljiviji i ne zahteva implementaciju serializer interfejsa.
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(serializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Bean
public RedisSerializer<Object> redisSerializer() {
// Kreiraj JSON serijalizator
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// https://www.cnblogs.com/shanheyongmu/p/15157378.html
// objectMapper.enableDefaultTyping() je zastareo
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.WRAPPER_ARRAY);
serializer.setObjectMapper(objectMapper);
return serializer;
}
}Pomoću RedisCacheConfiguration postavite vreme isteka kako biste izbegli nagomilavanje nepotrebnih podataka u kešu.
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
// Postavi važenje Redis keša na 1 dan
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer())).entryTtl(Duration.ofDays(1));
return new RedisCacheManager(redisCacheWriter, redisCacheConfiguration);
}Četvrti korak: interfejsu za ažuriranje tagova dodajte anotaciju @CachePut — dakle, pre poziva metode se ne traži u kešu, ali se nakon izvršenja metode povratna vrednost smešta u keš.
@Controller
@Api(tags = "Tagovi")
@RequestMapping("/postTag")
public class PostTagController {
@Autowired
private IPostTagService postTagService;
@Autowired
private IPostTagRelationService postTagRelationService;
@RequestMapping(value = "/update", method = RequestMethod.POST)
@ResponseBody
@ApiOperation("Izmeni tag")
@CachePut(value = "codingmore", key = "'codingmore:postag:'+#postAddTagParam.postTagId")
public ResultObject<String> update(@Valid PostTagParam postAddTagParam) {
if (postAddTagParam.getPostTagId() == null) {
return ResultObject.failed("ID taga ne sme biti prazan");
}
PostTag postTag = postTagService.getById(postAddTagParam.getPostTagId());
if (postTag == null) {
return ResultObject.failed("Tag ne postoji");
}
QueryWrapper<PostTag> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("description", postAddTagParam.getDescription());
int count = postTagService.count(queryWrapper);
if (count > 0) {
return ResultObject.failed("Ime taga već postoji");
}
BeanUtils.copyProperties(postAddTagParam, postTag);
return ResultObject.success(postTagService.updateById(postTag) ? "Izmena uspešna" : "Izmena neuspešna");
}
}Obratite pažnju na liniju sa anotacijom @CachePut:
@CachePut(value = "codingmore", key = "'codingmore:postag:'+#postAddTagParam.postTagId")- value: ime keša, odnosno namespace keša; ovde bi
namespacebio bolji naziv odvalue; - key: vrednost ključa keša unutar namespacea, može se pisati kao SpEL izraz, npr.
'codingmore:postag:'+#postAddTagParam.postTagId; - preostala svojstva
unlessiconditionza sada nisu u upotrebi — ona znače, redom: ako je uslov ispunjen, ne keširaj; ako je uslov ispunjen, keširaj.
Peti korak: pokrenite server, pokrenite klijent, izmenite neki tag i testirajte.

Preko klijenta Red (Redis desktop alat za macOS) možete videti da je povratna vrednost koju smo upravo ažurirali već dodata u Redis.

Korišćenje Redis connection pool-a
Redis je baza u memoriji i osmišljen je da poboljša performanse programa. Međutim, ako se ne koristi connection pool, uspostavljanje i raskidanje veze oduzima mnogo vremena.
Sa connection pool-om, klijent uspostavi više veza koje po potrebi uzima iz pool-a, a po završetku ih vraća nazad — time se štedi vreme na uspostavljanju i raskidanju veze.
Da bismo koristili connection pool, prvo moramo upoznati Redis klijente. Uobičajena su dva: Jedis i Lettuce.
- Jedis: podrazumevani Redis klijent u doba Spring Boota 1.5.x; direktno se povezuje na Redis Server i u višenitnom okruženju nije bezbedan po pitanju niti — tada se koristi connection pool kako bi se za svaki jedis instancu dodala fizička veza;
- Lettuce: podrazumevani Redis klijent od Spring Boota 2.x; zasnovan na Netty-ju, jedna instanca veze može se paralelno koristiti iz više niti, a po potrebi se mogu dodavati i dodatne instanci veze.
Oba su prilično popularna na GitHub-u — izaberite prema potrebama.

Ovde ću demonstrirati oba klijenta, radi lakše reference.
1) Lettuce
Prvi korak: izmenite application-dev.yml i dodajte konfiguraciju za Lettuce connection pool (čvor pool).
spring:
redis:
lettuce:
pool:
max-active: 8 # Maksimalan broj veza u pool-u
max-idle: 8 # Maksimalan broj slobodnih veza u pool-u
min-idle: 0 # Minimalan broj slobodnih veza u pool-u
max-wait: -1ms # Maksimalno vreme blokirajućeg čekanja u pool-u; negativna vrednost znači bez ograničenjaDrugi korak: u fajl pom.xml dodajte zavisnost commons-pool2, inače će pri pokretanju doći do greške ClassNotFoundException. Razlog je što Spring Boot 2.x podrazumevano ne uključuje connection pool.
Caused by: java.lang.ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
... 153 common frames omittedDodavanje zavisnosti commons-pool2:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.2</version>
<type>jar</type>
<scope>compile</scope>
</dependency>Ponovo pokrenite servis, u klasi RedisConfig unutar metode redisTemplate postavite breakpoint na redisTemplate i, u debug režimu, videćete informacije o konfiguraciji connection pool-a (redisConnectionFactory→clientConfiguration→poolConfig), kao što je prikazano na slici.

Ako u fajlu application-dev.yml niste dodali konfiguraciju za Lettuce connection pool, nećete videti

2) Jedis
Prvi korak: u fajl pom.xml dodajte zavisnost Jedis i uklonite podrazumevanu zavisnost Lettuce.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>Drugi korak: izmenite application-dev.yml i dodajte konfiguraciju za Jedis connection pool.
spring:
redis:
jedis:
pool:
max-active: 8 # Maksimalan broj veza u pool-u
max-idle: 8 # Maksimalan broj slobodnih veza u pool-u
min-idle: 0 # Minimalan broj slobodnih veza u pool-u
max-wait: -1ms # Maksimalno vreme blokirajućeg čekanja u pool-u; negativna vrednost znači bez ograničenjaNakon pokretanja servisa, posmatrajte čvor clientConfiguration unutar redisTemplate — videćete da mu je vrednost promenjena u objekat DefaultJedisClientConfiguration.

Naravno, možete i ne konfigurisati connection pool za Jedis klijent i prepustiti se podrazumevanoj konfiguraciji pool-a. Jedis klijent podrazumevano već povlači zavisnost connection pool-a — to možete proveri otvaranjem zavisnosti Jedis klijenta u fajlu pom.xml.

Slobodna operacija nad Redisom
Iako Spring Cache pruža zgodne metode za rad sa Redisom (poput anotacije @CachePut koju smo malopre demonstrirali), operacije koje anotacije omogućavaju su veoma ograničene — na primer, u keš mogu smeštati samo povratnu vrednost metode, a povratna vrednost nije nužno ono što bismo hteli da sačuvamo.

Umesto da čuvamo taj JSON koji se vraća klijentu, radije bismo sačuvali ažurirani tag. Kako onda slobodno operisati nad Redisom?

Prvi korak: dodajte interfejs RedisService:
public interface RedisService {
/**
* Čuvanje vrednosti
*/
void set(String key, Object value);
/**
* Dohvatanje vrednosti
*/
Object get(String key);
/**
* Brisanje vrednosti
*/
Boolean del(String key);
...
// Još metoda vidi na: https://github.com/itwanger/coding-more/blob/main/codingmore-mbg/src/main/java/com/codingmore/service/RedisService.java
}Drugi korak: dodajte implementacionu klasu RedisServiceImpl:
@Service
public class RedisServiceImpl implements RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
@Override
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
@Override
public Boolean del(String key) {
return redisTemplate.delete(key);
}
// Još koda vidi na: https://github.com/itwanger/coding-more/blob/main/codingmore-mbg/src/main/java/com/codingmore/service/impl/RedisServiceImpl.java
}Treći korak: u PostTagController za tagove dodajte Redis test interfejs simpleTest:
Izvorni kod projekta
- Codingmore: https://github.com/itwanger/coding-more
- Specijalno za Redis integraciju: https://github.com/itwanger/coding-more
