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. 01Spring アプリの 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

キャッシュスタンピード

ホットキーの有効期限が切れると、同時に発生したすべてのリクエストが一斉にキャッシュミスとなり、同じ値を再計算するためにデータベースへ集中します(「サンダリングハード」)。負荷の高い処理やトラフィックの多い環境では、1 つの呼び出し元が計算し、残りは待機するようにします。

// 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() を使うと、そのプレーンなマッパーには JSR-310 モジュールがないため、Instant/LocalDate フィールドのシリアライズに失敗します。JavaTimeModule を使って構築してください
  • Agent が JPA の遅延フィールドを持つエンティティをキャッシュする場合は、エンティティではなく DTO/レスポンスオブジェクトをキャッシュしてください
  • Agent が TTL を設定しない場合、必ず有効期限を設定してください。メモリは無限ではありません
  • Agent が @EnableCaching を忘れると、@Cacheable は何もせずに動作します
  • Agent が null 値をキャッシュする場合は、キャッシュミスの保存を避けるために .disableCachingNullValues() を使用してください
  • Agent がホットキーを保護しない場合は、有効期限切れ時のスタンピードを防ぐために @Cacheable(sync = true) を使用してください
  • Agent がすべてのエントリに同じ TTL を設定する場合は、キーが同期した波のように有効期限切れにならないよう、ジッターを加えてください

コメント