Spring Boot: Spring Data Redis

最后更新:2026-08-26

Redis 是缓存的瑞士军刀——String、Hash、List、Set、ZSet 五大数据结构,缓存、会话、消息一网打尽。

1. 你将学到


2. 一个集群架构师的真实故事

(1) 痛点:多实例缓存不共享

Alice 将 OrderFlow 从单实例扩展到 3 个实例后,Caffeine 本地缓存出了问题:实例 A 更新了商品价格并更新本地缓存,实例 B 和 C 的本地缓存还是旧价格,用户看到的价格不一致。Bob 尝试用 Redis 共享 Session,但不知道怎么集成 Spring Cache 和 Redis。

(2) Spring Data Redis 的解法

Redis 作为分布式缓存,所有实例共享:

JAVA
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
    return productRepository.findById(id).orElseThrow();
}
// Cache stored in Redis, shared across all instances

(3) 收益

Bob 用 Redis 替换 Caffeine 作为分布式缓存后,3 个实例缓存一致,商品价格更新实时同步。同时用 Redis Pub/Sub 实现订单事件通知,用 Redis 存储 JWT 黑名单,多实例共享登录状态。


3. Redis 数据结构操作

(1) 五大数据结构

类型 用途 典型场景
String 简单键值对 缓存、计数器、Session
Hash 对象属性 商品详情、用户信息
List 有序列表 消息队列、最新列表
Set 无序集合 标签、共同好友
ZSet 有序集合 排行榜、延迟队列

▶ 示例: RedisTemplate 基本操作

JAVA
@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();
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

4. @Cacheable 集成 Redis

(1) Redis 缓存配置

▶ 示例: RedisCacheManager 配置

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

输出:

TEXT 📖 仅展示
// 执行成功
YAML
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
JAVA
@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
连接模型 异步非阻塞(Netty) 同步阻塞
线程安全 是(单连接多线程共享) 否(需要连接池)
性能 高并发场景更优 简单场景够用
Spring Boot 默认 ✅ 默认 需手动切换

5. Redis 发布/订阅

(1) Pub/Sub 消息模型

100%
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"

▶ 示例: 发布订单事件

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);
    }

    public void publishOrderCancelled(Long orderId) {
        redis.convertAndSend("order-events",
            "ORDER_CANCELLED:" + orderId);
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例: 订阅订单事件

JAVA
@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");
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

6. Redis 共享 Session 与 JWT 黑名单

▶ 示例: JWT 黑名单实现

JAVA
@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));
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

▶ 示例: 分布式限流

JAVA
@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;
    }
}

输出:

TEXT 📖 仅展示
// 执行成功
应用场景 Redis 数据结构 Key 设计
分布式缓存 String / Hash cache:products:123
JWT 黑名单 String + TTL token:blacklist:tokenId
分布式限流 String + INCR + TTL rate-limit:clientId
最近订单 List + LPUSH + TRIM recent:orders
销售排行 ZSet sales:ranking
事件通知 Pub/Sub order-events

7. 综合示例:OrderFlow Redis 完整集成

JAVA
// 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);
    }
}
YAML
# application.yml
spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      lettuce:
        pool:
          max-active: 20
          max-idle: 10

❓ 常见问题

Q RedisTemplate 和 StringRedisTemplate 有什么区别?
A StringRedisTemplate 继承 RedisTemplate<String, String>,key 和 value 都是 String,使用 StringRedisSerializer。RedisTemplate 默认使用 JDK 序列化,可读性差。推荐使用 StringRedisTemplate 或自定义序列化器。
Q Redis 缓存和 Caffeine 缓存如何选择?
A 单实例用 Caffeine(最快),多实例用 Redis(共享)。最佳方案:L1 Caffeine + L2 Redis 两级缓存,本地缓存短期(1-5 分钟),Redis 缓存长期(30-60 分钟)。
Q Redis Pub/Sub 消息会丢失吗?
A 会。Pub/Sub 是"发后即忘",订阅者不在线时消息丢失。如果需要可靠消息,使用 Redis Streams 或专业消息队列(RabbitMQ/Kafka)。
Q Lettuce 连接池需要配置吗?
A Lettuce 基于单个连接多路复用,通常不需要连接池。但在高并发阻塞操作场景下,配置连接池可以提升吞吐量。Jedis 必须配置连接池。
Q Redis 缓存的序列化方式怎么选?
A 推荐 JSON 序列化(GenericJackson2JsonRedisSerializer),可读性好,跨语言兼容。JDK 序列化不可读,不推荐。也可以用 StringRedisSerializer + 手动 JSON 转换。
Q 如何处理 Redis 连接失败?
A 1)Spring Cache 失败时默认不抛异常(回退到数据库查询);2)配置 CacheErrorHandler 自定义失败处理;3)Redis Sentinel/Cluster 保证高可用。

📖 小节


📝 作业

  1. 基础题(难度⭐):为 OrderFlow 配置 Spring Data Redis,使用 @Cacheable 将商品缓存存储到 Redis,验证多实例缓存共享。

  2. 进阶题(难度⭐⭐):实现 Redis Pub/Sub 订单事件通知——下单时发布 ORDER_CREATED 事件,通知服务订阅并发送邮件。实现 JWT 黑名单 Token 注销功能。

  3. 挑战题(难度⭐⭐⭐):实现 L1 Caffeine + L2 Redis 两级缓存,当 Redis 缓存更新时通过 Pub/Sub 通知所有实例清除本地 Caffeine 缓存,保证两级缓存一致性。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏