Skip to content

Commit dea22b9

Browse files
committed
Docs
1 parent 0ccd64b commit dea22b9

File tree

4 files changed

+702
-0
lines changed

4 files changed

+702
-0
lines changed

README.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ All tutorials are documented in AsciiDoc format and published as an https://anto
5252
|link:data-jpa-event[Spring Data JPA: Event Driven] |Implement `Entity` validation at `Repository` level through `EventListeners`
5353
|link:data-jpa-filtered-query[Spring Data JPA: Global Filtered Query] |Implement global filtered query with Spring Data JPA by defining `repositoryBaseClass`
5454
|link:data-jpa-hibernate-cache[Spring Data JPA: Hibernate Second Level Caching with EhCache] |Implement Hibernate second level caching using Spring Data JPA and EhCache to improve application performance
55+
|link:data-redis-cache[Spring Data Redis: Implementing Caching with Spring Data Redis] |Implement caching with Spring Data Redis to improve application performance and compare with Hibernate second level caching
5556
|link:data-mongodb-audit[Spring Data MongoDB Audit] |Enable Audit with Spring Data MongoDB
5657
|link:data-mongodb-full-text-search[Spring Data MongoDB: Full Text Search] |Implement link:https://docs.mongodb.com/manual/text-search/[MongoDB Full Text Search] with Spring Data MongoDB
5758
|link:data-mongodb-transactional[Spring Data MongoDB: Transactional] |Enable `@Transactional` support for Spring Data MongoDB

data-redis-cache/README.adoc

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
= Spring Data Redis: Implementing Caching with Spring Data Redis
2+
:source-highlighter: highlight.js
3+
:highlightjs-languages: java, groovy
4+
Rashidi Zin <rashidi@zin.my>
5+
1.0, July 20, 2025: Initial version
6+
:toc:
7+
:nofooter:
8+
:icons: font
9+
:url-quickref: https://github.com/rashidi/spring-boot-tutorials/tree/master/data-redis-cache
10+
11+
In this tutorial, we will explore how to implement caching with Spring Data Redis to improve application performance. We will also compare this approach with Hibernate second level caching.
12+
13+
== Background
14+
15+
Caching is a technique used to store frequently accessed data in memory to reduce database load and improve application performance. Spring provides a caching abstraction that allows you to use different caching providers with minimal configuration changes. In this tutorial, we will focus on using Redis as the caching provider.
16+
17+
Redis is an in-memory data structure store that can be used as a database, cache, and message broker. It supports various data structures such as strings, hashes, lists, sets, and more. Redis is particularly well-suited for caching due to its high performance and support for data expiration.
18+
19+
== Implementation
20+
21+
=== Dependencies
22+
23+
To implement caching with Spring Data Redis, we need to add the following dependencies to our project:
24+
25+
[source, groovy]
26+
----
27+
dependencies {
28+
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
29+
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
30+
runtimeOnly 'org.postgresql:postgresql'
31+
// Test dependencies omitted for brevity
32+
}
33+
----
34+
35+
=== Entity Class
36+
37+
Let's start by defining our entity class. In this example, we'll use a simple `Customer` entity:
38+
39+
[source, java]
40+
----
41+
@Entity
42+
class Customer implements Serializable {
43+
44+
@Id
45+
@GeneratedValue
46+
private Long id;
47+
48+
private String name;
49+
50+
@Override
51+
public final boolean equals(Object o) {
52+
return o instanceof Customer another && this.id.equals(another.id);
53+
}
54+
55+
@Override
56+
public int hashCode() {
57+
return id.hashCode();
58+
}
59+
}
60+
----
61+
62+
Note that the entity implements `Serializable`, which is required for Redis caching as objects need to be serialized to be stored in Redis. The class also overrides `equals` and `hashCode` methods based on the `id` field, which is important for proper object comparison when retrieving from cache.
63+
64+
=== Repository Interface
65+
66+
Next, we'll define our repository interface with caching annotations:
67+
68+
[source, java]
69+
----
70+
interface CustomerRepository extends JpaRepository<Customer, Long> {
71+
72+
@Override
73+
@Cacheable(cacheNames = "customers", key = "#root.methodName")
74+
List<Customer> findAll();
75+
76+
@Override
77+
@Cacheable(cacheNames = "customerById")
78+
Optional<Customer> findById(Long id);
79+
}
80+
----
81+
82+
We've annotated the `findAll()` and `findById()` methods with `@Cacheable` to enable caching for these methods:
83+
84+
- `findAll()` is cached with the name "customers" and the key is the method name.
85+
- `findById()` is cached with the name "customerById" and the default key is the method parameter (id).
86+
87+
The `@Cacheable` annotation means that the results of these methods will be cached, and subsequent calls with the same parameters will retrieve the results from the cache instead of executing the method again.
88+
89+
=== Cache Configuration
90+
91+
To configure Redis as the cache provider, we need to create a configuration class:
92+
93+
[source, java]
94+
----
95+
@Configuration
96+
@EnableCaching
97+
class CacheConfiguration {
98+
99+
@Bean
100+
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
101+
return RedisCacheManager.create(connectionFactory);
102+
}
103+
}
104+
----
105+
106+
This class is responsible for configuring Redis as the cache provider for Spring's caching mechanism:
107+
108+
1. `@Configuration` annotation indicates that this class provides Spring configuration.
109+
2. `@EnableCaching` annotation enables Spring's caching mechanism.
110+
3. A bean method creates a `RedisCacheManager` using a `RedisConnectionFactory`.
111+
112+
The `RedisCacheManager` is created using the factory method `create()` with the `RedisConnectionFactory` as a parameter. This is a simple configuration that uses default settings for the Redis cache.
113+
114+
== Testing the Implementation
115+
116+
To test our caching implementation, we'll use Spring Boot's testing support along with Testcontainers to spin up Redis and PostgreSQL containers for integration testing.
117+
118+
=== Testcontainers Configuration
119+
120+
First, let's set up the Testcontainers configuration:
121+
122+
[source, java]
123+
----
124+
@TestConfiguration(proxyBeanMethods = false)
125+
public class TestcontainersConfiguration {
126+
127+
@Bean
128+
@ServiceConnection
129+
PostgreSQLContainer<?> postgresContainer() {
130+
return new PostgreSQLContainer<>(DockerImageName.parse("postgres:latest"));
131+
}
132+
133+
@Bean
134+
@ServiceConnection(name = "redis")
135+
RedisContainer redisContainer() {
136+
return new RedisContainer(DockerImageName.parse("redis:latest"));
137+
}
138+
}
139+
----
140+
141+
This class defines two beans:
142+
- A `PostgreSQLContainer` bean annotated with `@ServiceConnection`, which will be used for the database.
143+
- A `RedisContainer` bean annotated with `@ServiceConnection(name = "redis")`, which will be used for Redis caching.
144+
145+
The `@ServiceConnection` annotation is a Spring Boot feature that automatically configures the application to connect to these containers.
146+
147+
=== Repository Tests
148+
149+
Now, let's write tests to verify that our caching implementation works correctly:
150+
151+
[source, java]
152+
----
153+
@Import(TestcontainersConfiguration.class)
154+
@ImportAutoConfiguration({ RedisAutoConfiguration.class, CacheAutoConfiguration.class })
155+
@Sql(executionPhase = BEFORE_TEST_CLASS, statements = "INSERT INTO customer (id, name) VALUES (1, 'Rashidi Zin')")
156+
@DataJpaTest(properties = "spring.jpa.hibernate.ddl-auto=create-drop", includeFilters = @Filter(EnableCaching.class))
157+
class CustomerRepositoryTests {
158+
159+
@Autowired
160+
private CustomerRepository customers;
161+
162+
@Autowired
163+
private CacheManager caches;
164+
165+
@Test
166+
@Transactional(readOnly = true)
167+
@DisplayName("Given the method name is configured as the cache's key Then subsequent retrieval should return the same value as initial retrieval")
168+
void findAll() {
169+
var persisted = customers.findAll();
170+
var cached = caches.getCache("customers").get("findAll").get();
171+
172+
assertThat(cached).isEqualTo(persisted);
173+
}
174+
175+
@Test
176+
@Transactional(readOnly = true)
177+
@DisplayName("Given the cache is configured Then subsequent retrieval with the same key should return the same value as initial retrieval")
178+
void findById() {
179+
var persisted = customers.findById(1L).get();
180+
var cached = caches.getCache("customerById").get(1L).get();
181+
182+
assertThat(cached).isEqualTo(persisted);
183+
}
184+
}
185+
----
186+
187+
These tests verify that our caching implementation works correctly:
188+
189+
1. The `findAll()` test:
190+
- Calls the repository method to retrieve all customers.
191+
- Retrieves the cached value directly from the cache manager.
192+
- Asserts that the cached value is equal to the value returned by the repository method.
193+
194+
2. The `findById()` test:
195+
- Calls the repository method to retrieve a customer by ID.
196+
- Retrieves the cached value directly from the cache manager.
197+
- Asserts that the cached value is equal to the value returned by the repository method.
198+
199+
== Comparison with Hibernate Second Level Caching
200+
201+
Hibernate second level caching is another approach to caching in Spring applications. Let's compare it with Spring Data Redis caching.
202+
203+
=== Hibernate Second Level Caching Implementation
204+
205+
In Hibernate second level caching, caching is configured at the entity level rather than at the repository level. Here's how it's implemented:
206+
207+
==== Entity Configuration
208+
209+
[source, java]
210+
----
211+
@Entity
212+
@Cache(usage = READ_WRITE, region = "customer")
213+
class Customer {
214+
215+
@Id
216+
@GeneratedValue
217+
private Long id;
218+
219+
private String name;
220+
}
221+
----
222+
223+
The entity is annotated with `@Cache(usage = READ_WRITE, region = "customer")`, which:
224+
- Enables Hibernate second level caching for this entity.
225+
- Sets the cache concurrency strategy to READ_WRITE, which is suitable for entities that are occasionally updated.
226+
- Defines a cache region named "customer" for this entity.
227+
228+
==== Application Properties
229+
230+
[source, properties]
231+
----
232+
spring.jpa.properties.hibernate.cache.region.factory_class=jcache
233+
spring.jpa.properties.hibernate.cache.jcache.uri=/ehcache.xml
234+
spring.jpa.properties.hibernate.cache.jcache.provider=org.ehcache.jsr107.EhcacheCachingProvider
235+
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
236+
----
237+
238+
These properties configure Hibernate to use JCache (JSR-107) with EhCache as the provider.
239+
240+
==== EhCache Configuration
241+
242+
[source, xml]
243+
----
244+
<config xmlns='http://www.ehcache.org/v3'>
245+
<cache alias="customer">
246+
<resources>
247+
<offheap unit="MB">10</offheap>
248+
</resources>
249+
</cache>
250+
</config>
251+
----
252+
253+
This configuration defines a cache named "customer" with 10MB of off-heap memory.
254+
255+
==== Testing Hibernate Second Level Caching
256+
257+
[source, java]
258+
----
259+
@DataJpaTest(properties = {
260+
"spring.jpa.hibernate.ddl-auto=create-drop",
261+
"spring.jpa.properties.hibernate.generate_statistics=true"
262+
})
263+
@Import(TestcontainersConfiguration.class)
264+
@Sql(statements = "INSERT INTO customer (id, name) VALUES (1, 'Rashidi Zin')", executionPhase = BEFORE_TEST_CLASS)
265+
@TestMethodOrder(OrderAnnotation.class)
266+
class CustomerRepositoryTests {
267+
268+
@Autowired
269+
private CustomerRepository customers;
270+
271+
private Statistics statistics;
272+
273+
@BeforeEach
274+
void setupStatistics(@Autowired EntityManagerFactory entityManagerFactory) {
275+
statistics = entityManagerFactory.unwrap(SessionFactory.class).getStatistics();
276+
}
277+
278+
@Test
279+
@Order(1)
280+
@Transactional(propagation = REQUIRES_NEW)
281+
@DisplayName("On initial retrieval data will be retrieved from the database and customer cache will be stored")
282+
void initial() {
283+
customers.findById(1L).orElseThrow();
284+
285+
assertThat(statistics.getSecondLevelCachePutCount()).isEqualTo(1);
286+
assertThat(statistics.getSecondLevelCacheHitCount()).isZero();
287+
}
288+
289+
@Test
290+
@Order(2)
291+
@Transactional(propagation = REQUIRES_NEW)
292+
@DisplayName("On subsequent retrieval data will be retrieved from the customer cache")
293+
void subsequent() {
294+
customers.findById(1L).orElseThrow();
295+
296+
assertThat(statistics.getSecondLevelCacheHitCount()).isEqualTo(1);
297+
}
298+
}
299+
----
300+
301+
These tests use Hibernate's Statistics API to verify cache hits and misses.
302+
303+
=== Comparison
304+
305+
|===
306+
|Feature |Spring Data Redis Caching |Hibernate Second Level Caching
307+
308+
|Configuration Level
309+
|Repository level
310+
|Entity level
311+
312+
|Cache Provider
313+
|Redis
314+
|Various providers (EhCache in our example)
315+
316+
|Cache Granularity
317+
|Method level
318+
|Entity level
319+
320+
|Cache Control
321+
|Fine-grained control with SpEL expressions for keys
322+
|Limited control based on entity and region
323+
324+
|Distributed Caching
325+
|Yes, Redis is a distributed cache
326+
|Depends on the provider (EhCache can be distributed)
327+
328+
|Integration with Spring
329+
|Seamless integration with Spring's caching abstraction
330+
|Requires additional configuration
331+
332+
|Performance
333+
|High performance for all data types
334+
|Optimized for entity caching
335+
336+
|Use Cases
337+
|General-purpose caching, method results caching
338+
|Entity caching in JPA applications
339+
|===
340+
341+
== Conclusion
342+
343+
In this tutorial, we've explored how to implement caching with Spring Data Redis to improve application performance. We've also compared this approach with Hibernate second level caching.
344+
345+
Spring Data Redis caching is a flexible and powerful approach that allows you to cache method results at the repository level. It's particularly useful when you need fine-grained control over what gets cached and how keys are generated.
346+
347+
Hibernate second level caching, on the other hand, is more focused on entity caching and is tightly integrated with JPA. It's a good choice when you're primarily working with JPA entities and want to reduce database load.
348+
349+
Both approaches have their strengths and are suitable for different use cases. The choice between them depends on your specific requirements and the nature of your application.
350+
351+
The full implementation can be found in {url-quickref}[Github].

docs/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
** xref:data-mongodb-audit.adoc[MongoDB Audit]
1717
** xref:data-mongodb-full-text-search.adoc[MongoDB Full Text Search]
1818
** xref:data-mongodb-transactional.adoc[MongoDB Transactional]
19+
** xref:data-redis-cache.adoc[Implement Caching with Spring Data Redis]
1920
** xref:data-repository-definition.adoc[Repository Definition]
2021
** xref:data-rest-composite-id.adoc[Composite Id with Spring Data REST]
2122
** xref:data-rest-validation.adoc[REST Validation]

0 commit comments

Comments
 (0)