Skip to content

Bluetape4k Cache Part 2: Near Cache as a Practical Way to Reduce Redis Round Trips

Near cache workbench with robot workers connecting local and remote cache blocks
A remote cache is a warehouse. Near Cache is the small drawer on your desk.

This is Part 2 of the Bluetape4k Cache series. The full series continues with Part 1: cache module overview, Part 2: Near Cache, Part 3: Near Cache + Exposed strategies, and Part 4: workshop examples.

A remote cache is a good starting point. Multiple JVMs can see the same value. Entries can have TTLs. Mature systems such as Redis come with solid operational tooling.

The catch is in the word remote. Every access carries a network hop, serialization, connection contention, and tail latency. Redis can be fast and still not live under the same physics as an in-process memory read. Physics does not negotiate, even if invited to a meeting.

Near Cache accepts that difference. Frequently read values stay in an in-JVM L1 cache, while shared state and invalidation stay with an L2 cache and its protocol.

Near cache read, write, and invalidation flow with local L1 cache and Redis L2 cache
L1 hits finish inside the JVM. Misses and writes go to L2. The hard part is invalidation.

Remote cache gives you clear advantages.

  • Multiple application instances share the same values.
  • Cache state does not disappear just because one JVM restarts.
  • TTL, eviction, and memory policies can be managed centrally.

Hot read paths accumulate a different set of costs.

CostDescription
network RTTEven localhost Redis is much farther than JVM memory
serializationEncoding and decoding become visible as values grow
connection contentionRedis connections and event loops get busy under load
tail latencyAverage latency may look fine while P99 becomes user-visible

Near Cache is not “put everything local.” It is “finish hot reads locally, and let the remote cache and its protocol handle shared state and invalidation.”

Redisson’s RLocalCachedMap provides a local cached map. Each JVM keeps a local map, and changes propagate through Pub/Sub so other JVMs invalidate their local entries.

The benefits are clear.

  • Users do not have to design the protocol themselves.
  • Local cache and invalidation live inside the Redisson API.
  • It is more useful for read-heavy services than write-heavy ones.

Related source: RedissonNearCache.kt, LocalCachedMapOptionsExtensions.kt

There is a tradeoff. Pub/Sub is topic-based event propagation. Redis does not automatically know which client read which key. The client and library maintain the invalidation system together. So the simple-looking setup still means “receive the change event and remove the right local entry.”

Lettuce NearCache uses Redis RESP3 CLIENT TRACKING. Redis tracks which keys a client has read and pushes invalidation messages when those keys change.

val cache = LettuceCaches.nearCache<String>(redisClient) {
cacheName = "catalog"
maxLocalSize = 50_000
frontExpireAfterWrite = Duration.ofMinutes(10)
redisTtl = Duration.ofHours(1)
useRespProtocol3 = true
}

Related source: LettuceNearCache.kt, LettuceNearCacheConfig.kt, LettuceNearCacheTrackingTest.kt

The appeal of RESP3 tracking is that the server participates in key tracking. The application uses a fast L1 cache, and Redis pushes invalidation for changed keys. Instead of the client keeping a private diary of what it read, Redis holds the attendance sheet. The attendance sheet still needs care, but it is a better starting point.

bluetape4k-cache-lettuce includes a Near Cache benchmark.

Terminal window
./gradlew :bluetape4k-cache-lettuce:benchmark

Related source: NearCacheBenchmark.kt, Benchmark.ko.md

Lettuce Near Cache benchmark summary showing L1 hit far above Redis-backed operations
L1 hits are in a different weight class from Redis-backed operations.

Throughput is measured in ops/ms; higher is better. Looking at the source report with its error ranges, L1 is dramatically faster, while Redis-backed operations cluster around Redis RTT.

Benchmark512B4096B16384B
l1Hit65,560 ± 10,86163,458 ± 23,12064,580 ± 9,507
l2Hit4.067 ± 0.5324.130 ± 0.4523.930 ± 1.370
l2Miss3.961 ± 1.3943.917 ± 0.7844.208 ± 0.408
putSingle2.119 ± 0.1002.077 ± 0.2762.014 ± 0.152
putAll1.038 ± 0.2470.930 ± 0.1180.407 ± 0.281
removeSingle4.213 ± 0.1774.243 ± 0.3524.164 ± 0.271

For the read path, compare l1Hit and l2Hit. L1 hits are around 64K ops/ms. L2 hits and misses are around 4 ops/ms. If those numbers still make you want to send every hot read to Redis, the first cache to clear might be the one in your head.

The environment matters: Apple M4 Pro, GraalVM 21, Redis 7 Testcontainers, JMH 1.37, single thread. Absolute values will move on another machine. The conclusion that JVM L1 memory and a Redis-backed path are different classes of work is much harder to move.

The benchmark does not say “Redis is slow.” Redis is fast. It says an in-process memory read and a remote operation are not the same race.

Practical interpretation:

  • Read-heavy hot keys are good NearCache candidates.
  • Write-heavy data needs invalidation cost and staleness tolerance reviewed first.
  • Large payloads expose network bandwidth and serialization cost, especially in putAll.
  • If the L1 hit ratio is low, NearCache becomes decoration. Decoration does not reduce CPU.
  • Without a failure policy, cache becomes a failure propagator.
SituationRecommendation
Heavy existing Redisson usageRedisson local cached map
Lettuce-centered stack with Redis RESP3 availableLettuce NearCache
JCache standard API mattersNearJCache / SuspendNearJCache
Coroutine serviceSuspendNearCacheOperations
Redis outage needs graceful degradationResilient decorator

Next, we will connect NearCache to an Exposed repository. Once reads, writes, and write-behind queues enter the picture, cache stops being only a performance feature and becomes a data consistency design. That is the moment cache suddenly gets serious.

Comments

Leave a note or reaction with your GitHub account.