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 — u konzoli se pojavljuje ASCII logo i Ready to accept connections tcp.
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 (Software Store → pretraga „Redis“) da ga instaliram — panel ga instalira kao servis i nudi podešavanje porta i lozinke kroz grafički obrazac.
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 — u klijentu kliknete New Connection, unesete host (localhost), port (6379) i opcionu lozinku, pa Test Connection → Save.
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): izaberite bazu u levom panelu, kliknite dugme konzole i unosite komande.
Odgovarajuće tekstualne komande:
## Dodaj ključ "name" sa vrednošću Marko Marković
> set name 'Marko Marković'
OK
## Čitanje
> get name
Marko Marković
> set name 'Petar Petrović'
OK
> get name
Petar Petrović
## 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 Marko Marković
> hset user_hset user1 Marko Marković
1
> hset user_hset user2 Petar Petrović
1
## Broj polja
> hlen user_hset
2
## Sva polja
> HKEYS user_hset
user1
user2
## Sve vrednosti
> hvals user_hset
Marko Marković
Petar Petrović
## Vrednost polja user1
> hget user_hset user1
Marko Marković
## Dohvati sva polja i vrednosti za ključ user_hset
> hgetall user_hset
user1
Marko Marković
user2
Petar Petrović
## Ažuriranje polja
> hset user_hset user1 Nikola Nikolić
0
> hgetall user_hset
user1
Nikola Nikolić
user2
Petar Petrović
> hdel user_hset user1
1
> hgetall user_hset
user2
Petar Petrović3) list
list je jednostavna lista niski, sortirana prema redosledu umetanja.
## Dodaj ključ user_list sa vrednostima Marko Marković i Petar Petrović
> lpush user_list Marko Marković Petar Petrović
2
## Upit
> lrange user_list 0 -1
Petar Petrović
Marko Marković
## Dodaj na kraj
> rpush user_list Marko Marković je glup
3
## Dodaj na početak
> lpush user_list Marko Marković je budala
4
> lrange user_list 0 -1
Marko Marković je budala
Petar Petrović
Marko Marković
Marko Marković je glup
## Ažuriraj vrednost na indeksu 0
> lset user_list 0 Nikola Nikolić
OK
> lrange user_list 0 -1
Nikola Nikolić
Petar Petrović
Marko Marković
Marko Marković je glup
## Obriši vrednost na indeksu 0
> lrem user_list 0 Nikola Nikolić
1
> lrange user_list 0 -1
Petar Petrović
Marko Marković
Marko Marković je glup4) set
set je neuređeni skup niski i ne dozvoljava duplikate.
## Dodaj ključ user_set sa vrednostima Marko Marković, Petar Petrović i Markov podanik
> sadd user_set Marko Marković Petar Petrović Markov podanik
3
## Upit
> smembers user_set
Marko Marković
Markov podanik
Petar Petrović
## Obriši element sa vrednošću Marko Marković
> srem user_set Marko Marković
1
> smembers user_set
Markov podanik
Petar Petrović
## Dodavanje
> sadd user_set Marko Marković
1
> smembers user_set
Marko Marković
Markov podanik
Petar Petrović5) sorted set
sorted set je uređeni skup niski i ne dozvoljava duplikate.
> FLUSHDB
OK
## Dodaj ključ user_zset: rezultat 1 → Marko Marković, rezultat 2 → Petar Petrović, rezultat 3 → Markov podanik
> zadd user_zset 1 Marko Marković 2 Petar Petrović 3 Markov podanik
3
## Upit
> zrange user_zset 0 -1
Marko Marković
Petar Petrović
Markov podanik
## Obrnuti redosled
> zrevrange user_zset 0 -1
Markov podanik
Petar Petrović
Marko Marković
## Dohvati rezultat elementa Marko Marković
> zscore user_zset Marko Marković
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","Marko Marković");
// Upit
System.out.println(redisTemplate.opsForValue().get("name"));
// Brisanje
redisTemplate.delete("name");
// Ažuriranje
redisTemplate.opsForValue().set("name","Markov podanik");
// Upit
System.out.println(redisTemplate.opsForValue().get("name"));
// Dodavanje
stringRedisTemplate.opsForValue().set("name","Marko Marković");
// Upit
System.out.println(stringRedisTemplate.opsForValue().get("name"));
// Brisanje
stringRedisTemplate.delete("name");
// Ažuriranje
stringRedisTemplate.opsForValue().set("name","Markov 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 izlaz testa — npr. set name = Marko Marković, get name = Marko Marković i sl., zavisno od test metoda.
Podatke u Redis bazi možete pogledati i kroz klijent AnotherRedisDesktopManager — ključ name videćete u listi ključeva izabrane baze, zajedno sa tipom (string) i vrednošću.
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 — odgovor API-ja vraća izmenjeni tag.
Preko klijenta Red (Redis desktop alat za macOS) možete videti da je povratna vrednost koju smo upravo ažurirali već dodata u Redis — pod ključem keša pojaviće se novi sadržaj taga.
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 (Lettuce: ~5k zvezdica, Jedis: ~11k zvezdica).
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 — stablo redisConnectionFactory → clientConfiguration → poolConfig sa podešenim maxIdle, maxActive i maxWait.
Ako u fajlu application-dev.yml niste dodali konfiguraciju za Lettuce connection pool, poolConfig nećete videti — clientConfiguration tada ostaje na podrazumevanim vrednostima bez pool-a.
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 (commons-pool2) — to možete proveriti otvaranjem stabla 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.
Zamislite da metoda vraća JSON odgovor koji se šalje klijentu (sa poljima code, message, data) — u keš bismo radije sačuvali sam ažurirani tag nego taj omotani JSON. 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
