rgoussu@goussu: ~/library/java/storage
~/library/java/storage cat redis.md

Redis from Java

# Jedis vs Lettuce vs Redisson, Spring Data Redis and Session, Quarkus and Micronaut clients, plus cache, lock, rate-limit, and messaging patterns.

Conceptsaved 2026-08-09 #java#storage#redis#caching#spring

Overview

Redis is the default answer for caching, session state, distributed coordination, and light messaging — and from Java it comes with a genuine client choice, not just one driver. Jedis, Lettuce, and Redisson occupy different points on the simplicity ↔ capability curve, and the frameworks layer their own idioms on top: Spring Data Redis and the cache abstraction, Quarkus's reactive-first client, Micronaut's Lettuce integration. The patterns matter more than the API: cache-aside, locks, rate limiting, and pub/sub vs. streams each have well-known failure modes.

Key points

  • Jedis: synchronous, one connection per thread (pool required), minimal abstraction — simple and fast for straightforward blocking use.
  • Lettuce: netty-based, thread-safe shared connection, sync/async/reactive APIs from the same client; the default under Spring Data Redis and Micronaut.
  • Redisson: distributed objects on top of Redis — RLock, RMap, RSemaphore, rate limiters; buy it for coordination primitives, not for plain caching.
  • Spring Data Redis: RedisTemplate/StringRedisTemplate for imperative access, optional repositories over hash structures, and the cache abstraction (@Cacheable, @CacheEvict) with Redis as backing store.
  • Spring Session externalizes HTTP session state to Redis — the standard move for stateless app instances behind a load balancer.
  • Quarkus Redis client: reactive-first (Mutiny) with an imperative facade, configured via quarkus.redis.*; also backs Quarkus's cache annotations. Micronaut Redis wraps Lettuce and plugs into Micronaut's cache abstraction (@Cacheable).
  • Serialization is a real decision: JDK serialization is the historical default in RedisTemplate and almost always wrong — prefer String/JSON (Jackson) serializers; key serialization affects debuggability (KEYS/SCAN output) and cross-language access.
  • Single-threaded server: one slow command (KEYS, huge SMEMBERS) stalls everyone; use SCAN, bounded structures, and TTLs everywhere.

Details

Client comparison

Jedis Lettuce Redisson
I/O model blocking, pooled netty, multiplexed netty
APIs sync sync + async + reactive sync + async + distributed objects
Cluster/sentinel yes yes yes
Sweet spot simple blocking apps frameworks, reactive stacks distributed locks/coordination

Use patterns from Java

  • Cache-aside — read: try cache, on miss load from DB and SET with TTL; write: update DB then invalidate (delete, don't update) the key. @Cacheable/@CacheEvict automate exactly this. Stampede protection: jittered TTLs, or a short lock around the reload.
  • Distributed locksSET key val NX PX ttl at minimum; Redisson's RLock adds watchdog renewal. Perils: TTL expiring mid-critical-section (a fenced token check is the honest fix), clock-skew and failover edge cases (the Redlock debate) — treat Redis locks as efficiency locks, not correctness locks.
  • Rate limiting — fixed/sliding window with INCR + EXPIRE, or token bucket as a Lua script for atomicity; Redisson ships RRateLimiter; Bucket4j has a Redis backend.
  • Pub/sub vs. streams — pub/sub is fire-and-forget (subscriber offline = message lost); Streams (XADD/XREADGROUP) add persistence, consumer groups, and acking — the right base for anything resembling a work queue.

Framework wiring

  • Spring Boot: spring-boot-starter-data-redis auto-configures a Lettuce RedisConnectionFactory from spring.data.redis.*; add @EnableCaching and a RedisCacheManager for the cache abstraction.
  • Quarkus: quarkus-redis-client; inject RedisDataSource (imperative) or ReactiveRedisDataSource. Micronaut: micronaut-redis-lettuce; StatefulRedisConnection injectable, caches declared in config.

Examples

// Spring cache abstraction backed by Redis: cache-aside without the boilerplate
@Cacheable(cacheNames = "products", key = "#id")
public Product findProduct(long id) { return repository.findById(id).orElseThrow(); }

@CacheEvict(cacheNames = "products", key = "#product.id")
public void update(Product product) { repository.save(product); }
// Redisson distributed lock — always bound the wait and the lease
RLock lock = redisson.getLock("order:" + orderId);
if (lock.tryLock(2, 30, TimeUnit.SECONDS)) {
    try { process(orderId); }
    finally { lock.unlock(); }
}

Related