mcpField
S
spring-data-redis

spring-data-redis

github.com/rrezartprebreza开发328109 次安装6 天前更新
代码仓库

关于 spring-data-redis

面向 Java 开发者的 Spring Data Redis 参考技能,涵盖缓存配置、常见模式、限流及实现中的易错点。

主要功能

  1. 01Redis 与 Spring 缓存配置
  2. 02统一 Redis 键命名
  3. 03声明式与旁路缓存模式
  4. 04原子化 Redis 限流
  5. 05TTL 与缓存击穿处理

适用场景

  1. 01为 Spring 应用配置 Redis 缓存
  2. 02为订单或产品实现旁路读取
  3. 03实现不会遗留无过期时间键的限流
  4. 04检查缓存序列化与事务问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/rrezartprebreza/spring-boot-skills --skill spring-data-redis

安装后在 Claude Code 中输入/spring-data-redis确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

agents/
openai.yaml
examples/
bad-cache-service.java
good-cache-service.java
templates/
RedisConfig.java

详细介绍

Spring Data Redis

依赖项

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

配置

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(jsonSerializer()); // JSON, not Java serialization
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(jsonSerializer());
        return template;
    }

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jsonSerializer()))
            .disableCachingNullValues();

        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .transactionAware()   // put/evict only after the surrounding transaction commits
            .withCacheConfiguration("orders", config.entryTtl(Duration.ofMinutes(5)))
            .withCacheConfiguration("products", config.entryTtl(Duration.ofHours(1)))
            .build();
    }

    // Records are final, so DefaultTyping.NON_FINAL writes no type id for them and every
    // cache read fails ("missing type id property '@class'"). The builder's defaultTyping(true)
    // types user classes too; the validator limits type ids to an allow-list instead of
    // LaissezFaire's "any class". BigDecimal is non-final, so java.math must be allowed.
    private GenericJackson2JsonRedisSerializer jsonSerializer() {
        ObjectMapper mapper = JsonMapper.builder()
            .addModule(new JavaTimeModule())
            .polymorphicTypeValidator(BasicPolymorphicTypeValidator.builder()
                .allowIfSubType("com.example.")   // your DTO packages
                .allowIfSubType("java.util.")
                .allowIfSubType("java.math.")
                .allowIfSubType("java.time.")
                .build())
            .build();
        return GenericJackson2JsonRedisSerializer.builder()
            .objectMapper(mapper)
            .defaultTyping(true)
            .build();
    }
}

键命名约定

{app}:{domain}:{id}          → orders:order:uuid-here
{app}:{domain}:list:{filter} → orders:order:list:status:PENDING
{app}:session:{userId}       → orders:session:uuid-here
{app}:ratelimit:{ip}         → orders:ratelimit:192.168.1.1

@Cacheable — 声明式缓存

@Service
@RequiredArgsConstructor
public class ProductService {

    @Cacheable(value = "products", key = "#id")
    public ProductResponse findById(UUID id) {
        return productRepository.findById(id)
            .map(ProductResponse::from)
            .orElseThrow(() -> new EntityNotFoundException("Product not found: " + id));
    }

    @CachePut(value = "products", key = "#result.id")  // update cache after write
    @Transactional
    public ProductResponse update(UUID id, UpdateProductRequest request) {
        Product product = productRepository.findById(id).orElseThrow();
        product.update(request);
        return ProductResponse.from(productRepository.save(product));
    }

    @CacheEvict(value = "products", key = "#id")  // invalidate on delete
    @Transactional
    public void delete(UUID id) {
        productRepository.deleteById(id);
    }

    @CacheEvict(value = "products", allEntries = true)  // clear all
    public void clearCache() {}
}

手动旁路缓存模式

@Service
@RequiredArgsConstructor
public class OrderCacheService {

    private final RedisTemplate<String, Object> redisTemplate;
    private final ObjectMapper objectMapper;
    private static final Duration TTL = Duration.ofMinutes(5);

    public Optional<OrderResponse> get(UUID orderId) {
        String key = "orders:order:" + orderId;
        Object cached = redisTemplate.opsForValue().get(key);
        if (cached == null) return Optional.empty();
        return Optional.of(objectMapper.convertValue(cached, OrderResponse.class));
    }

    public void put(OrderResponse order) {
        String key = "orders:order:" + order.id();
        redisTemplate.opsForValue().set(key, order, TTL);
    }

    public void evict(UUID orderId) {
        redisTemplate.delete("orders:order:" + orderId);
    }
}

使用 Redis 限流

@Component
@RequiredArgsConstructor
public class RateLimiter {

    private final RedisTemplate<String, String> redisTemplate;

    // INCR and PEXPIRE in ONE atomic script: as two separate commands, a crash or
    // timeout between them leaves a key with no TTL and locks the caller out forever.
    private static final RedisScript<Long> INCR_WITH_TTL = RedisScript.of(
        "local c = redis.call('INCR', KEYS[1]) " +
        "if c == 1 then redis.call('PEXPIRE', KEYS[1], ARGV[1]) end " +
        "return c", Long.class);

    public boolean isAllowed(String identifier, int maxRequests, Duration window) {
        String key = "ratelimit:" + identifier;
        Long count = redisTemplate.execute(INCR_WITH_TTL, List.of(key), String.valueOf(window.toMillis()));
        return count != null && count <= maxRequests;
    }
}

application.yml

spring:
  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: ${REDIS_PORT:6379}
      password: ${REDIS_PASSWORD:}
      timeout: 2000ms
      lettuce:
        pool:
          max-active: 10
          max-idle: 5
          min-idle: 2
  cache:
    type: redis

缓存击穿

热点键过期时,所有并发请求会同时未命中,并一起冲击数据库以重新计算相同的值(即“惊群效应”)。对于计算开销大、流量高的负载,可以让一个调用方负责计算,其余调用方等待:

// sync = true — only one thread computes the value; others block on it
@Cacheable(value = "products", key = "#id", sync = true)
public ProductResponse findById(UUID id) { ... }

sync = true 会在单个实例内按键串行化重新计算。若要确保整个服务集群范围内的行为,请在重新计算时加上短时 Redis 锁(使用带 TTL 的 SETNX)。同时为 TTL 增加随机抖动,避免一批同时写入的键在同一秒过期。

注意事项

  • Agent 在非事务感知的缓存管理器上同时使用 @Transactional 和 @CachePut/@CacheEvict——缓存可能会在事务提交前更新(或先被清除,随后又填入过期数据);创建缓存管理器时使用 .transactionAware()
  • Agent 将限流器实现为单独的 INCR 和 EXPIRE 调用——如果两者之间发生故障,会留下一个没有 TTL 的键,导致调用方永远被阻止;使用一个 Lua 脚本
  • Agent 对默认类型处理仅允许 java.util.——BigDecimal 不是 final 类型,且带有类型 ID,因此缓存的价格/总计无法反序列化;还要允许 java.math.(以及 java.time.)
  • Agent 使用 Java 序列化处理值——始终使用 JSON(GenericJackson2JsonRedisSerializer)
  • Agent 使用 new GenericJackson2JsonRedisSerializer()——其默认 mapper 未包含 JSR-310 模块,因此任何 Instant/LocalDate 字段都无法序列化;创建时加入 JavaTimeModule
  • Agent 缓存带有 JPA 延迟加载字段的实体——应缓存 DTO/响应对象,而非实体
  • Agent 不设置 TTL——始终设置过期时间,内存并非无限
  • Agent 忘记了 @EnableCaching——没有它,@Cacheable 会静默失效
  • Agent 缓存 null 值——使用 .disableCachingNullValues(),避免存储未命中结果
  • Agent 未保护热点键——使用 @Cacheable(sync = true),防止键过期时发生缓存击穿
  • Agent 为每个条目设置相同的 TTL——加入随机抖动,避免键同步过期

评论

支持 Markdown