Im meinem letzten Beitrag zu Spring Data habe ich eine Serie an Beiträge zu diesem Thema versprochen. Hier nun der zweite Teil. Diesmal verwende ich das Spring Data Modul für den Key-Value-Store Redis.
Seit Version 3.1 beinhaltet das Spring Framework die Cache Abstraction. Damit wird das Zwischenspeichern von Daten deklarativ mittels Annotation (@Cacheable
, @CachePut
, @CacheEvict
, usw.) gesteuert. Spring Data Redis implementiert auf Basis der Cache Abstraction einen Cache Manager der auf Redis zurückgreift. Redis eignet sich dank seiner guten Performance hervorragend als Cache. Der Key-Value Store schafft bis zu 100.000 Schreib- und bis zu 80.000 Lese-Vorgänge in der Sekunde.
Um das Ganze etwas anschaulicher zu machen, habe ich dafür eine Demo-Anwendung erstellt. Diese könnt ihr unter springdata-redis-example anschauen und klonen.
Zunächst etwas zur Konfiguration: Für dieses Beispiel habe ich Spring Boot verwendet. Eine Einleitung zu Spring Boot bekommt ihr im Artikel Microservices und Spring Boot meines Namensvettern Waldemar Schneider.
Spring Boot bietet diverse Starter-Pakete, u.a. auch für Redis. Neben diesem habe ich auch die Pakete für Spring Data JPA und Spring Test eingebunden. Zu sehen in der pom.xml
.
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.1.8.RELEASE</version> </parent> <groupId>de.flaviait.blog.springdata</groupId> <artifactId>springdata-redis-example</artifactId> <version>0-SNAPSHOT</version> <properties> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> <dependency> <groupId>org.hsqldb</groupId> <artifactId>hsqldb</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies> </project>
Damit Redis als Cache verwendet wird, muss ein entsprechender Cache Manager definiert werden. Dazu habe ich in der Klasse Application
ein RedisTemplate und den Cache Manager instanziert. Die Besonderheit am RedisTemplate ist, dass die Werte als JSON gespeichert werden. Dies wird über Jackson2JsonRedisSerializer
bewerkstelligt. Mit der Annotation @Cacheable
wird das Caching aktiviert.
@Configuration @ComponentScan @EnableAutoConfiguration @EnableCaching public class Application { @Bean public RedisTemplate<String, String> template(RedisConnectionFactory factory) { final StringRedisTemplate template = new StringRedisTemplate(factory); template.setValueSerializer(new Jackson2JsonRedisSerializer<Book>(Book.class)); return template; } @Bean public CacheManager cacheManager(RedisTemplate<?, ?> template) { final RedisCacheManager cacheManager = new RedisCacheManager(template); cacheManager.setTransactionAware(true); cacheManager.setDefaultExpiration(Duration.ofSeconds(5).getSeconds()); return cacheManager; } public static void main(String... args) { SpringApplication.run(Application.class, args); } }
Der Serializer arbeitet mit der Klasse Book
. Diese Entität besteht aus den Attributen ISBN (Primärschlüssel), Titel und Autoren.
Die Deklaration des Cachings geschieht an der Methode findOne
des BookRepository
. Das entsprechende Repository wird mittels Spring Data JPA instanziert.
public interface BookRepository extends CrudRepository<Book, String> { @Cacheable(key = "#a0", value = "books") Book findOne(String isbn); }
@Cacheable
sagt an dieser Stelle aus, dass das Ergebnis der Abfrage mit dem Schlüssel ISBN (#a0) zwischengespeichert werden soll. Das Attribut value
gibt den Cache an. Damit ist das Caching bereits komplett konfiguriert. Sollte die Methode findOne
weitere Male mit dem gleichen Parameter-Wert aufgerufen werden, so wird keine Abfrage ausgeführt, sondern auf den Wert im Cache zurückgegriffen.
Ein JUnit-Test belegt, dass das Ergebnis im Cache gelandet ist. Die Methode getCache
wurde zweimal aufgerufen. zuerst beim Hinzufügen zum Zwischenspeicher und anschließend beim erneuten Aufruf von findOne
.
@RunWith(SpringJUnit4ClassRunner.class) @SpringApplicationConfiguration(classes = { Application.class, CachingTest.Config.class }) public class CachingTest { private static final String ISBN = "1234567890123"; @Configuration static class Config { @Autowired private Application application; @Bean @Primary public CacheManager cacheManager(RedisTemplate<?, ?> template) { return Mockito.spy(application.cacheManager(template)); } } @Autowired private CacheManager cacheManager; @Autowired private BookRepository repository; @Test public void caching() throws InterruptedException { final Book book = new Book(ISBN, "Sample Book", "Waldemar Biller"); repository.save(book); // load book from DB final Book loaded = repository.findOne(ISBN); assertThat(loaded, not(nullValue())); // ensure it gets cached and the cached one // is the same as the one from the DB final Book cached = repository.findOne(ISBN); verify(cacheManager, times(2)).getCache("books"); assertThat(cached, not(nullValue())); assertThat(cached, equalTo(loaded)); Thread.sleep(5000); // ensure the book is evicted after 5 seconds final Cache cache = cacheManager.getCache("books"); assertThat(cache.get(ISBN), nullValue()); } }
Ein kurzer Blick auf die Redis-Instanz zeigt, dass das Beispiel-Buch persistiert wurde.
Der Cache Manager ist so konfiguriert, dass die Einträge nach 5 Sekunden verworfen werden. Daher sollte der Cache nach einer kurzen Pause zur gegebenen ISBN null
zurück liefern.
Achtung: Die hier gewählte Konfiguration dient nur als Beispiel. Durch die Verwendung des Jackson2JsonRedisSerializer
ist das Caching auf einen Typen beschränkt. Man sollte über den Einsatz eines allgemeineren Serializers nachdenken (bspw. JdkSerializationRedisSerializer
).
Im nächsten Beitrag widme ich mich nochmal dem Modul Spring Data Redis und gehe dabei auf die Messaging Funktionalitäten von Redis ein.