404 Not Found

404 Not Found


nginx

Spring Data Redis

Redisはキャッシュのスイスアーミーナイフです。5つのデータ構造 (String, Hash, List, Set, ZSet)で, キャッシュからセッション, メッセージングまで幅広くカバーします。

1. 学ぶ内容


2. クラスタアーキテクトのリアルストーリー

(1) ペインポイント:複数インスタンス間でキャッシュが共有されない

AliceがOrderFlowを単一インスタンスから3インスタンスにスケールアウトした後, Caffeineローカルキャッシュに問題が発生しました。インスタンスAが商品価格を更新してローカルキャッシュをリフレッシュしても, インスタンスBとCのローカルキャッシュには古い価格が残っており, ユーザーに表示される価格に不整合が生じました。BobはRedisでセッション共有を試みましたが, Spring CacheとRedisの統合方法がわかりませんでした。

(2) Spring Data Redisによる解決策

分散キャッシュとしてのRedisは, 全インスタンス間で以下を共有します:

JAVA
@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
    return productRepository.findById(id).orElseThrow();
}
// キャッシュはRedisに保存され, 全インスタンスで共有される

(3) 成果

BobがCaffeineをRedisに置き換えて分散キャッシュにした後, 3インスタンス間でキャッシュの整合性が保たれ, 商品価格の更新がリアルタイムで同期されるようになりました。同時に, Redis Pub/Subで注文イベント通知を実装し, JWTブラックリストの保存にRedisを使用し, 複数インスタンス間でログイン状態を共有できるようになりました。


3. Redisデータ構造の操作

(1) 5大データ構造

用途 典型的なシナリオ
String シンプルなキー・バリュー キャッシュ, カウンター, セッション
Hash オブジェクトプロパティ 商品詳細, ユーザー情報
List 順序付きリスト メッセージキュー, 最近リスト
Set 順序なしセット タグ, 共通の友人
ZSet 順序付きセット ランキング, 遅延キュー

(1) ▶ サンプル:基本的なRedisTemplate操作

JAVA
@Service
public class RedisOperationService {

    private final StringRedisTemplate redis;

    public RedisOperationService(StringRedisTemplate redis) {
        this.redis = redis;
    }

    // String操作
    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);
    }

    // カウンター
    public Long incrementOrderCount() {
        return redis.opsForValue().increment("stats:daily-orders");
    }

    // Hash操作
    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操作 - 最近の注文
    public void addRecentOrder(Long orderId) {
        redis.opsForList().leftPush("recent:orders", orderId.toString());
        redis.opsForList().trim("recent:orders", 0, 99);  // 最新100件を保持
    }

    public List<String> getRecentOrders() {
        return redis.opsForList().range("recent:orders", 0, -1);
    }

    // Set操作 - 商品タグ
    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操作 - 売上ランキング
    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キャッシュ設定

(1) ▶ サンプル: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"

(1) ▶ サンプル:注文作成イベントのパブリッシュ

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
// 実行成功

(2) ▶ サンプル:注文イベントのサブスクライブ

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共有セッションとJWTブラックリスト

(1) ▶ サンプル: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
// 実行成功

(2) ▶ サンプル:分散レート制限

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データ構造 キー設計
分散キャッシュ 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();
    }
}

// Redisキャッシュ付きProductService
@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>を継承しており, キーも値もStringで, StringRedisSerializerを使用します。RedisTemplateはデフォルトでJDKシリアライズを使用し, 可読性が低いです。StringRedisTemplateまたはカスタムシリアライザの使用をお勧めします。
Q RedisキャッシュとCaffeineキャッシュはどちらを選ぶべきですか?
A 単一インスタンスならCaffeine (最速), 複数インスタンスならRedis (共有)。ベストプラクティス:CaffeineをL1, RedisをL2とする2段キャッシュシステムで, ローカルキャッシュは短期 (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ブラックリストのトークン無効化機能も実装してください。

  3. チャレンジ (難易度 ⭐⭐⭐):L1 CaffeineとL2 Redisの2段キャッシュシステムを実装してください。Redisキャッシュが更新された際, Pub/Subで全インスタンスにローカルCaffeineキャッシュのクリアを通知し, 両段階の整合性を確保してください。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%