Add redis: Boot 4.1 + Spring Data Redis 4.1, the JDK-serialisation default vs Jackson 3 serializers, @RedisHash TTL and keyspace events, @RedisListener, and Redis as the Spring cache
Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01B38FGKKam5SCGgwgduVAh3
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
[Home](../README.md) | Next: [Default serialization](02-default-serialization.md)
|
||||
|
||||
# 1. What Boot gives you
|
||||
|
||||
Add `spring-boot-starter-data-redis` and start the application. With no configuration at all you
|
||||
already have a connection factory, two templates, a Pub/Sub listener container and, if you declare
|
||||
an interface, a repository. This chapter lists exactly what, because every surprise in the later
|
||||
chapters comes from one of these defaults.
|
||||
|
||||
## What the starter brings
|
||||
|
||||
[output/17-dependencies.txt](output/17-dependencies.txt) is the starter's own pom plus a filtered
|
||||
`mvn dependency:tree`. Three things arrive: `spring-boot-starter`, `spring-boot-data-redis` (the
|
||||
auto-configuration) and `spring-messaging`. Under `spring-boot-data-redis` come `spring-data-redis`
|
||||
4.1.1 and the Lettuce client 7.5.2. There is no Jedis and **no JSON library**.
|
||||
|
||||
That last point matters: Jackson is not a transitive dependency of the Redis starter. This module
|
||||
adds `spring-boot-starter-jackson` (Jackson 3, package `tools.jackson`) itself, and that is the only
|
||||
reason `tools.jackson.core:jackson-databind:3.1.5` is in the tree. Note also that Jackson 3 still
|
||||
uses the Jackson 2 annotations artifact (`com.fasterxml.jackson.core:jackson-annotations:2.21`).
|
||||
|
||||
## What the auto-configuration registers
|
||||
|
||||
[output/16-serializers-javap.txt](output/16-serializers-javap.txt) reads the Boot jar with `javap`.
|
||||
Boot 4 moved the classes to `org.springframework.boot.data.redis.autoconfigure`, and properties live
|
||||
under `spring.data.redis.*` (`spring.data.redis.port` is what the tests set).
|
||||
|
||||
| Bean | Type | Notes |
|
||||
|---|---|---|
|
||||
| `redisTemplate` | `RedisTemplate<Object, Object>` | JDK serialization, see [chapter 2](02-default-serialization.md) |
|
||||
| `stringRedisTemplate` | `StringRedisTemplate` | String serialization, see [chapter 3](03-string-template-and-two-keyspaces.md) |
|
||||
| `redisMessageListenerContainer` | `RedisMessageListenerContainer` | Created when there is a single `RedisConnectionFactory` and no bean of that name. A second definition, `redisMessageListenerContainerVirtualThreads`, is annotated `@ConditionalOnThreading`. Also turns on `@EnableRedisListeners`. See [chapter 7](07-pub-sub.md) |
|
||||
| Redis repositories | `@RedisHash` repositories | Enabled with `enableKeyspaceEvents = OFF`. See [chapter 6](06-hash-ttl-and-keyspace-events.md) |
|
||||
| `RedisCacheManager` | when `@EnableCaching` is on | See [chapter 9](09-redis-as-cache.md) |
|
||||
|
||||
The templates and the listener container are `@ConditionalOnMissingBean` (the container by name), so the
|
||||
way to change a default is to declare your own bean. That is also how you lose the default without noticing: a bean of the same *name* replaces
|
||||
Boot's, a bean of a different name sits next to it. `JsonRedisConfig` in this module names its
|
||||
templates `jsonRedisTemplate` and `userRedisTemplate` for that reason.
|
||||
|
||||
## The setup used in this repository
|
||||
|
||||
- The tests start a throwaway `redis-server` on **6390** ([LocalRedis](../src/test/java/com/ankurm/redis/LocalRedis.java)) and stop it when the JVM exits.
|
||||
- They call `FLUSHALL`, so `LocalRedis` refuses to run against a server that is already listening.
|
||||
- Every transcript line starting with `$ redis-cli` was produced by running the real `redis-cli` from the test ([Transcript](../src/test/java/com/ankurm/redis/Transcript.java)).
|
||||
- `--no-raw` makes `redis-cli` print binary bytes as `\xNN` escapes and `(nil)` for a missing key. Without it, a non-terminal `redis-cli` prints raw bytes and a bare blank line for nil.
|
||||
- `| sort` after a `KEYS` or `SMEMBERS` command means the transcript sorted the lines, because Redis returns them in no particular order.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [Spring Boot reference: NoSQL, Redis](https://docs.spring.io/spring-boot/reference/data/nosql.html#data.nosql.redis)
|
||||
- [Spring Data Redis reference](https://docs.spring.io/spring-data/redis/reference/)
|
||||
- Article: [The Spring Cache Abstraction](https://ankurm.com/spring-cache-abstraction-cacheable-cacheevict-self-invocation-trap/)
|
||||
|
||||
[Home](../README.md) | Next: [Default serialization](02-default-serialization.md)
|
||||
@@ -0,0 +1,69 @@
|
||||
[Home](../README.md) | Prev: [What Boot gives you](01-what-boot-gives-you.md) | Next: [String template and two keyspaces](03-string-template-and-two-keyspaces.md)
|
||||
|
||||
# 2. Default serialization: why redis-cli shows garbage
|
||||
|
||||
Redis stores bytes. A `RedisTemplate` is a translator between Java objects and those bytes, and the
|
||||
translator is a `RedisSerializer`. The template Boot registers, `RedisTemplate<Object, Object>`,
|
||||
uses `JdkSerializationRedisSerializer` for keys **and** values
|
||||
([01-default-template.txt](output/01-default-template.txt)):
|
||||
|
||||
```
|
||||
key serializer: JdkSerializationRedisSerializer
|
||||
value serializer: JdkSerializationRedisSerializer
|
||||
```
|
||||
|
||||
## What lands in Redis
|
||||
|
||||
`redisTemplate.opsForValue().set("user:1", "Ankur")` stores this key, which is not `user:1`:
|
||||
|
||||
```
|
||||
$ redis-cli -p 6390 --no-raw KEYS '*'
|
||||
1) "\xac\xed\x00\x05t\x00\x06user:1"
|
||||
$ redis-cli -p 6390 --no-raw GET user:1
|
||||
(nil)
|
||||
```
|
||||
|
||||
Reading the bytes: `\xac\xed\x00\x05` is the header of every Java serialization stream (magic
|
||||
number `0xACED`, stream version 5). `t` is the type code for a string, `\x00\x06` its length, then
|
||||
the six characters. Java wrote a *serialised String object*, not the characters. `GET user:1`
|
||||
therefore misses: a different key.
|
||||
|
||||
The value is the same story (`"\xac\xed\x00\x05t\x00\x05Ankur"`), and Java reading it back through the same template
|
||||
works, which is why the problem hides: every test that goes through the template passes.
|
||||
|
||||
## What it does to objects
|
||||
|
||||
[02-default-template-not-serializable.txt](output/02-default-template-not-serializable.txt):
|
||||
|
||||
- A record that is not `java.io.Serializable` is rejected with `SerializationException: Cannot serialize`. The root cause message is Spring's `DefaultSerializer requires a Serializable payload`. Nothing is written.
|
||||
- The same record implementing `Serializable` is accepted, and the stored bytes contain the **fully qualified class name** (`com.ankurm.redis.model.SerializableUser`) and field descriptors. Rename or move the class and existing entries stop deserializing.
|
||||
|
||||
## The counter trap
|
||||
|
||||
`opsForValue().increment("hits")` sends `INCR`, and Redis stores the text `1`. The template wrote the
|
||||
key with JDK serialization, but the counter's value is plain text, so the next `get` fails
|
||||
([03-default-template-increment.txt](output/03-default-template-increment.txt)):
|
||||
|
||||
```
|
||||
org.springframework.data.redis.serializer.SerializationException
|
||||
message: Cannot deserialize
|
||||
root: java.io.EOFException: null
|
||||
```
|
||||
|
||||
Any command where Redis itself interprets the value (`INCR`, `INCRBYFLOAT`, `HINCRBY`) needs a
|
||||
serializer that writes plain text.
|
||||
|
||||
## Fixes
|
||||
|
||||
1. Use `StringRedisTemplate` when everything is text ([chapter 3](03-string-template-and-two-keyspaces.md)).
|
||||
2. Configure String keys and a JSON value serializer ([chapter 4](04-json-serializers.md)).
|
||||
3. If data written with the JDK serializer is already in production, **changing the serializer orphans it**: new keys have different bytes. Plan a migration or a new key prefix. This repository does not exercise a migration.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [User](../src/main/java/com/ankurm/redis/model/User.java) and [SerializableUser](../src/main/java/com/ankurm/redis/model/SerializableUser.java)
|
||||
- [DefaultTemplateTest](../src/test/java/com/ankurm/redis/DefaultTemplateTest.java)
|
||||
- [Java Object Serialization Specification: stream grammar and type codes](https://docs.oracle.com/en/java/javase/25/docs/specs/serialization/protocol.html)
|
||||
- [Spring Data Redis: serializers](https://docs.spring.io/spring-data/redis/reference/redis/template.html#redis:serializer)
|
||||
|
||||
[Home](../README.md) | Prev: [What Boot gives you](01-what-boot-gives-you.md) | Next: [String template and two keyspaces](03-string-template-and-two-keyspaces.md)
|
||||
@@ -0,0 +1,50 @@
|
||||
[Home](../README.md) | Prev: [Default serialization](02-default-serialization.md) | Next: [JSON serializers](04-json-serializers.md)
|
||||
|
||||
# 3. StringRedisTemplate, and why two templates do not share keys
|
||||
|
||||
`StringRedisTemplate` is the template Boot registers for text. Keys, values, hash keys and hash
|
||||
values all go through `StringRedisSerializer`, which is UTF-8 in and out
|
||||
([04-string-template.txt](output/04-string-template.txt)):
|
||||
|
||||
```
|
||||
key serializer: StringRedisSerializer
|
||||
value serializer: StringRedisSerializer
|
||||
hash key serializer: StringRedisSerializer
|
||||
hash value serializer: StringRedisSerializer
|
||||
```
|
||||
|
||||
Everything is then readable from `redis-cli`: strings, counters (`INCR` works and can be read back),
|
||||
hashes (`opsForHash`), lists, sets and sorted sets. The transcript shows each one.
|
||||
|
||||
## Two templates, two keyspaces
|
||||
|
||||
The important thing about a serializer is that it is part of the **key**. Write `user:1` with the
|
||||
`StringRedisTemplate` and read it with the default `RedisTemplate`, and you get `null`, because the
|
||||
default template looks for a different byte sequence
|
||||
([05-two-templates-two-keyspaces.txt](output/05-two-templates-two-keyspaces.txt)):
|
||||
|
||||
```
|
||||
stringTemplate.opsForValue().get("user:1") = from-string-template
|
||||
redisTemplate.opsForValue().get("user:1") = null
|
||||
```
|
||||
|
||||
Write it through both and Redis holds two entries (`DBSIZE` is 2), only one of which `redis-cli GET user:1` can see.
|
||||
This is one way to end up with "my cache works in one service and misses in another": two services,
|
||||
two templates, one Redis, and keys that only look the same in the logs.
|
||||
|
||||
The fix is a rule, not a class: pick one key serializer per Redis and put it in one shared
|
||||
configuration. `StringRedisSerializer` for keys is what you want unless you have a reason, and every
|
||||
template in [JsonRedisConfig](../src/main/java/com/ankurm/redis/config/JsonRedisConfig.java) does that.
|
||||
|
||||
## When StringRedisTemplate is enough
|
||||
|
||||
Counters, flags, locks, session ids, anything you would be happy to `GET` by hand. If you find
|
||||
yourself calling `objectMapper.writeValueAsString` before every `set`, you are doing by hand what a
|
||||
JSON value serializer does ([chapter 4](04-json-serializers.md)).
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [StringTemplateTest](../src/test/java/com/ankurm/redis/StringTemplateTest.java)
|
||||
- [Spring Data Redis: RedisTemplate and StringRedisTemplate](https://docs.spring.io/spring-data/redis/reference/redis/template.html)
|
||||
|
||||
[Home](../README.md) | Prev: [Default serialization](02-default-serialization.md) | Next: [JSON serializers](04-json-serializers.md)
|
||||
@@ -0,0 +1,93 @@
|
||||
[Home](../README.md) | Prev: [String template and two keyspaces](03-string-template-and-two-keyspaces.md) | Next: [@RedisHash](05-redis-hash.md)
|
||||
|
||||
# 4. JSON serializers: readable values, Jackson 3
|
||||
|
||||
The usual fix for unreadable values is JSON. Spring Data Redis 4.1 has two families of JSON
|
||||
serializer, and the names are easy to mix up
|
||||
([16-serializers-javap.txt](output/16-serializers-javap.txt), read from the jar):
|
||||
|
||||
| Class | Jackson | Status |
|
||||
|---|---|---|
|
||||
| `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 |
|
||||
|
||||
`Jackson` in the name means Jackson 3. `Jackson2` means Jackson 2, and it is what most examples on the
|
||||
web still show. The Redis starter includes neither ([chapter 1](01-what-boot-gives-you.md)); add
|
||||
`spring-boot-starter-jackson` for Jackson 3.
|
||||
|
||||
## Typed: JacksonJsonRedisSerializer<T>
|
||||
|
||||
One value type per template, plain JSON, nothing extra
|
||||
([06-jackson-serializers.txt](output/06-jackson-serializers.txt)):
|
||||
|
||||
```
|
||||
$ redis-cli -p 6390 GET user:1
|
||||
{"id":1,"name":"Ankur"}
|
||||
```
|
||||
|
||||
The constructors are `(Class)`, `(JavaType)`, `(ObjectMapper, Class)` and so on. The cost is one
|
||||
template bean per value type. [userRedisTemplate](../src/main/java/com/ankurm/redis/config/JsonRedisConfig.java) is the example.
|
||||
|
||||
## Generic: GenericJacksonJsonRedisSerializer
|
||||
|
||||
One template for any value type. To read a value back as the right class it has to know the class, so
|
||||
it needs *default typing*, which writes a type property into every value:
|
||||
|
||||
```
|
||||
$ redis-cli -p 6390 GET user:2
|
||||
{"@class":"com.ankurm.redis.model.User","id":2,"name":"Ankur"}
|
||||
```
|
||||
|
||||
Without typing configured (`GenericJacksonJsonRedisSerializer.builder().build()`), the JSON has no
|
||||
`@class`, and reading it back returns a `LinkedHashMap`, not a `User`:
|
||||
|
||||
```
|
||||
read back: {id=3, name=Ankur} (LinkedHashMap)
|
||||
```
|
||||
|
||||
The builder offers `enableDefaultTyping(PolymorphicTypeValidator)`, `enableUnsafeDefaultTyping()`,
|
||||
`typeValidator(...)`, `typePropertyName(...)`, `customize(Consumer)` and more.
|
||||
|
||||
## The `@class` property is input
|
||||
|
||||
Whatever is in Redis chooses the class the serializer instantiates. Anyone who can write to that
|
||||
Redis (another service, an operator, an attacker with network access to an unauthenticated
|
||||
instance) can write `{"@class": ...}` for any class on your classpath. That is why the builder has a
|
||||
method called `enableUnsafeDefaultTyping` and why `enableDefaultTyping` takes a validator.
|
||||
[JsonRedisConfig](../src/main/java/com/ankurm/redis/config/JsonRedisConfig.java) allows only subtypes under `com.ankurm.redis.`, and
|
||||
[07-jackson-untrusted-class.txt](output/07-jackson-untrusted-class.txt) shows what happens to a class outside it:
|
||||
|
||||
```
|
||||
$ redis-cli -p 6390 SET user:evil '{"@class":"com.ankurm.other.Outsider","name":"x"}'
|
||||
OK
|
||||
...
|
||||
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
|
||||
```
|
||||
|
||||
The same transcript shows the other failure you will meet: a class inside the allow-list that no
|
||||
longer exists (renamed, moved) fails with `Cannot locate class`. Type names in Redis are a
|
||||
compatibility surface, exactly as the class name inside a JDK-serialised value was.
|
||||
|
||||
## Which one?
|
||||
|
||||
| | Typed | Generic |
|
||||
|---|---|---|
|
||||
| redis-cli output | plain JSON | JSON with `@class` |
|
||||
| Other languages can read it | yes | yes, if they ignore `@class` |
|
||||
| Rename or move the class | safe | breaks reads |
|
||||
| Needs an allow-list | no | **yes** |
|
||||
| Templates needed | one per type | one |
|
||||
|
||||
If a Redis is shared between services, prefer typed, or configure `typePropertyName` and an
|
||||
allow-list deliberately. If it is private to one application, generic with a validator is fine.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [JsonRedisConfig](../src/main/java/com/ankurm/redis/config/JsonRedisConfig.java)
|
||||
- [JacksonSerializerTest](../src/test/java/com/ankurm/redis/JacksonSerializerTest.java)
|
||||
- [Spring Data Redis: serializers](https://docs.spring.io/spring-data/redis/reference/redis/template.html#redis:serializer)
|
||||
- [Jackson 2 to Jackson 3 migration guide](https://ankurm.com/jackson-3-migration-guide/)
|
||||
|
||||
[Home](../README.md) | Prev: [String template and two keyspaces](03-string-template-and-two-keyspaces.md) | Next: [@RedisHash](05-redis-hash.md)
|
||||
@@ -0,0 +1,48 @@
|
||||
[Home](../README.md) | Prev: [JSON serializers](04-json-serializers.md) | Next: [Hash TTL and keyspace events](06-hash-ttl-and-keyspace-events.md)
|
||||
|
||||
# 5. @RedisHash: what a repository actually writes
|
||||
|
||||
`@RedisHash` turns a class into a Redis hash and lets you use a `CrudRepository`. It does not use
|
||||
`RedisTemplate`'s value serializer at all: it converts the entity to a map of field names and text
|
||||
values, so there is no JDK-serialization problem here. What surprises people is the number of keys.
|
||||
|
||||
[Person](../src/main/java/com/ankurm/redis/hash/Person.java) has `@Id`, one `@Indexed` field
|
||||
(`lastName`) and two plain fields. Saving two people
|
||||
([08-redis-hash.txt](output/08-redis-hash.txt)) creates six keys:
|
||||
|
||||
```
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
person
|
||||
person:1
|
||||
person:1:idx
|
||||
person:2
|
||||
person:2:idx
|
||||
person:lastName:Mhatre
|
||||
```
|
||||
|
||||
| Key | Type | Purpose |
|
||||
|---|---|---|
|
||||
| `person:1` | hash | The entity: `_class`, `age`, `firstName`, `id`, `lastName` |
|
||||
| `person` | set | Every id in the keyspace. `count()` reads this set (see [chapter 6](06-hash-ttl-and-keyspace-events.md)) |
|
||||
| `person:lastName:Mhatre` | set | The secondary index for the `@Indexed` field: ids with that value |
|
||||
| `person:1:idx` | set | For each entity, the index sets it is in, so a delete or update can clean them |
|
||||
|
||||
`findByLastName` works because of the index: it reads `person:lastName:Mhatre` and loads each hash.
|
||||
|
||||
The `_class` field in the hash records the Java type
|
||||
(`com.ankurm.redis.hash.Person`), so this is a type-name dependency like the ones in
|
||||
[chapter 4](04-json-serializers.md).
|
||||
|
||||
## Deleting and updating
|
||||
|
||||
`deleteById("1")` removes `person:1`, `person:1:idx`, the id from `person` and the id from
|
||||
`person:lastName:Mhatre`. The transcript's final section shows the survivors. The bookkeeping is done
|
||||
by Spring Data Redis in the application, not by Redis, which is why [chapter 6](06-hash-ttl-and-keyspace-events.md)
|
||||
matters: when Redis removes the hash on its own, nothing does that bookkeeping.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [RedisHashTest](../src/test/java/com/ankurm/redis/RedisHashTest.java)
|
||||
- [Spring Data Redis: Redis Repositories](https://docs.spring.io/spring-data/redis/reference/redis/redis-repositories/usage.html)
|
||||
|
||||
[Home](../README.md) | Prev: [JSON serializers](04-json-serializers.md) | Next: [Hash TTL and keyspace events](06-hash-ttl-and-keyspace-events.md)
|
||||
@@ -0,0 +1,71 @@
|
||||
[Home](../README.md) | Prev: [@RedisHash](05-redis-hash.md) | Next: [Pub/Sub](07-pub-sub.md)
|
||||
|
||||
# 6. Hash TTL and keyspace events
|
||||
|
||||
`@RedisHash(value = "session", timeToLive = 2)` gives every saved [Session](../src/main/java/com/ankurm/redis/hash/Session.java)
|
||||
a two-second TTL. Redis will delete the hash when the time is up. The question is what happens to
|
||||
everything Spring Data Redis wrote *around* the hash: the id in the `session` set, the
|
||||
`session:user:ankur` index, the `session:s1:idx` bookkeeping set (see [chapter 5](05-redis-hash.md)).
|
||||
|
||||
## With Boot's defaults: nothing
|
||||
|
||||
Boot enables Redis repositories with `enableKeyspaceEvents = OFF`
|
||||
([16-serializers-javap.txt](output/16-serializers-javap.txt), "Defaults of @EnableRedisRepositories").
|
||||
Redis expires the hash and nobody tells the application
|
||||
([09-hash-ttl-events-off.txt](output/09-hash-ttl-events-off.txt)):
|
||||
|
||||
```
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
session
|
||||
session:s1:idx
|
||||
session:user:ankur
|
||||
$ redis-cli -p 6390 SMEMBERS session | sort
|
||||
s1
|
||||
...
|
||||
sessions.findById("s1") = Optional.empty
|
||||
sessions.count() = 1
|
||||
```
|
||||
|
||||
`findById` is right (there is no hash), `count()` is wrong (the id is still in the set), and
|
||||
`session:user:ankur` still lists an id that resolves to nothing. On a busy system this accumulates.
|
||||
|
||||
## With ON_STARTUP: cleanup
|
||||
|
||||
`@EnableRedisRepositories(enableKeyspaceEvents = ON_STARTUP)` changes three things
|
||||
([10-hash-ttl-events-on.txt](output/10-hash-ttl-events-on.txt), test config in
|
||||
[HashTtlEventsOnTest](../src/test/java/com/ankurm/redis/HashTtlEventsOnTest.java)):
|
||||
|
||||
1. **The server setting.** Redis only publishes expiry notifications if `notify-keyspace-events` is set. It is empty by default, and Spring Data Redis sets it at startup (the annotation's `keyspaceNotificationsConfigParameter` defaults to `Ex`; Redis reports it back as `xE`):
|
||||
|
||||
```
|
||||
$ redis-cli -p 6390 --no-raw CONFIG GET notify-keyspace-events
|
||||
1) "notify-keyspace-events"
|
||||
2) "xE"
|
||||
```
|
||||
|
||||
Some managed Redis services restrict `CONFIG SET`; this repository does not run against one, so check yours. The `EnableKeyspaceEvents` enum also has an `ON_DEMAND` value, which is not exercised here.
|
||||
|
||||
2. **A phantom key.** Once a hash has expired there is nothing left to read, so Spring Data Redis writes a copy, `session:s1:phantom`, that outlives the hash by five minutes (302 seconds for a 2-second TTL). When the expiry notification arrives, Spring reads the phantom copy, removes the index entries, and deletes the phantom.
|
||||
|
||||
3. **An application event.** `RedisKeyExpiredEvent` is published with the deleted entity as its value, so an `@EventListener` can react (audit, cleanup, notify):
|
||||
|
||||
```
|
||||
RedisKeyExpiredEvent: keyspace=session id=s1 value=Session[id=s1, user=ankur]
|
||||
```
|
||||
|
||||
After it, `DBSIZE` is 0 and `count()` is 0.
|
||||
|
||||
## The catch
|
||||
|
||||
The expiry notification is delivered over Pub/Sub, which is fire-and-forget: a subscriber that is
|
||||
not connected when the message is published never sees it ([chapter 7](07-pub-sub.md) shows that
|
||||
with a stopped listener). If your application is down when a session expires, or the connection
|
||||
drops, the cleanup for that entry is not guaranteed. This repository does not test a missed notification. If exactness matters, do not build correctness on keyspace events; run a periodic job.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [HashTtlEventsOffTest](../src/test/java/com/ankurm/redis/HashTtlEventsOffTest.java)
|
||||
- [Redis keyspace notifications](https://redis.io/docs/latest/develop/pubsub/keyspace-notifications/)
|
||||
- [Spring Data Redis: Expirations and Time To Live](https://docs.spring.io/spring-data/redis/reference/redis/redis-repositories/expirations.html)
|
||||
|
||||
[Home](../README.md) | Prev: [@RedisHash](05-redis-hash.md) | Next: [Pub/Sub](07-pub-sub.md)
|
||||
@@ -0,0 +1,66 @@
|
||||
[Home](../README.md) | Prev: [Hash TTL and keyspace events](06-hash-ttl-and-keyspace-events.md) | Next: [TTL](08-ttl.md)
|
||||
|
||||
# 7. Pub/Sub
|
||||
|
||||
Redis Pub/Sub is a broadcast: `PUBLISH channel message` sends the message to whoever is subscribed
|
||||
to the channel *at that moment*, and Redis keeps nothing. The reply to `PUBLISH` is the number of
|
||||
subscribers that received it.
|
||||
|
||||
## Boot 4.1 sets up the receiving side
|
||||
|
||||
Older articles declare a `RedisMessageListenerContainer` bean by hand. Boot 4.1 registers one
|
||||
(`redisMessageListenerContainer`) and enables `@RedisListener`, so a listener is a method
|
||||
([NewsListener](../src/main/java/com/ankurm/redis/messaging/NewsListener.java)):
|
||||
|
||||
```java
|
||||
@RedisListener(topic = "news")
|
||||
void onNews(String body) { ... }
|
||||
```
|
||||
|
||||
[11-pub-sub.txt](output/11-pub-sub.txt) starts with what Boot did:
|
||||
|
||||
```
|
||||
beans of type RedisMessageListenerContainer: [redisMessageListenerContainer]
|
||||
running=true listening=true
|
||||
```
|
||||
|
||||
A topic that contains a glob (`alerts.*`) subscribes with `PSUBSCRIBE`; the channel the message came
|
||||
in on is available as a header (`@Header(PubSubHeaders.CHANNEL) String channel`). The
|
||||
`@RedisListener` attributes, from the jar, are `id`, `container`, `value`, `topic` and `consumes`.
|
||||
A `String` parameter receives the raw body; a `org.springframework.data.redis.connection.Message`
|
||||
parameter did not work in this test, because the adapter tried to parse the payload as JSON
|
||||
(a `StreamReadException` on the text `90% full`). This repository does not investigate why.
|
||||
|
||||
## Sending
|
||||
|
||||
`stringRedisTemplate.convertAndSend(channel, message)` returns the number of receivers:
|
||||
|
||||
- to a channel nobody listens to: `0`, and the message is gone;
|
||||
- to `news` with the annotation listener: `1`;
|
||||
- after a second listener is added to the same channel through the container, still `1`, because the container subscribes once to the channel and fans out to its listeners in-process. Both listeners received the message. The number is receivers on the Redis side, not listeners on yours.
|
||||
|
||||
`PUBSUB NUMSUB alerts.disk` is `0` while a pattern subscriber is receiving on `alerts.*`: `NUMSUB`
|
||||
counts channel subscribers only, and `PUBSUB NUMPAT` counts patterns. `PUBLISH` counts both.
|
||||
|
||||
## At-most-once
|
||||
|
||||
[12-pub-sub-lost-message.txt](output/12-pub-sub-lost-message.txt): stop the container, publish, start
|
||||
it again.
|
||||
|
||||
```
|
||||
template.convertAndSend("news", "while you were away") = 0 receivers
|
||||
...
|
||||
messages the listener has: []
|
||||
```
|
||||
|
||||
Nothing is queued. That is the design, and it is why Pub/Sub is right for cache invalidation hints and
|
||||
live dashboards, and wrong for anything that must not be lost. For that, look at Redis Streams
|
||||
(not covered in this repository).
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [PubSubTest](../src/test/java/com/ankurm/redis/PubSubTest.java)
|
||||
- [Redis Pub/Sub](https://redis.io/docs/latest/develop/pubsub/)
|
||||
- [Spring Data Redis: Pub/Sub messaging](https://docs.spring.io/spring-data/redis/reference/redis/pubsub.html)
|
||||
|
||||
[Home](../README.md) | Prev: [Hash TTL and keyspace events](06-hash-ttl-and-keyspace-events.md) | Next: [TTL](08-ttl.md)
|
||||
@@ -0,0 +1,54 @@
|
||||
[Home](../README.md) | Prev: [Pub/Sub](07-pub-sub.md) | Next: [Redis as the Spring cache](09-redis-as-cache.md)
|
||||
|
||||
# 8. TTL: setting it, reading it, and losing it
|
||||
|
||||
[13-ttl.txt](output/13-ttl.txt) drives `StringRedisTemplate` and checks the server with `redis-cli`.
|
||||
|
||||
## Reading a TTL
|
||||
|
||||
`getExpire(key)` returns seconds, with two special values that are easy to misread as errors:
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `-1` | The key exists and has no TTL |
|
||||
| `-2` | The key does not exist |
|
||||
|
||||
Redis rounds `TTL` to the nearest second (a fresh 10-minute TTL reads `600`). The template's
|
||||
`getExpire(key, TimeUnit)` converts, and the conversion **truncates**: the same key, read a moment
|
||||
later, gave `600` seconds and `9` minutes.
|
||||
|
||||
## Setting one
|
||||
|
||||
- `opsForValue().set(key, value, Duration)` sets value and TTL in one command, so there is no window in which the key exists without a TTL.
|
||||
- `Duration.ofMillis(1500)` is honoured (`getExpire(key, MILLISECONDS)` is at most 1500); `getExpire(key)` in seconds reads `1`.
|
||||
- `expire(key, Duration)` sets a TTL on an existing key; `persist(key)` removes it.
|
||||
|
||||
## The trap: a plain `set` throws the TTL away
|
||||
|
||||
```
|
||||
set("session", "v1", Duration.ofMinutes(10)) -> TTL 600
|
||||
set("session", "v2") -> TTL -1
|
||||
```
|
||||
|
||||
Overwriting a key with a plain `SET` clears its TTL. If you refresh a cached value, pass the TTL again, or
|
||||
overwrite with `Expiration.keepTtl()` (the `KEEPTTL` option), which the transcript shows leaving the TTL at 600.
|
||||
|
||||
## setIfAbsent with a TTL
|
||||
|
||||
`setIfAbsent("lock:job", "worker-1", Duration.ofSeconds(30))` is a `SET` with the `NX` option and a 30-second expiry: the first caller gets
|
||||
`true`, the second `false`, and the key disappears by itself if the holder dies. It is the smallest useful
|
||||
lock. It is not a complete distributed-lock recipe (no owner-checked release, no fencing), and this repository does
|
||||
not test one.
|
||||
|
||||
## Expiry is real
|
||||
|
||||
The transcript sets a 300 ms TTL, reads the value, waits until `get` returns `null`, and confirms with
|
||||
`EXISTS blink` returning `0`.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [TtlTest](../src/test/java/com/ankurm/redis/TtlTest.java)
|
||||
- [Redis EXPIRE](https://redis.io/docs/latest/commands/expire/) and [SET](https://redis.io/docs/latest/commands/set/)
|
||||
- [Chapter 6](06-hash-ttl-and-keyspace-events.md) for TTL on `@RedisHash` entities
|
||||
|
||||
[Home](../README.md) | Prev: [Pub/Sub](07-pub-sub.md) | Next: [Redis as the Spring cache](09-redis-as-cache.md)
|
||||
@@ -0,0 +1,63 @@
|
||||
[Home](../README.md) | Prev: [TTL](08-ttl.md) | Next: [Production checklist](10-production-checklist.md)
|
||||
|
||||
# 9. Redis as the Spring cache
|
||||
|
||||
The annotations (`@Cacheable`, `@CacheEvict`, keys, the self-invocation trap) are covered in
|
||||
[The Spring Cache Abstraction](https://ankurm.com/spring-cache-abstraction-cacheable-cacheevict-self-invocation-trap/) and its
|
||||
[companion project](../../caching). This chapter is only about what changes when the cache is Redis.
|
||||
|
||||
With `spring-boot-starter-cache` and the Redis starter on the classpath and `@EnableCaching`, Boot
|
||||
picks `org.springframework.data.redis.cache.RedisCacheManager`
|
||||
([14-cache-default.txt](output/14-cache-default.txt)). [UserService](../src/main/java/com/ankurm/redis/cache/UserService.java)
|
||||
has two cached methods that differ only in whether the result is `Serializable`.
|
||||
|
||||
## What the defaults store
|
||||
|
||||
Keys are readable: `<cacheName>::<key>`, here `legacy::1`. The value is JDK-serialised, exactly as in
|
||||
[chapter 2](02-default-serialization.md):
|
||||
|
||||
```
|
||||
$ redis-cli -p 6390 --no-raw GET legacy::1
|
||||
"\xac\xed\x00\x05sr\x00'com.ankurm.redis.model.SerializableUser..."
|
||||
```
|
||||
|
||||
`spring.cache.redis.time-to-live=10m` works: `TTL legacy::1` is `600`.
|
||||
|
||||
A result that is **not** `Serializable` is not cached, and the *method call fails*:
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
The body ran (twice in total: once for the good call, once for the failing one), the write to Redis threw, and
|
||||
the caller sees the exception. A `@Cacheable` on a method returning a non-Serializable type breaks that method.
|
||||
|
||||
## Switching to JSON
|
||||
|
||||
Declare a `RedisCacheConfiguration` bean. [JsonCacheConfig](../src/main/java/com/ankurm/redis/config/JsonCacheConfig.java)
|
||||
(active under the `json-cache` profile) does it with the same allow-listed Jackson 3 serializer as
|
||||
[chapter 4](04-json-serializers.md) ([15-cache-json.txt](output/15-cache-json.txt)):
|
||||
|
||||
```
|
||||
$ redis-cli -p 6390 GET users::1
|
||||
{"@class":"com.ankurm.redis.model.User","id":1,"name":"Ankur"}
|
||||
```
|
||||
|
||||
The non-Serializable record now caches, and the method body ran once for two calls.
|
||||
|
||||
## The property that stops applying
|
||||
|
||||
The test sets `spring.cache.redis.time-to-live=10m` **and** declares a bean with
|
||||
`entryTtl(Duration.ofMinutes(5))`. The key's TTL is `300`. When you define your own
|
||||
`RedisCacheConfiguration`, Boot used that bean and the `spring.cache.redis.time-to-live` property had no effect
|
||||
on the TTL. Put the TTL in the bean.
|
||||
|
||||
## Going deeper
|
||||
|
||||
- [RedisCacheDefaultTest](../src/test/java/com/ankurm/redis/RedisCacheDefaultTest.java) and [RedisCacheJsonTest](../src/test/java/com/ankurm/redis/RedisCacheJsonTest.java)
|
||||
- [Spring Boot: Caching, Redis](https://docs.spring.io/spring-boot/reference/io/caching.html#io.caching.provider.redis)
|
||||
- [Spring Data Redis: Redis Cache](https://docs.spring.io/spring-data/redis/reference/redis/redis-cache.html)
|
||||
|
||||
[Home](../README.md) | Prev: [TTL](08-ttl.md) | Next: [Production checklist](10-production-checklist.md)
|
||||
@@ -0,0 +1,34 @@
|
||||
[Home](../README.md) | Prev: [Redis as the Spring cache](09-redis-as-cache.md)
|
||||
|
||||
# 10. Production checklist
|
||||
|
||||
Each line points at the chapter where the reason was demonstrated. Items marked *not run here* are
|
||||
general advice this repository does not exercise.
|
||||
|
||||
**Serialization**
|
||||
|
||||
- [ ] Keys go through `StringRedisSerializer`, in every template, in every service that shares the Redis ([chapters 2 and 3](03-string-template-and-two-keyspaces.md)).
|
||||
- [ ] Values are JSON with a serializer you chose, not the JDK default ([chapter 4](04-json-serializers.md)).
|
||||
- [ ] If you use the generic Jackson serializer, it has a `PolymorphicTypeValidator` allow-list, not `enableUnsafeDefaultTyping()` ([chapter 4](04-json-serializers.md)).
|
||||
- [ ] You are on `GenericJacksonJsonRedisSerializer` / `JacksonJsonRedisSerializer`, not the deprecated `Jackson2` classes ([chapter 4](04-json-serializers.md)).
|
||||
- [ ] Changing a serializer on a Redis that already has data has a migration or a new key prefix (*not run here*).
|
||||
|
||||
**Expiry**
|
||||
|
||||
- [ ] Every cache and session key has a TTL, and refreshes re-supply it ([chapter 8](08-ttl.md)).
|
||||
- [ ] `@RedisHash` entities with `timeToLive` either have keyspace events on or you accept the leftover index entries ([chapter 6](06-hash-ttl-and-keyspace-events.md)).
|
||||
- [ ] Nothing correctness-critical depends on receiving an expiry event ([chapters 6 and 7](07-pub-sub.md)).
|
||||
- [ ] With a custom `RedisCacheConfiguration`, the TTL is in the bean ([chapter 9](09-redis-as-cache.md)).
|
||||
|
||||
**Messaging**
|
||||
|
||||
- [ ] Pub/Sub is used only for messages you can afford to lose ([chapter 7](07-pub-sub.md)).
|
||||
|
||||
**Operations** (*not run here*)
|
||||
|
||||
- [ ] `KEYS *` is fine in these transcripts because the keyspace has a handful of keys. On a real server it is O(N) and blocks it; use `SCAN`.
|
||||
- [ ] Redis requires authentication and is not reachable from the internet. Anyone who can write to it can choose the class your generic serializer instantiates ([chapter 4](04-json-serializers.md)).
|
||||
- [ ] Connection and command timeouts are set deliberately, not left at defaults.
|
||||
- [ ] Do not point these tests at a production Redis: [LocalRedis](../src/test/java/com/ankurm/redis/LocalRedis.java) exists to stop that, because the tests call `FLUSHALL`.
|
||||
|
||||
[Home](../README.md) | Prev: [Redis as the Spring cache](09-redis-as-cache.md)
|
||||
@@ -0,0 +1,20 @@
|
||||
# The RedisTemplate Boot gives you: JDK serialisation for keys and values
|
||||
|
||||
|
||||
--- Which serializers is it using? ---
|
||||
key serializer: JdkSerializationRedisSerializer
|
||||
value serializer: JdkSerializationRedisSerializer
|
||||
|
||||
--- Write a String under the key user:1 ---
|
||||
redisTemplate.opsForValue().set("user:1", "Ankur")
|
||||
|
||||
--- What redis-cli sees ---
|
||||
$ redis-cli -p 6390 --no-raw KEYS '*'
|
||||
1) "\xac\xed\x00\x05t\x00\x06user:1"
|
||||
$ redis-cli -p 6390 --no-raw GET user:1
|
||||
(nil)
|
||||
$ redis-cli -p 6390 --no-raw EVAL 'return redis.call("GET", redis.call("KEYS", "*")[1])' 0
|
||||
"\xac\xed\x00\x05t\x00\x05Ankur"
|
||||
|
||||
--- What Java sees ---
|
||||
redisTemplate.opsForValue().get("user:1") = Ankur
|
||||
@@ -0,0 +1,16 @@
|
||||
# The default template refuses a value that is not java.io.Serializable
|
||||
|
||||
|
||||
--- A record that does not implement Serializable ---
|
||||
redisTemplate.opsForValue().set("user:2", new User(2, "Ankur"))
|
||||
org.springframework.data.redis.serializer.SerializationException
|
||||
message: Cannot serialize
|
||||
cause: Failed to serialize object using DefaultSerializer
|
||||
root: DefaultSerializer requires a Serializable payload but received an object of type [com.ankurm.redis.model.User]
|
||||
keys afterwards: 0
|
||||
|
||||
--- The same shape, implementing Serializable ---
|
||||
redisTemplate.opsForValue().set("user:3", new SerializableUser(3, "Ankur"))
|
||||
$ redis-cli -p 6390 --no-raw EVAL 'return redis.call("GET", redis.call("KEYS", "*")[1])' 0
|
||||
"\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\x03t\x00\x05Ankur"
|
||||
read back: SerializableUser[id=3, name=Ankur]
|
||||
@@ -0,0 +1,17 @@
|
||||
# INCR works with the default template, but the template cannot read the counter it made
|
||||
|
||||
|
||||
--- Increment a counter ---
|
||||
redisTemplate.opsForValue().increment("hits") = 1
|
||||
|
||||
--- What redis-cli sees ---
|
||||
$ redis-cli -p 6390 --no-raw KEYS '*'
|
||||
1) "\xac\xed\x00\x05t\x00\x04hits"
|
||||
$ redis-cli -p 6390 --no-raw EVAL 'return redis.call("GET", redis.call("KEYS", "*")[1])' 0
|
||||
"1"
|
||||
|
||||
--- Read it back through the same template ---
|
||||
redisTemplate.opsForValue().get("hits")
|
||||
org.springframework.data.redis.serializer.SerializationException
|
||||
message: Cannot deserialize
|
||||
root: java.io.EOFException: null
|
||||
@@ -0,0 +1,45 @@
|
||||
# StringRedisTemplate: every key and value is UTF-8 text
|
||||
|
||||
|
||||
--- Which serializers is it using? ---
|
||||
key serializer: StringRedisSerializer
|
||||
value serializer: StringRedisSerializer
|
||||
hash key serializer: StringRedisSerializer
|
||||
hash value serializer: StringRedisSerializer
|
||||
|
||||
--- A string value ---
|
||||
stringTemplate.opsForValue().set("user:1", "Ankur")
|
||||
$ redis-cli -p 6390 --no-raw GET user:1
|
||||
"Ankur"
|
||||
|
||||
--- A counter ---
|
||||
stringTemplate.opsForValue().increment("hits") twice = 2
|
||||
$ redis-cli -p 6390 --no-raw GET hits
|
||||
"2"
|
||||
|
||||
--- A hash ---
|
||||
stringTemplate.opsForHash().put("user:1:profile", "city", "Pune") and ("editor", "vim")
|
||||
$ redis-cli -p 6390 --no-raw HGETALL user:1:profile
|
||||
1) "city"
|
||||
2) "Pune"
|
||||
3) "editor"
|
||||
4) "vim"
|
||||
|
||||
--- A list, a set and a sorted set ---
|
||||
$ redis-cli -p 6390 --no-raw LRANGE queue 0 -1
|
||||
1) "a"
|
||||
2) "b"
|
||||
3) "c"
|
||||
$ redis-cli -p 6390 SMEMBERS tags | sort
|
||||
java
|
||||
spring
|
||||
$ redis-cli -p 6390 --no-raw ZRANGE scores 0 -1 WITHSCORES
|
||||
1) "ankur"
|
||||
2) "42"
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
hits
|
||||
queue
|
||||
scores
|
||||
tags
|
||||
user:1
|
||||
user:1:profile
|
||||
@@ -0,0 +1,16 @@
|
||||
# Same key name, two templates, two different keys
|
||||
|
||||
|
||||
--- Write user:1 with StringRedisTemplate ---
|
||||
stringTemplate.opsForValue().get("user:1") = from-string-template
|
||||
redisTemplate.opsForValue().get("user:1") = null
|
||||
|
||||
--- Write user:1 with the default RedisTemplate ---
|
||||
stringTemplate.opsForValue().get("user:1") = from-string-template
|
||||
redisTemplate.opsForValue().get("user:1") = from-default-template
|
||||
|
||||
--- What Redis holds ---
|
||||
$ redis-cli -p 6390 --no-raw DBSIZE
|
||||
(integer) 2
|
||||
$ redis-cli -p 6390 --no-raw GET user:1
|
||||
"from-string-template"
|
||||
@@ -0,0 +1,30 @@
|
||||
# Jackson 3 serializers: JacksonJsonRedisSerializer and GenericJacksonJsonRedisSerializer
|
||||
|
||||
|
||||
--- JacksonJsonRedisSerializer<User>: one type, plain JSON ---
|
||||
value serializer: org.springframework.data.redis.serializer.JacksonJsonRedisSerializer
|
||||
userTemplate.opsForValue().set("user:1", new User(1, "Ankur"))
|
||||
$ redis-cli -p 6390 KEYS '*'
|
||||
user:1
|
||||
$ redis-cli -p 6390 GET user:1
|
||||
{"id":1,"name":"Ankur"}
|
||||
read back: User[id=1, name=Ankur] (User)
|
||||
|
||||
--- GenericJacksonJsonRedisSerializer with an allow-list: any type, plus @class ---
|
||||
value serializer: org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer
|
||||
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"}
|
||||
read back: User[id=2, name=Ankur] (User)
|
||||
|
||||
--- GenericJacksonJsonRedisSerializer with no typing configured ---
|
||||
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)
|
||||
|
||||
--- Two different shapes of the same JSON ---
|
||||
$ redis-cli -p 6390 GET user:1
|
||||
{"id":1,"name":"Ankur"}
|
||||
$ redis-cli -p 6390 GET user:2
|
||||
{"@class":"com.ankurm.redis.model.User","id":2,"name":"Ankur"}
|
||||
@@ -0,0 +1,16 @@
|
||||
# The @class property is input: the allow-list decides what it may name
|
||||
|
||||
|
||||
--- A class outside the allow-list ---
|
||||
$ redis-cli -p 6390 SET user:evil '{"@class":"com.ankurm.other.Outsider","name":"x"}'
|
||||
OK
|
||||
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
|
||||
|
||||
--- A class inside the allow-list that no longer exists ---
|
||||
$ redis-cli -p 6390 SET user:gone '{"@class":"com.ankurm.redis.model.Gone","id":9}'
|
||||
OK
|
||||
jsonTemplate.opsForValue().get("user:gone")
|
||||
org.springframework.data.redis.serializer.SerializationException
|
||||
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
|
||||
@@ -0,0 +1,61 @@
|
||||
# @RedisHash and CrudRepository: the keys Spring Data Redis creates
|
||||
|
||||
|
||||
--- Save two people ---
|
||||
people.save(new Person("1", "Ankur", "Mhatre", 40))
|
||||
people.save(new Person("2", "Asha", "Mhatre", 38))
|
||||
|
||||
--- Every key it created ---
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
person
|
||||
person:1
|
||||
person:1:idx
|
||||
person:2
|
||||
person:2:idx
|
||||
person:lastName:Mhatre
|
||||
|
||||
--- The entity is a hash ---
|
||||
$ redis-cli -p 6390 --no-raw TYPE person:1
|
||||
hash
|
||||
$ redis-cli -p 6390 --no-raw HGETALL person:1
|
||||
1) "_class"
|
||||
2) "com.ankurm.redis.hash.Person"
|
||||
3) "age"
|
||||
4) "40"
|
||||
5) "firstName"
|
||||
6) "Ankur"
|
||||
7) "id"
|
||||
8) "1"
|
||||
9) "lastName"
|
||||
10) "Mhatre"
|
||||
|
||||
--- The keyspace is a set of ids ---
|
||||
$ redis-cli -p 6390 SMEMBERS person | sort
|
||||
1
|
||||
2
|
||||
|
||||
--- The @Indexed field is a set per value ---
|
||||
$ redis-cli -p 6390 SMEMBERS person:lastName:Mhatre | sort
|
||||
1
|
||||
2
|
||||
|
||||
--- And each entity remembers which index sets it is in ---
|
||||
$ redis-cli -p 6390 SMEMBERS person:1:idx | sort
|
||||
person:lastName:Mhatre
|
||||
|
||||
--- What the repository returns ---
|
||||
people.findById("1") = Person[id=1, Ankur Mhatre, age=40]
|
||||
people.count() = 2
|
||||
people.findByLastName("Mhatre") = 2 results
|
||||
|
||||
--- Delete one, and look again ---
|
||||
people.deleteById("1")
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
person
|
||||
person:2
|
||||
person:2:idx
|
||||
person:lastName:Mhatre
|
||||
$ redis-cli -p 6390 SMEMBERS person:lastName:Mhatre | sort
|
||||
2
|
||||
$ redis-cli -p 6390 SMEMBERS person | sort
|
||||
2
|
||||
@@ -0,0 +1,35 @@
|
||||
# @RedisHash(timeToLive = 2) with Boot's defaults (keyspace events OFF)
|
||||
|
||||
|
||||
--- Server setting ---
|
||||
$ redis-cli -p 6390 --no-raw CONFIG GET notify-keyspace-events
|
||||
1) "notify-keyspace-events"
|
||||
2) ""
|
||||
|
||||
--- Save a session that lives for two seconds ---
|
||||
sessions.save(new Session("s1", "ankur"))
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
session
|
||||
session:s1
|
||||
session:s1:idx
|
||||
session:user:ankur
|
||||
$ redis-cli -p 6390 --no-raw TTL session:s1
|
||||
(integer) 2
|
||||
|
||||
--- Wait for Redis to expire it ---
|
||||
$ redis-cli -p 6390 --no-raw EXISTS session:s1
|
||||
(integer) 0
|
||||
|
||||
--- What is left behind ---
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
session
|
||||
session:s1:idx
|
||||
session:user:ankur
|
||||
$ redis-cli -p 6390 SMEMBERS session | sort
|
||||
s1
|
||||
$ redis-cli -p 6390 SMEMBERS session:user:ankur | sort
|
||||
s1
|
||||
|
||||
--- What the repository says ---
|
||||
sessions.findById("s1") = Optional.empty
|
||||
sessions.count() = 1
|
||||
@@ -0,0 +1,33 @@
|
||||
# @RedisHash(timeToLive = 2) with enableKeyspaceEvents = ON_STARTUP
|
||||
|
||||
|
||||
--- Server setting before the application context started ---
|
||||
$ redis-cli -p 6390 --no-raw CONFIG GET notify-keyspace-events
|
||||
1) "notify-keyspace-events"
|
||||
2) ""
|
||||
|
||||
--- Server setting now ---
|
||||
$ redis-cli -p 6390 --no-raw CONFIG GET notify-keyspace-events
|
||||
1) "notify-keyspace-events"
|
||||
2) "xE"
|
||||
|
||||
--- Save a session that lives for two seconds ---
|
||||
sessions.save(new Session("s1", "ankur"))
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
session
|
||||
session:s1
|
||||
session:s1:idx
|
||||
session:s1:phantom
|
||||
session:user:ankur
|
||||
$ redis-cli -p 6390 --no-raw TTL session:s1
|
||||
(integer) 2
|
||||
$ redis-cli -p 6390 --no-raw TTL session:s1:phantom
|
||||
(integer) 302
|
||||
|
||||
--- Wait for Redis to expire it, and for Spring to react ---
|
||||
RedisKeyExpiredEvent: keyspace=session id=s1 value=Session[id=s1, user=ankur]
|
||||
|
||||
--- What is left behind ---
|
||||
$ redis-cli -p 6390 --no-raw DBSIZE
|
||||
(integer) 0
|
||||
sessions.count() = 0
|
||||
@@ -0,0 +1,38 @@
|
||||
# Pub/Sub: what is delivered, to whom, and what is lost
|
||||
|
||||
|
||||
--- The container Boot created ---
|
||||
beans of type RedisMessageListenerContainer: [redisMessageListenerContainer]
|
||||
running=true listening=true
|
||||
|
||||
--- Publishing to a channel nobody listens to ---
|
||||
template.convertAndSend("quiet", "hello?") = 0 receivers
|
||||
$ redis-cli -p 6390 --no-raw PUBSUB NUMSUB quiet
|
||||
1) "quiet"
|
||||
2) (integer) 0
|
||||
|
||||
--- Publishing to news, which has an @RedisListener ---
|
||||
$ redis-cli -p 6390 --no-raw PUBSUB NUMSUB news
|
||||
1) "news"
|
||||
2) (integer) 1
|
||||
template.convertAndSend("news", "first") = 1 receiver
|
||||
@RedisListener got 'first'
|
||||
|
||||
--- A second listener that subscribes late ---
|
||||
container.addMessageListener(late, new ChannelTopic("news"))
|
||||
$ redis-cli -p 6390 --no-raw PUBSUB NUMSUB news
|
||||
1) "news"
|
||||
2) (integer) 1
|
||||
template.convertAndSend("news", "second") = 1 receiver
|
||||
@RedisListener got 'first'
|
||||
@RedisListener got 'second'
|
||||
late listener got 'second' on news
|
||||
|
||||
--- A pattern subscription (topic = "alerts.*") ---
|
||||
$ redis-cli -p 6390 --no-raw PUBSUB NUMPAT
|
||||
(integer) 1
|
||||
$ redis-cli -p 6390 --no-raw PUBSUB NUMSUB alerts.disk
|
||||
1) "alerts.disk"
|
||||
2) (integer) 0
|
||||
template.convertAndSend("alerts.disk", "90% full") = 1 receiver
|
||||
@RedisListener got '90% full' on alerts.disk
|
||||
@@ -0,0 +1,20 @@
|
||||
# Pub/Sub is at-most-once: a stopped listener loses messages
|
||||
|
||||
|
||||
--- Stop the listener container, then publish ---
|
||||
container.stop()
|
||||
$ redis-cli -p 6390 --no-raw PUBSUB NUMSUB news
|
||||
1) "news"
|
||||
2) (integer) 0
|
||||
template.convertAndSend("news", "while you were away") = 0 receivers
|
||||
|
||||
--- Start it again ---
|
||||
container.start()
|
||||
$ redis-cli -p 6390 --no-raw PUBSUB NUMSUB news
|
||||
1) "news"
|
||||
2) (integer) 1
|
||||
messages the listener has: []
|
||||
|
||||
--- Once it is back, messages flow again ---
|
||||
template.convertAndSend("news", "welcome back") = 1 receiver
|
||||
@RedisListener got 'welcome back'
|
||||
@@ -0,0 +1,51 @@
|
||||
# TTL: set, read, lose, keep and clear it
|
||||
|
||||
|
||||
--- No TTL, and a key that does not exist ---
|
||||
set("plain", "v")
|
||||
getExpire("plain") = -1
|
||||
getExpire("missing") = -2
|
||||
|
||||
--- Set a value with a Duration ---
|
||||
set("session", "v1", Duration.ofMinutes(10))
|
||||
getExpire("session") = 600
|
||||
getExpire("session", TimeUnit.MINUTES) = 9
|
||||
$ redis-cli -p 6390 --no-raw TTL session
|
||||
(integer) 600
|
||||
|
||||
--- A plain set() afterwards throws the TTL away ---
|
||||
set("session", "v2")
|
||||
getExpire("session") = -1
|
||||
$ redis-cli -p 6390 --no-raw TTL session
|
||||
(integer) -1
|
||||
|
||||
--- Put a TTL back, then overwrite while keeping it ---
|
||||
expire("session", Duration.ofMinutes(10))
|
||||
set("session", "v3", Expiration.keepTtl())
|
||||
getExpire("session") = 600, value = v3
|
||||
$ redis-cli -p 6390 --no-raw TTL session
|
||||
(integer) 600
|
||||
|
||||
--- Sub-second precision ---
|
||||
set("short", "v", Duration.ofMillis(1500))
|
||||
getExpire("short", TimeUnit.MILLISECONDS) <= 1500: true
|
||||
getExpire("short") in seconds = 1
|
||||
|
||||
--- persist() removes a TTL ---
|
||||
persist("session")
|
||||
getExpire("session") = -1
|
||||
|
||||
--- Expiry actually happens ---
|
||||
set("blink", "v", Duration.ofMillis(300))
|
||||
get("blink") straight away = v
|
||||
get("blink") later = null
|
||||
$ redis-cli -p 6390 --no-raw EXISTS blink
|
||||
(integer) 0
|
||||
|
||||
--- setIfAbsent with a TTL is the smallest lock ---
|
||||
setIfAbsent("lock:job", "worker-1", 30s) = true
|
||||
setIfAbsent("lock:job", "worker-2", 30s) = false
|
||||
$ redis-cli -p 6390 --no-raw GET lock:job
|
||||
"worker-1"
|
||||
$ redis-cli -p 6390 --no-raw TTL lock:job
|
||||
(integer) 30
|
||||
@@ -0,0 +1,23 @@
|
||||
# Redis as the Spring cache: what the defaults store
|
||||
|
||||
|
||||
--- Which cache manager did Boot pick? ---
|
||||
org.springframework.data.redis.cache.RedisCacheManager
|
||||
|
||||
--- A Serializable result, cached ---
|
||||
findSerializable(1) called twice, method body ran 1 time(s)
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
legacy::1
|
||||
$ 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"
|
||||
|
||||
--- A result that is not Serializable ---
|
||||
users.find(1)
|
||||
java.lang.IllegalStateException
|
||||
message: Cannot serialize value of type com.ankurm.redis.model.User without a serializer
|
||||
root: Cannot serialize value of type com.ankurm.redis.model.User without a serializer
|
||||
method body ran 2 time(s) in total
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
legacy::1
|
||||
@@ -0,0 +1,15 @@
|
||||
# Redis as the Spring cache with a Jackson 3 value serializer
|
||||
|
||||
|
||||
--- The result that was not Serializable now caches ---
|
||||
find(1) called twice, method body ran 1 time(s)
|
||||
second call returned: User[id=1, name=Ankur]
|
||||
|
||||
--- What is in Redis ---
|
||||
$ redis-cli -p 6390 KEYS '*' | sort
|
||||
users::1
|
||||
$ 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
|
||||
spring.cache.redis.time-to-live=10m is set, and the bean says 5 minutes
|
||||
@@ -0,0 +1,89 @@
|
||||
# Serializer classes in spring-data-redis-4.1.1.jar, read from the jar
|
||||
|
||||
--- What ships in org/springframework/data/redis/serializer ---
|
||||
$ unzip -l spring-data-redis-*.jar | grep serializer/ (top-level classes, names only)
|
||||
ByteArrayRedisSerializer
|
||||
GenericJackson2JsonRedisSerializer
|
||||
GenericJacksonJsonRedisSerializer
|
||||
GenericToStringSerializer
|
||||
Jackson2JsonRedisSerializer
|
||||
JacksonJsonRedisSerializer
|
||||
JdkSerializationRedisSerializer
|
||||
OxmSerializer
|
||||
RedisSerializer
|
||||
StringRedisSerializer
|
||||
|
||||
--- Which of them are deprecated ---
|
||||
$ 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.Jackson2JsonRedisSerializer | grep -E 'Deprecated|since'
|
||||
#125 = Utf8 Deprecated
|
||||
#127 = Utf8 Ljava/lang/Deprecated;
|
||||
#128 = Utf8 since
|
||||
Deprecated: true
|
||||
java.lang.Deprecated(
|
||||
since="3.0"
|
||||
Deprecated: true
|
||||
java.lang.Deprecated(
|
||||
since="3.0"
|
||||
Deprecated: true
|
||||
java.lang.Deprecated(
|
||||
since="4.0"
|
||||
(end of output for Jackson2JsonRedisSerializer)
|
||||
$ javap -v -cp spring-data-redis-*.jar org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer | grep -E 'Deprecated|since'
|
||||
(end of output for GenericJacksonJsonRedisSerializer)
|
||||
$ javap -v -cp spring-data-redis-*.jar org.springframework.data.redis.serializer.JacksonJsonRedisSerializer | grep -E 'Deprecated|since'
|
||||
(end of output for JacksonJsonRedisSerializer)
|
||||
|
||||
--- Jackson 3: what the constructors and factories accept ---
|
||||
$ 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);
|
||||
$ javap -public -cp <classpath> org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer | grep -E 'GenericJacksonJsonRedisSerializer\(|create|builder'
|
||||
public org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer(tools.jackson.databind.ObjectMapper);
|
||||
public static org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer create(java.util.function.Consumer<org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer$GenericJacksonJsonRedisSerializerBuilder<tools.jackson.databind.json.JsonMapper$Builder>>);
|
||||
public static org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer$GenericJacksonJsonRedisSerializerBuilder<tools.jackson.databind.json.JsonMapper$Builder> builder();
|
||||
public static <B extends tools.jackson.databind.cfg.MapperBuilder<? extends tools.jackson.databind.ObjectMapper, ? extends tools.jackson.databind.cfg.MapperBuilder<?, ?>>> org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer$GenericJacksonJsonRedisSerializerBuilder<B> builder(java.util.function.Supplier<B>);
|
||||
|
||||
--- Jackson 3: how the generic serializer's builder turns typing on ---
|
||||
$ javap -public -cp <classpath> 'org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer$GenericJacksonJsonRedisSerializerBuilder' | grep -E 'Typing|typeValidator|typePropertyName'
|
||||
public org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer$GenericJacksonJsonRedisSerializerBuilder<B> enableUnsafeDefaultTyping();
|
||||
public org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer$GenericJacksonJsonRedisSerializerBuilder<B> enableDefaultTyping(tools.jackson.databind.jsontype.PolymorphicTypeValidator);
|
||||
public org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer$GenericJacksonJsonRedisSerializerBuilder<B> typeValidator(tools.jackson.databind.jsontype.PolymorphicTypeValidator);
|
||||
public org.springframework.data.redis.serializer.GenericJacksonJsonRedisSerializer$GenericJacksonJsonRedisSerializerBuilder<B> typePropertyName(java.lang.String);
|
||||
|
||||
--- What Boot 4.1 registers ---
|
||||
$ javap -p -cp spring-boot-data-redis-*.jar org.springframework.boot.data.redis.autoconfigure.DataRedisAutoConfiguration | grep -E 'redisTemplate|stringRedisTemplate'
|
||||
org.springframework.data.redis.core.RedisTemplate<java.lang.Object, java.lang.Object> redisTemplate(org.springframework.data.redis.connection.RedisConnectionFactory);
|
||||
org.springframework.data.redis.core.StringRedisTemplate stringRedisTemplate(org.springframework.data.redis.connection.RedisConnectionFactory);
|
||||
$ javap -p -cp spring-boot-data-redis-*.jar org.springframework.boot.data.redis.autoconfigure.DataRedisAnnotationDrivenConfiguration | grep 'redisMessageListenerContainer'
|
||||
org.springframework.boot.data.redis.autoconfigure.RedisMessageListenerContainerConfigurer redisMessageListenerContainerConfigurer();
|
||||
org.springframework.data.redis.listener.RedisMessageListenerContainer redisMessageListenerContainer(org.springframework.boot.data.redis.autoconfigure.RedisMessageListenerContainerConfigurer, org.springframework.data.redis.connection.RedisConnectionFactory);
|
||||
org.springframework.data.redis.listener.RedisMessageListenerContainer redisMessageListenerContainerVirtualThreads(org.springframework.boot.data.redis.autoconfigure.RedisMessageListenerContainerConfigurer, org.springframework.data.redis.connection.RedisConnectionFactory);
|
||||
|
||||
--- Defaults of @EnableRedisRepositories that matter for expiry ---
|
||||
$ javap -v -cp spring-data-redis-*.jar org.springframework.data.redis.repository.configuration.EnableRedisRepositories | grep -A6 -E '(enableKeyspaceEvents|keyspaceNotificationsConfigParameter)\(\);' | grep -E '\(\);|"Ex"|\.OFF'
|
||||
public abstract org.springframework.data.redis.core.RedisKeyValueAdapter$EnableKeyspaceEvents enableKeyspaceEvents();
|
||||
Lorg/springframework/data/redis/core/RedisKeyValueAdapter$EnableKeyspaceEvents;.OFF
|
||||
public abstract java.lang.String keyspaceNotificationsConfigParameter();
|
||||
"Ex"
|
||||
|
||||
--- The @RedisListener annotation ---
|
||||
$ javap -public -cp spring-data-redis-*.jar org.springframework.data.redis.annotation.RedisListener
|
||||
Compiled from "RedisListener.java"
|
||||
public interface org.springframework.data.redis.annotation.RedisListener extends java.lang.annotation.Annotation {
|
||||
public abstract java.lang.String id();
|
||||
public abstract java.lang.String container();
|
||||
public abstract java.lang.String value();
|
||||
public abstract java.lang.String topic();
|
||||
public abstract java.lang.String consumes();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
# What the Redis starter brings, and what it does not
|
||||
|
||||
--- The starter's own dependencies (from its pom) ---
|
||||
$ grep -E 'artifactId|scope' spring-boot-starter-data-redis-*.pom
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
<scope>compile</scope>
|
||||
<artifactId>spring-boot-data-redis</artifactId>
|
||||
<scope>compile</scope>
|
||||
<artifactId>spring-messaging</artifactId>
|
||||
<scope>compile</scope>
|
||||
|
||||
--- The client and JSON libraries in this module (mvn dependency:tree, filtered) ---
|
||||
$ mvn dependency:tree -Dincludes=io.lettuce,redis.clients,tools.jackson.core,com.fasterxml.jackson.core,org.springframework.data,org.springframework:spring-messaging
|
||||
com.ankurm:redis:jar:1.0.0
|
||||
+- org.springframework.boot:spring-boot-starter-data-redis:jar:4.1.1:compile
|
||||
| +- org.springframework.boot:spring-boot-data-redis:jar:4.1.1:compile
|
||||
| | +- org.springframework.boot:spring-boot-data-commons:jar:4.1.1:compile
|
||||
| | | \- org.springframework.data:spring-data-commons:jar:4.1.1:compile
|
||||
| | +- io.lettuce:lettuce-core:jar:7.5.2.RELEASE:compile
|
||||
| | \- org.springframework.data:spring-data-redis:jar:4.1.1:compile
|
||||
| | \- org.springframework.data:spring-data-keyvalue:jar:4.1.1:compile
|
||||
| \- org.springframework:spring-messaging:jar:7.0.9:compile
|
||||
\- org.springframework.boot:spring-boot-starter-jackson:jar:4.1.1:compile
|
||||
\- org.springframework.boot:spring-boot-jackson:jar:4.1.1:compile
|
||||
\- tools.jackson.core:jackson-databind:jar:3.1.5:compile
|
||||
+- com.fasterxml.jackson.core:jackson-annotations:jar:2.21:compile
|
||||
\- tools.jackson.core:jackson-core:jar:3.1.5:compile
|
||||
|
||||
--- The server the tests start ---
|
||||
$ redis-server --version
|
||||
Redis server v=7.0.15 sha=00000000:0 malloc=jemalloc-5.3.0 bits=64 build=e53ff17674aa6190
|
||||
Reference in New Issue
Block a user