Spring Data Redis
Redisはキャッシュのスイスアーミーナイフです。5つのデータ構造 (String, Hash, List, Set, ZSet)で, キャッシュからセッション, メッセージングまで幅広くカバーします。
1. 学ぶ内容
RedisTemplateとStringRedisTemplateによるString / Hash / List / Set / ZSet操作@CacheableとRedisの統合による分散キャッシュバックエンドの実現- Redisパブリッシュ・サブスクライブ (Pub/Sub)による注文イベント通知
- Redis接続プール設定の比較:Lettuce vs. Jedis
- BobがRedisでセッションをキャッシュし, OrderFlowの複数インスタンス間でログイン状態を共有
2. クラスタアーキテクトのリアルストーリー
(1) ペインポイント:複数インスタンス間でキャッシュが共有されない
AliceがOrderFlowを単一インスタンスから3インスタンスにスケールアウトした後, Caffeineローカルキャッシュに問題が発生しました。インスタンスAが商品価格を更新してローカルキャッシュをリフレッシュしても, インスタンスBとCのローカルキャッシュには古い価格が残っており, ユーザーに表示される価格に不整合が生じました。BobはRedisでセッション共有を試みましたが, Spring CacheとRedisの統合方法がわかりませんでした。
(2) Spring Data Redisによる解決策
分散キャッシュとしてのRedisは, 全インスタンス間で以下を共有します:
@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操作
@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();
}
}
出力:
// 実行成功
4. @CacheableとRedisの統合
(1) Redisキャッシュ設定
(1) ▶ サンプル:RedisCacheManager設定
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
出力:
// 実行成功
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 |
|---|---|---|
| 接続モデル | 非同期ノンブロッキング (Netty) | 同期ブロッキング |
| スレッドセーフ | はい (単一接続を複数スレッドで共有) | いいえ (接続プールが必要) |
| パフォーマンス | 高同時シナリオで優位 | シンプルなシナリオで十分 |
| Spring Bootデフォルト | ✅ デフォルト | 手動で切り替えが必要 |
5. Redisパブリッシュ・サブスクライブ
(1) Pub/Subメッセージモデル
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) ▶ サンプル:注文作成イベントのパブリッシュ
@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);
}
}
出力:
// 実行成功
(2) ▶ サンプル:注文イベントのサブスクライブ
@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");
}
}
出力:
// 実行成功
6. Redis共有セッションとJWTブラックリスト
(1) ▶ サンプル:JWTブラックリストの実装
@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));
}
}
出力:
// 実行成功
(2) ▶ サンプル:分散レート制限
@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;
}
}
出力:
// 実行成功
| ユースケース | 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の完全統合
// 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);
}
}
# application.yml
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:}
lettuce:
pool:
max-active: 20
max-idle: 10
❓ よくある質問
RedisTemplate<String, String>を継承しており, キーも値もStringで, StringRedisSerializerを使用します。RedisTemplateはデフォルトでJDKシリアライズを使用し, 可読性が低いです。StringRedisTemplateまたはカスタムシリアライザの使用をお勧めします。StringRedisSerializerと手動JSON変換の組み合わせも可能です。CacheErrorHandlerを設定してカスタム障害処理を実装, 3)Redis SentinelまたはClusterで高可用性を確保してください。📖 まとめ
- Redisの5大データ構造:String, Hash, List, Set, ZSet
@Cacheable+ RedisCacheManagerで全インスタンス共有の分散キャッシュを実現- LettuceはSpring BootのデフォルトRedisクライアント。非同期, ノンブロッキング, スレッドセーフ
- Redis Pub/Subでイベント通知を実現するが, メッセージは信頼性がない (サブスクライバーがオフラインの場合は損失の可能性あり)
- JWTブラックリストにはRedis String + TTL, 分散レート制限にはINCR + TTLを使用
- 本番環境ではRedis SentinelまたはClusterで高可用性を確保
📝 練習問題
-
基本問題 (難易度 ⭐):OrderFlowにSpring Data Redisを設定し,
@Cacheableで商品キャッシュをRedisに保存し, 複数インスタンス間でキャッシュが共有されることを確認してください。 -
応用問題 (難易度 ⭐⭐):Redis Pub/Subの注文イベント通知を実装してください。注文作成時にORDER_CREATEDイベントをパブリッシュし, 通知サービスがサブスクライブしてメールを送信するようにしてください。JWTブラックリストのトークン無効化機能も実装してください。
-
チャレンジ (難易度 ⭐⭐⭐):L1 CaffeineとL2 Redisの2段キャッシュシステムを実装してください。Redisキャッシュが更新された際, Pub/Subで全インスタンスにローカルCaffeineキャッシュのクリアを通知し, 両段階の整合性を確保してください。



