使用 Redis 与 TTL 实现分布式缓存
使用 Redis 作为集中式缓存后端,在控制序列化的同时跨实例共享缓存状态。
使用 Redis 与 TTL 实现分布式缓存 是 CoddyKit 上的免费 Spring Boot 4 Complete Guide 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Spring Boot 4 Complete Guide 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Spring Boot 4 Complete Guide 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
Why Distributed Caching?
Caffeine is a brilliant in-process cache: ultra-fast, but each application instance keeps its own private copy. The moment you scale horizontally to several pods, those copies drift apart.
- Instance A evicts a user; instance B still serves the stale entry.
- A cache warm-up on one node helps nobody else.
- Total memory cost grows linearly with replica count.
A distributed cache solves this by putting cache state in a shared backend that every instance reads and writes. In Spring Boot, Redis is the most common choice for this role.
Adding the Redis Cache Starter
Spring Boot's caching abstraction is backend-agnostic. To swap Caffeine for Redis you mostly change dependencies and a property — your @Cacheable annotations stay the same.
Pull in the Redis starter and the cache abstraction:
spring-boot-starter-data-redisprovides the connection andRedisCacheManager.spring-boot-starter-cacheenables the@Cacheable/@CacheEvictannotations.
Then declare Redis as the cache type so Boot auto-configures a RedisCacheManager instead of a simple map.
<!-- pom.xml -->
<dependencies>
<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>
</dependencies>Configuring the Connection and Cache Type
Point Boot at your Redis server and tell the cache abstraction to use Redis. With spring.cache.type=redis, Boot wires a RedisCacheManager automatically.
spring.data.redis.host/portconfigure the Lettuce client (the default driver).spring.cache.redis.time-to-livesets a default TTL for every entry.spring.cache.cache-namescan pre-declare caches at startup.
# application.yml
spring:
data:
redis:
host: localhost
port: 6379
cache:
type: redis
cache-names: products, users
redis:
time-to-live: 10m
cache-null-values: false
use-key-prefix: trueThe Same @Cacheable, a Shared Backend
This is the payoff of Spring's abstraction: the service code is identical to the Caffeine version. Only the CacheManager behind it changed.
Now when instance A populates products::42, instance B reads the very same key from Redis on its next call — no duplicate computation, no drift.
@Service
public class ProductService {
private final ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
@Cacheable(cacheNames = "products", key = "#id")
public Product findById(Long id) {
// Runs only on a cache miss across the whole cluster
return repository.findById(id)
.orElseThrow(() -> new ProductNotFoundException(id));
}
@CacheEvict(cacheNames = "products", key = "#product.id")
public Product update(Product product) {
return repository.save(product);
}
}TTLs: Bounding Staleness
A TTL (time-to-live) is the maximum age of a cache entry before Redis evicts it automatically. TTLs are the primary defense against serving stale data in a distributed cache.
- Short TTL (seconds) → fresher data, more backend load.
- Long TTL (hours) → cheaper, but staleness risk grows.
- TTL is enforced server-side by Redis, so it applies uniformly to every instance.
Unlike Caffeine's expireAfterWrite, Redis TTLs survive a single instance restart because the data lives outside the JVM.
Per-Cache TTLs with a Custom RedisCacheManager
A single global TTL rarely fits every cache. Override the auto-configuration to give each cache its own expiry by supplying a RedisCacheManagerBuilderCustomizer (or a full RedisCacheManager bean).
Here products tolerates 30 minutes of staleness while volatile prices expires after 1 minute.
@Configuration
public class CacheConfig {
@Bean
public RedisCacheManagerBuilderCustomizer cacheCustomizer() {
return builder -> builder
.withCacheConfiguration("products",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)))
.withCacheConfiguration("prices",
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(1)));
}
}Serialization: How Values Reach Redis
Redis stores bytes, not Java objects. Every cached value must be serialized on write and deserialized on read. The default RedisCacheManager uses Java's native serialization (JdkSerializationRedisSerializer), which has real drawbacks:
- Values are opaque binary blobs — unreadable with
redis-cli. - The cached class must implement
Serializable. - Tight coupling to class internals breaks across versions.
For interoperable, human-readable entries, switch the value serializer to JSON.
Controlling Serialization with JSON
Use GenericJackson2JsonRedisSerializer for values and a plain StringRedisSerializer for keys. JSON keeps entries inspectable and decouples them from Java class internals.
The serializer embeds type metadata (@class) so polymorphic values deserialize back to the correct concrete type.
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.disableCachingNullValues()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()));
}Key Prefixes Prevent Collisions
When several caches share one Redis database, their keys must not collide. By default Spring prefixes every entry with the cache name, so products::42 and users::42 stay separate.
use-key-prefix: true(default) keeps caches isolated.- You can supply a custom prefix, e.g. an app or tenant name, to share a Redis instance safely across services.
This matters most in multi-tenant or shared-infrastructure setups where one Redis serves many apps.
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.computePrefixWith(cacheName -> "shop:" + cacheName + "::");
// key becomes shop:products::42
}Avoiding the Thundering Herd
A distributed cache concentrates risk: when a hot key's TTL expires, every instance misses at once and stampedes the database — the thundering herd problem.
Mitigations:
- Stagger TTLs by adding a small random jitter so keys don't expire together.
- Refresh entries proactively before expiry rather than lazily on miss.
- Use a short-lived lock so only one instance recomputes a missed key while others wait.
This pure-Java helper shows how to compute a jittered TTL you would feed into entryTtl(...).
import java.time.Duration;
import java.util.concurrent.ThreadLocalRandom;
public class TtlJitter {
static Duration withJitter(Duration base, double jitterFraction) {
long baseMs = base.toMillis();
long spread = (long) (baseMs * jitterFraction);
long offset = ThreadLocalRandom.current().nextLong(-spread, spread + 1);
return Duration.ofMillis(baseMs + offset);
}
public static void main(String[] args) {
Duration base = Duration.ofMinutes(10);
for (int i = 0; i < 3; i++) {
Duration ttl = withJitter(base, 0.1); // +/- 10%
System.out.println("TTL seconds: " + ttl.getSeconds());
}
}
}Two-Tier Caching: Caffeine + Redis
You don't have to choose. A common production pattern is a two-tier (near) cache:
- L1 — Caffeine in each instance, very short TTL, absorbs hot reads at nanosecond speed.
- L2 — Redis, shared across the cluster, the source of truth for cached state.
A request checks Caffeine first; on a miss it falls back to Redis; on a Redis miss it hits the database. This cuts network round-trips while keeping the cluster consistent. Spring can compose this with a CompositeCacheManager or a dedicated near-cache library.
Quick Check: Choosing a TTL Strategy
Test your understanding of distributed cache trade-offs.
Recap: Distributed Caching with Redis
You moved from a private in-process cache to a shared, distributed one:
- Why Redis: one cache state across all instances eliminates drift and duplicated work.
- Drop-in swap: set
spring.cache.type=redisand your@Cacheablecode is unchanged. - TTLs: Redis enforces expiry server-side, uniformly across the cluster, surviving instance restarts; tune per cache with a
RedisCacheManagerBuilderCustomizer. - Serialization: prefer
GenericJackson2JsonRedisSerializerfor readable, version-tolerant values over default JDK serialization. - Key prefixes isolate caches sharing one Redis; TTL jitter and two-tier Caffeine+Redis caches tame the thundering herd.
常见问题解答
「使用 Redis 与 TTL 实现分布式缓存」课时是免费的吗?
是的 — 「使用 Redis 与 TTL 实现分布式缓存」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Spring Boot 4 Complete Guide 课程的其余内容,请升级到 CoddyKit PRO。 Spring Boot 4 Complete Guide 课程共包含 4 节课。
「使用 Redis 与 TTL 实现分布式缓存」这节课中我会学到什么?
使用 Redis 作为集中式缓存后端,在控制序列化的同时跨实例共享缓存状态。 你通过在浏览器中直接运行的动手代码来练习 Spring Boot 4 Complete Guide,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Spring Boot 4 Complete Guide 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Spring Boot 4 Complete Guide 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。
「使用 Redis 与 TTL 实现分布式缓存」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Spring Boot 4 Complete Guide 课中编写并运行代码吗?
能。每节 Spring Boot 4 Complete Guide 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- Spring 缓存抽象基础
- 使用 Caffeine 调优内存缓存
- 使用 Redis 与 TTL 实现分布式缓存
- 缓存击穿、失效与一致性