Redis with Spring Boot 4.1: RedisTemplate vs StringRedisTemplate, @RedisHash, Pub/Sub and TTL
RedisTemplate vs StringRedisTemplate, @RedisHash, Pub/Sub, TTL and Redis as the Spring cache on Spring Boot 4.1, with the JDK-serialisation default and the Jackson 3 fix shown in redis-cli.
Redis is a server that keeps data in memory under keys you choose: strings, hashes, lists, sets, sorted sets. It is quick enough to hold sessions, counters, caches and small messages, and Spring Boot 4.1 connects to it with one dependency. That ease is also why a lot of people hit the same surprise on day one: you save a value from Java, open redis-cli to look at it, and the key is \xac\xed\x00\x05t\x00\x06user:1, and GET user:1 answers (nil).
This page starts there and works outward: why it happens, what to use instead, and then @RedisHash repositories, expiry, Pub/Sub, TTLs and Redis as the Spring cache. Every number and every redis-cli reply below came from a real redis-server that the tests start, driven by the real redis-cli, and every block links to the file it came from in asmhatre/spring-boot-demo, redis module.
Versions. Spring Boot 4.1.1, Spring Data Redis 4.1.1, Lettuce 7.5.2, Jackson 3.1.5 (tools.jackson), Redis server 7.0.15, JDK 25 (Temurin 25.0.4.1). Class names and deprecations were read from the jars with javap (transcript 16), because the reference documentation and most tutorials still show the Jackson 2 classes. Retrieved 21 September 2026.
Add one starter and Redis is already wired up
You need one dependency to talk to Redis. This module also adds the Jackson 3 starter, for a reason the JSON section explains, and the cache starter, for the caching section at the end:
<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>
<!-- Jackson 3 (tools.jackson). The Redis starter does not bring a JSON library. -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jackson</artifactId>
</dependency>
That is from pom.xml. Start the application with a Redis on localhost:6379 and Boot has already created, without a line of configuration from you, a connection factory, two templates for reading and writing (RedisTemplate and StringRedisTemplate), a container that receives Pub/Sub messages, and, if you declare an interface, a Redis repository. In Boot 4 the properties live under spring.data.redis.*, which is what the tests use to point at their throwaway server (RedisTest.java):
The picture is the map for the rest of the page. Your code talks to Spring Data Redis, which owns the serializers, the repositories, the listener container and the cache manager, and which hands commands to the Lettuce client. Redis itself only ever sees bytes, so every problem in the next few sections happens at the left-hand edge of the server box: the moment an object or a string becomes bytes. What the auto-configuration registers, read from the Boot jar (transcript 16), is this:
Bean
What it is
Where this page covers it
redisTemplate
RedisTemplate<Object, Object>, JDK serialization
the serializer sections
stringRedisTemplate
StringRedisTemplate, text in and out
the StringRedisTemplate section
redisMessageListenerContainer
receives Pub/Sub messages, created for you
the Pub/Sub section
Redis repositories
@RedisHash entities, keyspace events off
the two @RedisHash sections
RedisCacheManager
when @EnableCaching is on
the caching section
The dependency tree shows what the starter actually brought (transcript 17):
Two things in it matter later. The client is Lettuce 7.5.2, and Jackson is there only because this module asked for spring-boot-starter-jackson itself. The starter’s own pom lists three dependencies:
The Redis starter includes no JSON library. If you want JSON values you add Jackson yourself. In Boot 4 that means Jackson 3 (tools.jackson), which is not the Jackson your older tutorials import. Getting the class name wrong is the second surprise of this page.
What Boot registers and what the tests do: chapter 1.
Boot also ships a Redis health indicator (DataRedisHealthIndicator is in the same jar); how health indicators and their groups work: Spring Boot Actuator in Production.
Why redis-cli shows garbage instead of your keys
Redis stores bytes. A template is the translator between your Java objects and those bytes, and the translator is called a serializer. Boot registers a RedisTemplate<Object, Object>, and if you inject that one and do nothing else you get the JDK serializer for keys and values:
key serializer: JdkSerializationRedisSerializer
value serializer: JdkSerializationRedisSerializer
Now save one plain string, and look at it from the Redis side, with redis-cli --no-raw so that binary bytes print as escapes (same transcript, 01-default-template.txt):
Two things happened. The stored key is not user:1: it is the bytes Java writes when it serializes a String object. And GET user:1 found nothing, because that key does not exist. Java reading the value back through the same template works fine, which is why this hides so well: every test that goes through the template passes.
Reading the bytes left to right: \xac\xed and \x00\x05 are the header every Java serialization stream starts with (a magic number and version 5), t is the type code for a string, \x00\x06 is its length, and only then come the six characters. The type codes are in the Java Object Serialization Specification: stream protocol. A thirteen-byte key that contains your six-byte key is not the same key.
The same serializer applies to objects, with two more consequences. A value that does not implement Serializable is refused and nothing is written:
redisTemplate.opsForValue().set("user:2", new User(2, "Ankur"))
org.springframework.data.redis.serializer.SerializationException
message: Cannot serialize
root: DefaultSerializer requires a Serializable payload but received an object of type [com.ankurm.redis.model.User]
A value that does implement it is accepted, and what lands in Redis embeds the fully qualified class name, so a rename or a package move makes existing entries unreadable (transcript 02):
The counter is the least obvious. A counter uses increment, which sends Redis’s INCR command. Redis stores the plain text 1 as the value, and the template then tries to read that text back as a Java serialization stream:
Any command where Redis itself interprets the value (INCR, INCRBYFLOAT, HINCRBY) needs a serializer that writes plain text. The run is transcript 03.
The fingerprint of this bug. You see keys beginning \xac\xed\x00\x05 in redis-cli, a SerializationException: Cannot deserialize on a counter, or a GET that returns (nil) for a key you know you wrote. All three are the JDK serializer, not Redis. Changing the serializer on a Redis that already holds data orphans the old entries, because the new keys have different bytes; this repository does not exercise a migration, so plan one before you change it.
StringRedisTemplate: readable, and a trap when you use both
The fix for text is the other template Boot registers. StringRedisTemplate sends every key, value, hash key and hash value through StringRedisSerializer, which is UTF-8 in and out:
Everything is readable, counters work and read back, and the same goes for lists, sets and sorted sets, which the transcript also shows. That is the whole story for text. The trap is what happens when an application has both templates, or when two services share a Redis and configure their templates differently:
The serializer is part of the key. Write user:1 through one template and read it through the other and you get null, because the second template asks for different bytes (transcript 05):
Redis now holds two entries, and only one of them is the key you can type. This is one way to end up with a cache that works in one service and misses in another: two templates, one Redis, and keys that only look the same in the logs.
One key serializer per Redis. Pick StringRedisSerializer for keys and use it in every template in every service that shares the server. It is what you want unless you have a specific reason, and every template configured in the next section does it.
Store JSON instead, with the Jackson 3 serializers
For objects, the usual answer is JSON: readable in redis-cli, readable from other languages, and not tied to a Java class layout. Spring Data Redis 4.1 has two families of JSON serializer, and their names differ by one character. Reading the jar (transcript 16):
Class
Jackson
Status in 4.1.1
GenericJackson2JsonRedisSerializer
2
@Deprecated(since = "4.0")
Jackson2JsonRedisSerializer<T>
2
@Deprecated(since = "4.0")
GenericJacksonJsonRedisSerializer
3 (tools.jackson)
not deprecated
JacksonJsonRedisSerializer<T>
3 (tools.jackson)
not deprecated
The evidence for that table is javap -v: the Jackson 2 class carries @Deprecated(since="4.0") in its bytecode, and the Jackson 3 class carries nothing (transcript 16):
$ javap -v -cp spring-data-redis-*.jar org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer | grep -E 'Deprecated|since'
#377 = Utf8 Deprecated
#379 = Utf8 Ljava/lang/Deprecated;
#380 = Utf8 since
Deprecated: true
java.lang.Deprecated(
since="4.0"
(end of output for GenericJackson2JsonRedisSerializer)
$ javap -v -cp spring-data-redis-*.jar org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer | grep -E 'Deprecated|since'
(end of output for GenericJacksonJsonRedisSerializer)
If a tutorial imports com.fasterxml.jackson.databind.ObjectMapper into a Redis serializer, it is written for the older generation. The Jackson 3 classes take a tools.jackson.databind.ObjectMapper (Jackson 2 to Jackson 3 Migration Guide covers the rename). The constructors of the typed one, from the same transcript (transcript 16):
$ javap -public -cp <classpath> org.springframework.data.redis.serializer.JacksonJsonRedisSerializer | grep 'JacksonJsonRedisSerializer('
public org.springframework.data.redis.serializer.JacksonJsonRedisSerializer(java.lang.Class<T>);
public org.springframework.data.redis.serializer.JacksonJsonRedisSerializer(tools.jackson.databind.JavaType);
public org.springframework.data.redis.serializer.JacksonJsonRedisSerializer(tools.jackson.databind.ObjectMapper, java.lang.Class<T>);
public org.springframework.data.redis.serializer.JacksonJsonRedisSerializer(tools.jackson.databind.ObjectMapper, tools.jackson.databind.JavaType);
public org.springframework.data.redis.serializer.JacksonJsonRedisSerializer(tools.jackson.databind.ObjectMapper, tools.jackson.databind.JavaType, org.springframework.data.redis.serializer.JacksonObjectReader, org.springframework.data.redis.serializer.JacksonObjectWriter);
There are two ways to use them. The typed serializer is for one value type per template and writes plain JSON. The generic one is a single template for any type, and to read a value back as the right class it has to store the class name inside the JSON. The configuration is in JsonRedisConfig.java:
That is JsonRedisConfig.java, which also defines a userRedisTemplate using new JacksonJsonRedisSerializer<>(User.class). Running all three shapes (transcript 06):
userTemplate.opsForValue().set("user:1", new User(1, "Ankur"))
$ redis-cli -p 6390 GET user:1
{"id":1,"name":"Ankur"}
read back: User[id=1, name=Ankur] (User)
jsonTemplate.opsForValue().set("user:2", new User(2, "Ankur"))
$ redis-cli -p 6390 GET user:2
{"@class":"com.ankurm.redis.model.User","id":2,"name":"Ankur"}
plain.opsForValue().set("user:3", new User(3, "Ankur"))
$ redis-cli -p 6390 GET user:3
{"id":3,"name":"Ankur"}
read back: {id=3, name=Ankur} (LinkedHashMap)
Three shapes: plain JSON from the typed serializer, JSON with an @class property from the generic one with typing turned on, and, if you use the generic serializer without configuring typing, JSON that reads back as a LinkedHashMap instead of your class. The bottom of the diagram is the part to take seriously. @class is not metadata, it is an instruction: whatever is in Redis chooses which class the serializer instantiates, and anyone who can write to that Redis can write any class name that is on your classpath. That is why the builder has a method literally called enableUnsafeDefaultTyping() and why the safe one takes a validator. The allow-list above accepts only types under com.ankurm.redis., and here is a class outside it, and a class inside it that no longer exists (transcript 07):
$ redis-cli -p 6390 SET user:evil '{"@class":"com.ankurm.other.Outsider","name":"x"}'
jsonTemplate.opsForValue().get("user:evil")
org.springframework.data.redis.serializer.SerializationException
message: Could not read JSON: Could not resolve type id 'com.ankurm.other.Outsider' as a subtype of `com.ankurm.other.Outsider`: Configured `PolymorphicTypeValidator` (of type `tools.jackson.databind.jsontype.BasicPolymorphicTypeValidator`) denied resolution
$ redis-cli -p 6390 SET user:gone '{"@class":"com.ankurm.redis.model.Gone","id":9}'
message: Could not read JSON: Failed to parse type 'com.ankurm.redis.model.Gone' (remaining: ''): Cannot locate class 'com.ankurm.redis.model.Gone', problem: com.ankurm.redis.model.Gone
Both failures are worth recognising in a log. The first is the allow-list doing its job. The second is what a refactor looks like when old entries are still in Redis: type names stored in Redis are a compatibility surface, exactly as the class name inside a JDK-serialised value was. If you would rather not have one, the typed serializer stores no class name at all. Jackson Security Best Practices: Defending Against Deserialization Gadget Attacks and Jackson Polymorphic Deserialisation explain the risk in general.
Typed
Generic
redis-cli output
plain JSON
JSON with @class
Rename or move the class
safe
breaks reads
Needs an allow-list
no
yes
Templates you need
one per value type
one
Both serializers, the builder options and the failures: chapter 4.
If you would rather not call the template yourself, Spring Data Redis can map a class to a Redis hash and give you a repository. The entity needs @RedisHash and an @Id; @Indexed on a field makes it searchable. The files are Person.java and PersonRepository.java:
@RedisHash("person")
public class Person {
@Id
private String id;
private String firstName;
/** Indexed fields get a Redis set per value, e.g. {@code person:lastName:Mhatre}. */
@Indexed
private String lastName;
private int age;
public interface PersonRepository extends CrudRepository<Person, String> {
/** Answered from the index set {@code person:lastName:<value>}, which exists because lastName is @Indexed. */
List<Person> findByLastName(String lastName);
}
Nothing else is configured: Boot enables Redis repositories on its own. Save two people and list what appeared (transcript 08):
Two people produced six keys. Each person is a hash, person:1, and the hash records the Java type in _class. The other keys are bookkeeping that Spring Data Redis maintains for you: a set called person that holds every id (this is what count() reads), a set per indexed value (person:lastName:Mhatre, which is how findByLastName is answered), and a small set per entity, person:1:idx, that lists the index sets the entity is in, which the delete at the end of the transcript removes along with the rest:
It does not go through the template serializers described above: the transcript shows the fields stored as plain text values, so the JDK default is not the problem here. The catch is the bookkeeping. Spring Data Redis keeps those extra keys tidy only when it is the one doing the deleting.
The six keys and what deleting does to them: chapter 5.
Expiry on a @RedisHash: the leftovers, and the fix
A hash can carry a time to live: @RedisHash(value = "session", timeToLive = 2) gives every saved session two seconds. Redis deletes the hash when the time is up. The question is what happens to everything Spring Data Redis wrote around the hash. With Boot’s defaults, the answer is nothing, and the transcript shows what that costs (transcript 09):
The hash is gone and the index entries are not. findById is right (there is no hash to find), but count() still says 1 because the id is still in the session set, and session:user:ankur still lists an id that resolves to nothing. On a busy system these accumulate. The default is enableKeyspaceEvents = OFF, and I read that from the annotation’s bytecode rather than from prose (transcript 16).
The fix is to switch keyspace events on. That does three things, which the second timeline shows. This is the configuration and the listener from HashTtlEventsOnTest.java:
First, Spring Data Redis changes a server setting. Redis only publishes expiry notifications when notify-keyspace-events is non-empty, and the transcript (transcript 10) reads it before the application context starts and after:
The annotation’s keyspaceNotificationsConfigParameter defaults to Ex, and Redis reports the same setting back as xE. Second, it keeps a phantom copy of the hash (session:s1:phantom) alive five minutes longer than the hash itself, 302 seconds for a two-second TTL, so that the entity still exists when the expiry announcement arrives. Third, the announcement becomes a Spring event (same transcript, 10-hash-ttl-events-on.txt):
Redis expired the hash and announced it; Spring Data Redis then removed the index entries and the phantom key, and delivered a RedisKeyExpiredEvent carrying the entity, which the phantom copy is there to make possible. Afterwards the database is empty and count() is 0.
Do not build correctness on it. The announcement travels over Pub/Sub, which is fire-and-forget: a subscriber that is not connected when it is sent never sees it, as the Pub/Sub section below demonstrates with a stopped listener. If your application is down when a session expires, its cleanup is not guaranteed. This repository does not test a missed notification, so treat that as a design consequence rather than a measured result. Some managed Redis services also restrict CONFIG SET; I did not run against one, so check yours. Redis keyspace notifications describes the mechanism.
Off versus on, with the server setting: chapter 6.
Pub/Sub: Boot 4.1 removes the boilerplate, and Redis keeps nothing
Pub/Sub is a broadcast. PUBLISH channel message sends the message to whoever is subscribed to the channel at that moment, and the reply is the number of subscribers that got it. Older articles start by declaring a RedisMessageListenerContainer bean. Boot 4.1 declares one for you, named redisMessageListenerContainer, and enables the @RedisListener annotation, so a listener is a method (both are in NewsListener.java):
A topic that contains a glob, like alerts.*, subscribes as a pattern, and the channel a message actually arrived on is available as a header. Sending is one call on the string template, which returns the receiver count (transcript 11):
beans of type RedisMessageListenerContainer: [redisMessageListenerContainer]
running=true listening=true
template.convertAndSend("quiet", "hello?") = 0 receivers
template.convertAndSend("news", "first") = 1 receiver
@RedisListener got 'first'
container.addMessageListener(late, new ChannelTopic("news"))
template.convertAndSend("news", "second") = 1 receiver
@RedisListener got 'second'
late listener got 'second' on news
$ redis-cli -p 6390 --no-raw PUBSUB NUMPAT
(integer) 1
template.convertAndSend("alerts.disk", "90% full") = 1 receiver
@RedisListener got '90% full' on alerts.disk
Read the numbers, because they are the point. A message to a channel nobody listens to reaches zero receivers and is simply gone. A message to news reaches one. Add a second listener to the same channel and PUBLISH still says one, because the container subscribes once and fans the message out to its listeners inside your application: the number is receivers on the Redis side, not listeners on yours. And a pattern subscriber counts as a receiver of PUBLISH, though PUBSUB NUMSUB on the channel says zero, because NUMSUB counts only direct channel subscribers.
Nothing is queued, which the next run makes concrete by stopping the container, publishing, and starting it again:
container.stop()
template.convertAndSend("news", "while you were away") = 0 receivers
container.start()
messages the listener has: []
template.convertAndSend("news", "welcome back") = 1 receiver
@RedisListener got 'welcome back'
The message sent while the listener was away is never delivered (transcript 12). That is the design, and it is why Pub/Sub suits hints and live updates and does not suit anything that must not be lost. Redis Streams exist for that; they are not covered here.
A parameter that did not work. A listener method declared with org.springframework.data.redis.connection.Message as its parameter failed in this test: the adapter tried to parse the payload as JSON and threw a StreamReadException on the text 90% full. A String body plus @Header(PubSubHeaders.CHANNEL), as above, worked. I did not investigate why.
Receiver counts, patterns and the lost message: chapter 7.
A TTL is how long a key lives. getExpire(key) returns the seconds left, with two negative values that look like errors and are not: -1 means the key exists and never expires, and -2 means it does not exist. Setting a value and its TTL together in one command avoids a window in which the key exists without one (the run is 13-ttl.txt):
Read the run with the diagram. A fresh ten-minute TTL reads 600 seconds, and the same key asked in minutes reads 9, because the conversion truncates. Then the trap: a plain set on a key with a TTL removes the TTL, so refreshing a cached value without passing the TTL again makes it immortal. If you need to overwrite and keep the clock, use KEEPTTL:
That line is in TtlTest.java, and the transcript shows the TTL still at 600 afterwards. The same run shows that Duration.ofMillis(1500) is honoured, that persist removes a TTL, and that a 300-millisecond key really is gone when read back. One more use of a TTL is the smallest lock: setIfAbsent with a duration succeeds for the first caller, fails for the second, and disappears by itself if the holder dies.
That is not a full distributed lock. There is no owner-checked release and no fencing here, and this repository does not test either. It shows what setIfAbsent with a TTL does, no more.
Everything above applies when Redis backs @Cacheable, with two twists. The annotations themselves (keys, eviction, and the self-invocation trap that silently disables them) are covered in The Spring Cache Abstraction: @Cacheable, @CacheEvict, Key Generators and the Self-Invocation Trap; this section is only about what changes when the store is Redis. With the cache starter and @EnableCaching, Boot picks RedisCacheManager, and the two cached methods below return results that differ only in whether they are Serializable:
@Cacheable("legacy")
public SerializableUser findSerializable(long id) {
calls.incrementAndGet();
return new SerializableUser(id, "Ankur");
}
@Cacheable("users")
public User find(long id) {
calls.incrementAndGet();
return new User(id, "Ankur");
}
That is UserService.java. The keys are readable, cacheName::key, so the key half is not a problem. The value half is the same as in the first serializer section, because the cache’s default serializer for values is the JDK one (transcript 14):
org.springframework.data.redis.cache.RedisCacheManager
findSerializable(1) called twice, method body ran 1 time(s)
$ redis-cli -p 6390 --no-raw TTL legacy::1
(integer) 600
$ redis-cli -p 6390 --no-raw GET legacy::1
"\xac\xed\x00\x05sr\x00'com.ankurm.redis.model.SerializableUser\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x02J\x00\x02idL\x00\x04namet\x00\x12Ljava/lang/String;xp\x00\x00\x00\x00\x00\x00\x00\x01t\x00\x05Ankur"
users.find(1)
java.lang.IllegalStateException
message: Cannot serialize value of type com.ankurm.redis.model.User without a serializer
method body ran 2 time(s) in total
Two consequences. The stored value is unreadable bytes with a class name inside. And a result that is not Serializable does not just fail to cache: the method call fails. The body ran, the write to Redis threw, and the caller got the exception, so a @Cacheable on a method that returns a plain record breaks that method. A spring.cache.redis.time-to-live=10m property did take effect on the Serializable case (TTL is 600).
The fix is one bean, a RedisCacheConfiguration that stores JSON. Here it uses the same allow-listed serializer as in the JSON section:
find(1) called twice, method body ran 1 time(s)
$ redis-cli -p 6390 GET users::1
{"@class":"com.ankurm.redis.model.User","id":1,"name":"Ankur"}
$ redis-cli -p 6390 --no-raw TTL users::1
(integer) 300
The plain record now caches, the method body ran once for two calls, and the value is JSON you can read.
The bean overrides the property. The test sets spring.cache.redis.time-to-live=10mand declares a bean with entryTtl(Duration.ofMinutes(5)). The key’s TTL is 300. When you define your own RedisCacheConfiguration, Boot used it and the property had no effect on the TTL, so put the TTL in the bean. I observed this for the TTL only; I did not check every spring.cache.redis.* property.
Most of the choices above come down to one question: what do you want to see when you open redis-cli?
You want to store
Use
Watch out for
Text, counters, flags
StringRedisTemplate
Do not mix it with the default template on the same keys
One object type as JSON
RedisTemplate with JacksonJsonRedisSerializer<T>
One template per type
Many object types as JSON
Generic Jackson 3 serializer with an allow-list
@class is input; a rename breaks old entries
Entities you query by a field
@RedisHash repository
Six keys for two entities; turn on keyspace events if you use a TTL
A cache
@Cacheable with a RedisCacheConfiguration bean
The bean overrides spring.cache.redis.*
Notifications you can afford to lose
@RedisListener
Nothing is queued for a listener that is not there
Should you use Redis at all? If you run one instance and only need a cache, an in-process cache is simpler and has no network hop or second system to operate, and The Spring Cache Abstraction: @Cacheable, @CacheEvict, Key Generators and the Self-Invocation Trap shows the annotations work the same on it. Redis earns its place when several instances must share state: a cache they all see, sessions, counters, or a broadcast. The default serializer is the reason a first Redis integration often looks broken. It is not a reason to avoid Redis, and it is fixed with a few lines of configuration. One caution for the tests in the repository: they call FLUSHALL, which is why they start their own server on port 6390 and refuse to reuse one.
FAQs
What is the difference between RedisTemplate and StringRedisTemplate?
The serializer. StringRedisTemplate writes keys and values as UTF-8 text, so you can read them in redis-cli. The default RedisTemplate<Object, Object> Boot registers writes JDK-serialised bytes for both. Because the serializer is part of the key, the two templates do not see each other’s keys (transcript 05).
Why does redis-cli GET return (nil) for a key I just saved?
Almost certainly because the key in Redis is not the text you typed. With the default template the stored key is \xac\xed\x00\x05t\x00\x06user:1 (transcript 01). Use StringRedisTemplate, or set StringRedisSerializer as the key serializer.
Is Jackson2JsonRedisSerializer deprecated in Spring Boot 4?
Yes: in Spring Data Redis 4.1.1 both Jackson2JsonRedisSerializer and GenericJackson2JsonRedisSerializer carry @Deprecated(since="4.0"). The Jackson 3 replacements are JacksonJsonRedisSerializer and GenericJacksonJsonRedisSerializer (transcript 16).
How do I set a TTL with Spring Data Redis?
For a plain key, opsForValue().set(key, value, Duration), or expire(key, Duration) afterwards. For a @RedisHash entity, timeToLive on the annotation, with keyspace events on if you want the index entries cleaned up. A plain set on a key with a TTL clears it; use Expiration.keepTtl() to keep it (transcript 13).
Do I need to declare a RedisMessageListenerContainer in Boot 4.1?
No. Boot registers one named redisMessageListenerContainer and enables @RedisListener, and the listener in the transcript (transcript 11) uses nothing else. Declare your own bean of that name only if you want to replace it.
Conclusion
Redis with Spring Boot 4.1 is easy to connect and easy to misread. Nearly all the confusion comes from one default, the JDK serializer, applied to keys as well as values: fix that first, with StringRedisTemplate or a String key serializer plus a Jackson 3 value serializer, and the tools you use to look at Redis start telling the truth. After that the interesting parts are the ones that happen around your data rather than to it: the keys a repository creates on your behalf, the bookkeeping that expiry leaves behind unless keyspace events are on, the way a plain set erases a TTL, and the fact that Pub/Sub keeps nothing.
No Comments yet!