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/StringRedisTemplatefor 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
RedisTemplateand almost always wrong — prefer String/JSON (Jackson) serializers; key serialization affects debuggability (KEYS/SCANoutput) and cross-language access. - Single-threaded server: one slow command (
KEYS, hugeSMEMBERS) stalls everyone; useSCAN, 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
SETwith TTL; write: update DB then invalidate (delete, don't update) the key.@Cacheable/@CacheEvictautomate exactly this. Stampede protection: jittered TTLs, or a short lock around the reload. - Distributed locks —
SET key val NX PX ttlat minimum; Redisson'sRLockadds 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 shipsRRateLimiter; 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-redisauto-configures a LettuceRedisConnectionFactoryfromspring.data.redis.*; add@EnableCachingand aRedisCacheManagerfor the cache abstraction. - Quarkus:
quarkus-redis-client; injectRedisDataSource(imperative) orReactiveRedisDataSource. Micronaut:micronaut-redis-lettuce;StatefulRedisConnectioninjectable, 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
- Storage access from Java — the map — parent overview by storage kind.
- Cache management — the design-side theory these clients implement.
- Rate limiting and idempotency — the patterns Redis counters and Lua scripts back.
- Integration testing — Testcontainers Redis module for testing against the real server.