Spring Data Redis
Redis is the Swiss Army knife of caching—with its five data structures—String, Hash, List, Set, and ZSet—it covers everything from caching to sessions to messaging.
1. What You'll Learn
RedisTemplateandStringRedisTemplateoperations on String / Hash / List / Set / ZSet@CacheableIntegrating Redis as a distributed cache backend- Redis Publish/Subscribe (Pub/Sub) for Order Event Notifications
- Comparison of Redis Connection Pool Configurations: Lettuce vs. Jedis
- Bob uses Redis to cache sessions, enabling multiple instances of OrderFlow to share login status
2. A True Story of a Cluster Architect
(1) Pain Point: Caches Across Multiple Instances Are Not Shared
After Alice scaled OrderFlow from a single instance to three instances, issues arose with the Caffeine local cache: Instance A updated the product prices and refreshed the local cache, but the local caches on Instances B and C still contained the old prices, resulting in inconsistent prices displayed to users. Bob tried using Redis to share sessions, but didn't know how to integrate Spring Cache with Redis.
(2) Solution using Spring Data Redis
As a distributed cache, Redis shares the following across all instances:
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productRepository.findById(id).orElseThrow();
}
// Cache stored in Redis, shared across all instances
(3) Revenue
After Bob replaced Caffeine with Redis as the distributed cache, the three instances maintained consistent caches, and product price updates were synchronized in real time. At the same time, Redis Pub/Sub was used to implement order event notifications, Redis was used to store the JWT blacklist, and login status was shared across multiple instances.
3. Working with Redis Data Structures
(1) Five Major Data Structures
| Type | Purpose | Typical Scenarios |
|---|---|---|
| String | Simple key-value pairs | Cache, counters, sessions |
| Hash | Object Properties | Product Details, User Information |
| List | Ordered list | Message queue, recent list |
| Set | Unordered Set | Tags, Mutual Friends |
| ZSet | Ordered Set | Leaderboards, Latency Queues |
(1) ▶ Example: Basic RedisTemplate Operations
@Service
public class RedisOperationService {
private final StringRedisTemplate redis;
public RedisOperationService(StringRedisTemplate redis) {
this.redis = redis;
}
// String operations
public void cacheProduct(Long id, String json) {
redis.opsForValue().set("product:" + id, json, Duration.ofHours(1));
}
public String getProduct(Long id) {
return redis.opsForValue().get("product:" + id);
}
// Counter
public Long incrementOrderCount() {
return redis.opsForValue().increment("stats:daily-orders");
}
// Hash operations
public void saveProductDetail(Long id, Map<String, String> details) {
redis.opsForHash().putAll("product:detail:" + id, details);
}
public Map<Object, Object> getProductDetail(Long id) {
return redis.opsForHash().entries("product:detail:" + id);
}
// List operations - Recent orders
public void addRecentOrder(Long orderId) {
redis.opsForList().leftPush("recent:orders", orderId.toString());
redis.opsForList().trim("recent:orders", 0, 99); // Keep last 100
}
public List<String> getRecentOrders() {
return redis.opsForList().range("recent:orders", 0, -1);
}
// Set operations - Product tags
public void addProductTag(Long productId, String tag) {
redis.opsForSet().add("product:tags:" + productId, tag);
}
public Set<String> getProductTags(Long productId) {
return redis.opsForSet().members("product:tags:" + productId);
}
// ZSet operations - Sales ranking
public void updateSalesRank(Long productId, double salesCount) {
redis.opsForZSet().add("sales:ranking", productId.toString(), salesCount);
}
public List<String> getTopSellingProducts(int limit) {
return redis.opsForZSet()
.reverseRange("sales:ranking", 0, limit - 1)
.stream().toList();
}
}
Output:
// Execution Successful
4. Integrating @Cacheable with Redis
(1) Redis Cache Configuration
(1) ▶ Example: RedisCacheManager Configuration
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Output:
// Execution Successful
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
"products", config.entryTtl(Duration.ofHours(1)),
"product-list", config.entryTtl(Duration.ofMinutes(10)),
"order-stats", config.entryTtl(Duration.ofMinutes(5))
);
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigs)
.transactionAware()
.build();
}
}
| Lettuce vs. Jedis | Lettuce | Jedis |
|---|---|---|
| Connection Model | Asynchronous Non-Blocking (Netty) | Synchronous Blocking |
| Thread-safe | Yes (single connection shared by multiple threads) | No (requires a connection pool) |
| Performance | Better for high-concurrency scenarios | Adequate for simple scenarios |
| Spring Boot Default | ✅ Default | Must be switched manually |
5. Redis Publish/Subscribe
(1) Pub/Sub Message Model
sequenceDiagram
participant P as Publisher<br/>(OrderService)
participant R as Redis<br/>Pub/Sub
participant S1 as Subscriber 1<br/>(NotificationService)
participant S2 as Subscriber 2<br/>(AnalyticsService)
P->>R: PUBLISH order-events "ORDER_CREATED:123"
R->>S1: Message: "ORDER_CREATED:123"
R->>S2: Message: "ORDER_CREATED:123"
(1) ▶ Example: Order Placement Event
@Service
public class OrderEventPublisher {
private final StringRedisTemplate redis;
public OrderEventPublisher(StringRedisTemplate redis) {
this.redis = redis;
}
public void publishOrderCreated(Long orderId) {
redis.convertAndSend("order-events",
"ORDER_CREATED:" + orderId);
}
public void publishOrderCancelled(Long orderId) {
redis.convertAndSend("order-events",
"ORDER_CANCELLED:" + orderId);
}
}
Output:
// Execution Successful
(2) ▶ Example: Subscribing to Order Events
@Component
public class OrderEventSubscriber {
private static final Logger log = LoggerFactory.getLogger(OrderEventSubscriber.class);
@RedisListener(topic = "order-events")
public void handleOrderEvent(String message) {
String[] parts = message.split(":");
String eventType = parts[0];
Long orderId = Long.parseLong(parts[1]);
switch (eventType) {
case "ORDER_CREATED" -> log.info("Processing order created: {}", orderId);
case "ORDER_CANCELLED" -> log.info("Processing order cancelled: {}", orderId);
}
}
}
@Configuration
public class RedisPubSubConfig {
@Bean
public RedisMessageListenerContainer container(
RedisConnectionFactory factory,
MessageListenerAdapter listenerAdapter) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener(listenerAdapter,
new ChannelTopic("order-events"));
return container;
}
@Bean
public MessageListenerAdapter listenerAdapter(OrderEventSubscriber subscriber) {
return new MessageListenerAdapter(subscriber, "handleOrderEvent");
}
}
Output:
// Execution Successful
6. Redis Shared Sessions and JWT Blacklists
(1) ▶ Example: JWT Blacklist Implementation
@Service
public class TokenBlacklistService {
private final StringRedisTemplate redis;
public TokenBlacklistService(StringRedisTemplate redis) {
this.redis = redis;
}
public void blacklistToken(String tokenId, Duration remainingTtl) {
redis.opsForValue().set(
"token:blacklist:" + tokenId, "1", remainingTtl);
}
public boolean isBlacklisted(String tokenId) {
return Boolean.TRUE.equals(
redis.hasKey("token:blacklist:" + tokenId));
}
}
Output:
// Execution Successful
(2) ▶ Example: Distributed Rate Limiting
@Service
public class RateLimitService {
private final StringRedisTemplate redis;
public RateLimitService(StringRedisTemplate redis) {
this.redis = redis;
}
public boolean isAllowed(String clientId, int maxRequests, Duration window) {
String key = "rate-limit:" + clientId;
Long count = redis.opsForValue().increment(key);
if (count != null && count == 1) {
redis.expire(key, window);
}
return count != null && count <= maxRequests;
}
}
Output:
// Execution Successful
| Use Cases | Redis Data Structures | Key Design |
|---|---|---|
| Distributed Cache | String / Hash | cache:products:123 |
| JWT Blacklist | String + TTL | token:blacklist:tokenId |
| Distributed Rate Limiting | String + INCR + TTL | rate-limit:clientId |
| Recent Orders | List + LPUSH + TRIM | recent:orders |
| Best Sellers | ZSet | sales:ranking |
| Event Notification | Pub/Sub | order-events |
7. Comprehensive Example: Complete Integration of OrderFlow and Redis
// RedisCacheConfig.java
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(defaults)
.withInitialCacheConfigurations(Map.of(
"products", defaults.entryTtl(Duration.ofHours(1)),
"product-list", defaults.entryTtl(Duration.ofMinutes(10))
))
.build();
}
}
// ProductService with Redis cache
@Service
public class ProductService {
private final ProductRepository productRepository;
public ProductService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Product", id));
}
@CachePut(value = "products", key = "#result.id")
@CacheEvict(value = "product-list", allEntries = true)
public Product updateProduct(Long id, String name, BigDecimal price) {
Product p = getProduct(id);
p.setName(name);
p.setPrice(price);
return productRepository.save(p);
}
}
// OrderEventPublisher.java
@Service
public class OrderEventPublisher {
private final StringRedisTemplate redis;
public OrderEventPublisher(StringRedisTemplate redis) { this.redis = redis; }
public void publishOrderCreated(Long orderId) {
redis.convertAndSend("order-events", "ORDER_CREATED:" + orderId);
}
}
# application.yml
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
lettuce:
pool:
max-active: 20
max-idle: 10
❓ FAQ
RedisTemplate<String, String>; both keys and values are Strings, and it uses StringRedisSerializer. RedisTemplate uses JDK serialization by default, which is less readable. We recommend using StringRedisTemplate or a custom serializer.StringRedisSerializer combined with manual JSON conversion.CacheErrorHandler to implement custom failure handling; 3) Use Redis Sentinel or Cluster to ensure high availability.📖 Summary
- The Five Major Data Structures in Redis: String, Hash, List, Set, and ZSet
@Cacheable+ RedisCacheManager to implement a distributed cache shared by all instances- Lettuce is Spring Boot's default Redis client; it is asynchronous, non-blocking, and thread-safe.
- Redis Pub/Sub implements event notification, but messages are unreliable (messages may be lost if subscribers are offline).
- Use Redis String + TTL for the JWT blacklist; use INCR + TTL for distributed rate limiting
- Use Redis Sentinel or Cluster in production environments to ensure high availability
📝 Exercises
-
Basic Exercise (Difficulty ⭐): Configure Spring Data Redis for OrderFlow, use
@Cacheableto store product cache in Redis, and verify cache sharing across multiple instances. -
Advanced Exercise (Difficulty: ⭐⭐): Implement Redis Pub/Sub order event notifications—publish the ORDER_CREATED event when an order is placed, and have the notification service subscribe to it and send an email. Implement a JWT blacklist token revocation feature.
-
Challenge (Difficulty: ⭐⭐⭐): Implement a two-tier cache system consisting of L1 Caffeine and L2 Redis. When the Redis cache is updated, use Pub/Sub to notify all instances to clear their local Caffeine caches, ensuring consistency across both tiers.



