From 129008f58552e5ca44aecde2826730545464fc5b Mon Sep 17 00:00:00 2001 From: Andy Stark Date: Fri, 30 Oct 2026 11:36:58 +0000 Subject: [PATCH] DOC-7226 Move four Jedis use cases from JedisPool to RedisClient Rate limiter, leaderboard, session store and cache-aside (java-jedis) now take a RedisClient instead of a JedisPool and call commands on it directly. The getResource() try-with-resources blocks are gone, the demo servers build the client with RedisClient.create(host, port) and check it with ping(), and the pages' snippets, prose and Maven/Gradle versions move to Jedis 8.0.2. Each page's javac/java commands now list the six jars Jedis 8.0.2 needs at runtime. With a pool, jedis.multi() and jedis.pipelined() ran on the connection the try block already held. On a RedisClient each one borrows its own pooled connection and only close() returns it, so every transaction and pipeline is now in try-with-resources. cache-aside updateField needed a real redesign, not a rename. WATCH is per-connection, and UnifiedJedis has no watch(). It now opens jedis.transaction(false), calls tx.watch(), then reads existence through the same transaction (tx.exists(key).get()) before tx.multi(). The first port read existence with jedis.exists() while the transaction held a connection; that passed at maxTotal=64 and deadlocked at the default maxTotal=8 (jstack: 40 threads parked in GenericObjectPool.borrowObject at the exists line). Verification, each demo compiled with -Xlint:deprecation (0 warnings) against Jedis 8.0.2 and run against redis:8.10 on a scratch port: rate limiter 10 allowed then denied and 200 concurrent requests; leaderboard 300 concurrent increments land exactly (400), trim and pipeline hydration work; session store full login/refresh/increment/ttl/logout cycle, 300 concurrent increments exact; cache-aside stampede x50 does one primary read. A driver calling RedisCache.updateField under contention (12 updaters, 6 invalidators, 2 HLEN samplers) gave 0 orphan single-field hashes at maxTotal 64, 8 and 2, while a no-WATCH control produced thousands, so the sampler can see the failure. The pool is back to 0 active afterward in every run. The page commands themselves were extracted from each _index.md and run literally in a clean directory holding only the six jars; all four compile and serve. Constraint: never call the client while a transaction or pipeline is open; each holds a pooled connection and a second borrow can deadlock a small pool Constraint: every multi()/pipelined() result on a RedisClient must be closed (try-with-resources) or its connection leaks Rejected: Lua script for updateField (atomic, no retry loop) because the Go, redis-py and Lettuce pages all teach WATCH/MULTI/EXEC for this path and this page lists those commands Learned: RedisClient.transaction(false) accepts reads before multi(); they run immediately on that connection and Response.get() returns at once Learned: the cache-aside demo's /update endpoint writes the primary and invalidates, so it never calls RedisCache.updateField; test that method with a driver, not HTTP Learned: Jedis 8.0.2 runtime jars are jedis, commons-pool2, slf4j-api, gson, json and redis-authx-core (its pom's compile scope). A smaller set appears to work until a class that needs the rest loads (JedisPubSub needs redis-authx-core), so test the classpath with the feature that loads it Learned: before this change the pages' jar-only or partial classpaths could not run either (commons-pool2 and slf4j-api were missing), on Jedis 5.x too Ticket: DOC-7226 Co-Authored-By: Claude Sonnet 5.5 --- .../cache-aside/java-jedis/DemoServer.java | 15 +- .../cache-aside/java-jedis/RedisCache.java | 90 +++++----- .../cache-aside/java-jedis/_index.md | 66 ++++---- .../leaderboard/java-jedis/DemoServer.java | 17 +- .../java-jedis/RedisLeaderboard.java | 158 ++++++++---------- .../leaderboard/java-jedis/_index.md | 33 ++-- .../rate-limiter/java-jedis/DemoServer.java | 15 +- .../rate-limiter/java-jedis/TokenBucket.java | 43 +++-- .../rate-limiter/java-jedis/_index.md | 112 ++++++------- .../session-store/java-jedis/DemoServer.java | 17 +- .../java-jedis/RedisSessionStore.java | 126 +++++++------- .../session-store/java-jedis/_index.md | 42 ++--- 12 files changed, 333 insertions(+), 401 deletions(-) diff --git a/content/develop/use-cases/cache-aside/java-jedis/DemoServer.java b/content/develop/use-cases/cache-aside/java-jedis/DemoServer.java index 9ada261333..9ca8595e73 100644 --- a/content/develop/use-cases/cache-aside/java-jedis/DemoServer.java +++ b/content/develop/use-cases/cache-aside/java-jedis/DemoServer.java @@ -1,8 +1,7 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.RedisClient; import java.io.IOException; import java.io.InputStream; @@ -27,15 +26,15 @@ *

Usage:

* *
{@code
- * javac -cp jedis-5.2.0.jar RedisCache.java MockPrimaryStore.java DemoServer.java
- * java -cp .:jedis-5.2.0.jar DemoServer --port 8080 --redis-host localhost --redis-port 6379
+ * javac -cp jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar RedisCache.java MockPrimaryStore.java DemoServer.java
+ * java -cp .:jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar DemoServer --port 8080 --redis-host localhost --redis-port 6379
  * }
*/ public class DemoServer { private static RedisCache cache; private static MockPrimaryStore primary; - private static JedisPool jedisPool; + private static RedisClient jedis; public static void main(String[] args) { int port = 8080; @@ -67,14 +66,14 @@ public static void main(String[] args) { } try { - jedisPool = new JedisPool(new JedisPoolConfig(), redisHost, redisPort); - jedisPool.getResource().close(); + jedis = RedisClient.create(redisHost, redisPort); + jedis.ping(); } catch (Exception e) { System.err.printf("Failed to connect to Redis at %s:%d: %s%n", redisHost, redisPort, e.getMessage()); System.exit(1); } - cache = new RedisCache(jedisPool, "cache:product:", ttl, 2000, 25); + cache = new RedisCache(jedis, "cache:product:", ttl, 2000, 25); primary = new MockPrimaryStore(primaryLatencyMs); try { diff --git a/content/develop/use-cases/cache-aside/java-jedis/RedisCache.java b/content/develop/use-cases/cache-aside/java-jedis/RedisCache.java index 62fd005267..9b4d2bf39d 100644 --- a/content/develop/use-cases/cache-aside/java-jedis/RedisCache.java +++ b/content/develop/use-cases/cache-aside/java-jedis/RedisCache.java @@ -6,9 +6,8 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.Transaction; +import redis.clients.jedis.AbstractTransaction; +import redis.clients.jedis.RedisClient; import redis.clients.jedis.exceptions.JedisException; /** @@ -31,7 +30,7 @@ public class RedisCache { private static final SecureRandom RANDOM = new SecureRandom(); - private final JedisPool pool; + private final RedisClient jedis; private final String prefix; private final int ttl; private final int lockTtlMs; @@ -41,18 +40,18 @@ public class RedisCache { private final AtomicLong misses = new AtomicLong(); private final AtomicLong stampedesSuppressed = new AtomicLong(); - public RedisCache(JedisPool pool) { - this(pool, "cache:product:", 30, 2000, 25); + public RedisCache(RedisClient jedis) { + this(jedis, "cache:product:", 30, 2000, 25); } - public RedisCache(JedisPool pool, String prefix, int ttl, int lockTtlMs, int waitPollMs) { - if (pool == null) { - throw new IllegalArgumentException("pool is required"); + public RedisCache(RedisClient jedis, String prefix, int ttl, int lockTtlMs, int waitPollMs) { + if (jedis == null) { + throw new IllegalArgumentException("jedis is required"); } if (ttl < 1) { throw new IllegalArgumentException("ttl must be at least 1 second"); } - this.pool = pool; + this.jedis = jedis; this.prefix = (prefix == null || prefix.isEmpty()) ? "cache:product:" : prefix; this.ttl = ttl; this.lockTtlMs = lockTtlMs; @@ -82,9 +81,7 @@ public Result get(String entityId, Function> loader) Map cached; long startedNs = System.nanoTime(); - try (Jedis jedis = pool.getResource()) { - cached = jedis.hgetAll(cacheKey); - } + cached = jedis.hgetAll(cacheKey); double redisLatencyMs = (System.nanoTime() - startedNs) / 1_000_000.0; if (cached != null && !cached.isEmpty()) { @@ -99,9 +96,7 @@ public Result get(String entityId, Function> loader) /** Delete the cached entry for entityId. */ public boolean invalidate(String entityId) { - try (Jedis jedis = pool.getResource()) { - return jedis.del(cacheKey(entityId)) == 1L; - } + return jedis.del(cacheKey(entityId)) == 1L; } /** @@ -113,13 +108,17 @@ public boolean invalidate(String entityId) { public boolean updateField(String entityId, String field, String value) { String cacheKey = cacheKey(entityId); while (true) { - try (Jedis jedis = pool.getResource()) { - jedis.watch(cacheKey); - if (!jedis.exists(cacheKey)) { - jedis.unwatch(); + // WATCH is tied to one connection, so open a transaction that + // holds it until EXEC instead of calling watch() on the client. + // Read through the same transaction: borrowing a second pooled + // connection while holding this one can deadlock a small pool. + try (AbstractTransaction tx = jedis.transaction(false)) { + tx.watch(cacheKey); + if (!tx.exists(cacheKey).get()) { + tx.unwatch(); return false; } - Transaction tx = jedis.multi(); + tx.multi(); tx.hset(cacheKey, field, value); tx.expire(cacheKey, ttl); List result = tx.exec(); @@ -133,9 +132,7 @@ public boolean updateField(String entityId, String field, String value) { /** Remaining TTL in seconds, or -2/-1 per Redis semantics. */ public long ttlRemaining(String entityId) { - try (Jedis jedis = pool.getResource()) { - return jedis.ttl(cacheKey(entityId)); - } + return jedis.ttl(cacheKey(entityId)); } /** Hit/miss/stampede counters with the derived hit rate. */ @@ -166,29 +163,28 @@ private Map loadWithSingleFlight( String lockKey = lockKey(entityId); String token = randomToken(); - try (Jedis jedis = pool.getResource()) { - Object acquired = jedis.eval( - ACQUIRE_LOCK_SCRIPT, - List.of(lockKey), - List.of(token, Integer.toString(lockTtlMs))); - if (asInt(acquired) == 1) { - try { - Map record = loader.apply(entityId); - if (record == null) { - return null; - } - Transaction tx = jedis.multi(); + Object acquired = jedis.eval( + ACQUIRE_LOCK_SCRIPT, + List.of(lockKey), + List.of(token, Integer.toString(lockTtlMs))); + if (asInt(acquired) == 1) { + try { + Map record = loader.apply(entityId); + if (record == null) { + return null; + } + try (AbstractTransaction tx = jedis.multi()) { tx.del(cacheKey); tx.hset(cacheKey, record); tx.expire(cacheKey, ttl); tx.exec(); - return record; - } finally { - try { - jedis.eval(RELEASE_LOCK_SCRIPT, List.of(lockKey), List.of(token)); - } catch (JedisException ignored) { - // Lock will expire on its own. - } + } + return record; + } finally { + try { + jedis.eval(RELEASE_LOCK_SCRIPT, List.of(lockKey), List.of(token)); + } catch (JedisException ignored) { + // Lock will expire on its own. } } } @@ -202,11 +198,9 @@ private Map loadWithSingleFlight( Thread.currentThread().interrupt(); return null; } - try (Jedis jedis = pool.getResource()) { - Map cached = jedis.hgetAll(cacheKey); - if (cached != null && !cached.isEmpty()) { - return cached; - } + Map cached = jedis.hgetAll(cacheKey); + if (cached != null && !cached.isEmpty()) { + return cached; } } return loader.apply(entityId); diff --git a/content/develop/use-cases/cache-aside/java-jedis/_index.md b/content/develop/use-cases/cache-aside/java-jedis/_index.md index a972cb0b35..0c1a8c92c4 100644 --- a/content/develop/use-cases/cache-aside/java-jedis/_index.md +++ b/content/develop/use-cases/cache-aside/java-jedis/_index.md @@ -50,12 +50,11 @@ The `RedisCache` class wraps the cache-aside operations ([source](RedisCache.java)): ```java -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.RedisClient; -JedisPool pool = new JedisPool(new JedisPoolConfig(), "localhost", 6379); +RedisClient jedis = RedisClient.create("localhost", 6379); MockPrimaryStore primary = new MockPrimaryStore(150); -RedisCache cache = new RedisCache(pool); +RedisCache cache = new RedisCache(jedis); // Read through the cache. RedisCache.Result result = cache.get("p-001", primary::read); @@ -101,9 +100,7 @@ public Result get(String entityId, Function> loader) Map cached; long startedNs = System.nanoTime(); - try (Jedis jedis = pool.getResource()) { - cached = jedis.hgetAll(cacheKey); - } + cached = jedis.hgetAll(cacheKey); double redisLatencyMs = (System.nanoTime() - startedNs) / 1_000_000.0; if (cached != null && !cached.isEmpty()) { @@ -152,24 +149,23 @@ private Map loadWithSingleFlight( String lockKey = lockKey(entityId); String token = randomToken(); - try (Jedis jedis = pool.getResource()) { - Object acquired = jedis.eval( - ACQUIRE_LOCK_SCRIPT, - List.of(lockKey), - List.of(token, Integer.toString(lockTtlMs))); - if (asInt(acquired) == 1) { - try { - Map record = loader.apply(entityId); - if (record == null) return null; - Transaction tx = jedis.multi(); + Object acquired = jedis.eval( + ACQUIRE_LOCK_SCRIPT, + List.of(lockKey), + List.of(token, Integer.toString(lockTtlMs))); + if (asInt(acquired) == 1) { + try { + Map record = loader.apply(entityId); + if (record == null) return null; + try (AbstractTransaction tx = jedis.multi()) { tx.del(cacheKey); tx.hset(cacheKey, record); tx.expire(cacheKey, ttl); tx.exec(); - return record; - } finally { - jedis.eval(RELEASE_LOCK_SCRIPT, List.of(lockKey), List.of(token)); } + return record; + } finally { + jedis.eval(RELEASE_LOCK_SCRIPT, List.of(lockKey), List.of(token)); } } @@ -177,10 +173,8 @@ private Map loadWithSingleFlight( long deadline = System.nanoTime() + (long) lockTtlMs * 1_000_000L; while (System.nanoTime() < deadline) { Thread.sleep(waitPollMs); - try (Jedis jedis = pool.getResource()) { - Map cached = jedis.hgetAll(cacheKey); - if (cached != null && !cached.isEmpty()) return cached; - } + Map cached = jedis.hgetAll(cacheKey); + if (cached != null && !cached.isEmpty()) return cached; } return loader.apply(entityId); } @@ -194,9 +188,7 @@ When a write hits the primary, the application invalidates the cache key. The ne ```java public boolean invalidate(String entityId) { - try (Jedis jedis = pool.getResource()) { - return jedis.del(cacheKey(entityId)) == 1L; - } + return jedis.del(cacheKey(entityId)) == 1L; } ``` @@ -210,13 +202,15 @@ Because each record is stored as a hash, the cache helper can also update a sing public boolean updateField(String entityId, String field, String value) { String cacheKey = cacheKey(entityId); while (true) { - try (Jedis jedis = pool.getResource()) { - jedis.watch(cacheKey); - if (!jedis.exists(cacheKey)) { - jedis.unwatch(); + // WATCH is tied to one connection, so open a transaction that + // holds it until EXEC. + try (AbstractTransaction tx = jedis.transaction(false)) { + tx.watch(cacheKey); + if (!tx.exists(cacheKey).get()) { + tx.unwatch(); return false; } - Transaction tx = jedis.multi(); + tx.multi(); tx.hset(cacheKey, field, value); tx.expire(cacheKey, ttl); List result = tx.exec(); @@ -268,10 +262,10 @@ A local demo server is included to show the cache-aside layer in action ([source](DemoServer.java)): ```bash -javac -cp jedis-5.0.0.jar:commons-pool2-2.12.1.jar:slf4j-api-1.7.36.jar:gson-2.9.1.jar:json-20230618.jar \ +javac -cp jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar \ RedisCache.java MockPrimaryStore.java DemoServer.java -java -cp .:jedis-5.0.0.jar:commons-pool2-2.12.1.jar:slf4j-api-1.7.36.jar:gson-2.9.1.jar:json-20230618.jar \ +java -cp .:jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar \ DemoServer --port 8080 --redis-host localhost --redis-port 6379 ``` @@ -333,9 +327,9 @@ In this demo, a missing record returns `null` and nothing is cached. In a real s The lock TTL needs to be longer than the worst-case primary read latency so a slow loader does not lose the lock midway. The unique token in `RELEASE_LOCK_SCRIPT` ensures the original caller does not delete someone else's lock if its lock has expired. -### Tune the JedisPool +### Tune the connection pool -The demo uses a default `JedisPoolConfig`. In production, set `setMaxTotal()` and `setMaxIdle()` to match your concurrency profile and server-side `maxclients`. Wrap pool acquisition in try-with-resources so connections are returned even on exceptions. +The demo uses the default jedis settings of `RedisClient.create()`. In production, build the client with `RedisClient.builder()` and a `ConnectionPoolConfig`, and set `setMaxTotal()` and `setMaxIdle()` to match your concurrency profile and server-side `maxclients`. Wrap transactions and pipelines in try-with-resources so their connections return to the jedis even on exceptions. An open transaction or pipeline holds a pooled connection, so send any reads it needs through the transaction or pipeline itself, not through the client. ### Namespace cache keys in shared Redis deployments diff --git a/content/develop/use-cases/leaderboard/java-jedis/DemoServer.java b/content/develop/use-cases/leaderboard/java-jedis/DemoServer.java index a453396490..5f17557361 100644 --- a/content/develop/use-cases/leaderboard/java-jedis/DemoServer.java +++ b/content/develop/use-cases/leaderboard/java-jedis/DemoServer.java @@ -1,8 +1,7 @@ import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.RedisClient; import java.io.IOException; import java.io.InputStream; @@ -19,8 +18,8 @@ * Redis leaderboard demo server. * * Usage: - * javac -cp jedis-5.2.0.jar RedisLeaderboard.java DemoServer.java - * java -cp .:jedis-5.2.0.jar DemoServer [--port PORT] [--redis-host HOST] [--redis-port PORT] + * javac -cp jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar RedisLeaderboard.java DemoServer.java + * java -cp .:jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar DemoServer [--port PORT] [--redis-host HOST] [--redis-port PORT] */ public class DemoServer { @@ -54,7 +53,7 @@ private record DemoPlayer(String userId, double score, Map metad ); private static RedisLeaderboard leaderboard; - private static JedisPool jedisPool; + private static RedisClient jedis; public static void main(String[] args) { int port = 8080; @@ -80,15 +79,15 @@ public static void main(String[] args) { } try { - jedisPool = new JedisPool(new JedisPoolConfig(), redisHost, redisPort); - jedisPool.getResource().close(); + jedis = RedisClient.create(redisHost, redisPort); + jedis.ping(); } catch (Exception e) { System.err.printf("Failed to connect to Redis: %s%n", e.getMessage()); System.err.printf("Make sure Redis is running at %s:%d%n", redisHost, redisPort); System.exit(1); } - leaderboard = new RedisLeaderboard(jedisPool, "leaderboard:demo", maxEntries); + leaderboard = new RedisLeaderboard(jedis, "leaderboard:demo", maxEntries); resetDemoData(); try { @@ -109,7 +108,7 @@ public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("\nStopping server."); - jedisPool.close(); + jedis.close(); server.stop(0); })); } catch (IOException e) { diff --git a/content/develop/use-cases/leaderboard/java-jedis/RedisLeaderboard.java b/content/develop/use-cases/leaderboard/java-jedis/RedisLeaderboard.java index a3c0039971..29048bbac4 100644 --- a/content/develop/use-cases/leaderboard/java-jedis/RedisLeaderboard.java +++ b/content/develop/use-cases/leaderboard/java-jedis/RedisLeaderboard.java @@ -5,11 +5,10 @@ import java.util.Map; import java.util.Set; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; +import redis.clients.jedis.AbstractTransaction; import redis.clients.jedis.Pipeline; +import redis.clients.jedis.RedisClient; import redis.clients.jedis.Response; -import redis.clients.jedis.Transaction; import redis.clients.jedis.params.ZRangeParams; import redis.clients.jedis.resps.Tuple; @@ -30,21 +29,21 @@ public LeaderboardEntry withTrimmedUserIds(List userIds) { } } - private final JedisPool jedisPool; + private final RedisClient jedis; private final String key; private int maxEntries; - public RedisLeaderboard(JedisPool jedisPool, String key, int maxEntries) { - if (jedisPool == null) { - throw new IllegalArgumentException("jedisPool is required"); + public RedisLeaderboard(RedisClient jedis, String key, int maxEntries) { + if (jedis == null) { + throw new IllegalArgumentException("jedis is required"); } - this.jedisPool = jedisPool; + this.jedis = jedis; this.key = (key == null || key.isBlank()) ? "leaderboard:demo" : key; this.maxEntries = normalizePositiveInt(maxEntries == 0 ? 100 : maxEntries, "maxEntries"); } - public RedisLeaderboard(JedisPool jedisPool) { - this(jedisPool, "leaderboard:demo", 100); + public RedisLeaderboard(RedisClient jedis) { + this(jedis, "leaderboard:demo", 100); } public String getKey() { @@ -58,8 +57,7 @@ public int getMaxEntries() { public LeaderboardEntry upsertUser(String userId, double score, Map metadata) { Map payload = coerceMetadata(metadata); - try (Jedis jedis = jedisPool.getResource()) { - Transaction tx = jedis.multi(); + try (AbstractTransaction tx = jedis.multi()) { tx.zadd(key, score, userId); if (!payload.isEmpty()) { tx.hset(metadataKey(userId), payload); @@ -79,8 +77,7 @@ public LeaderboardEntry incrementScore(String userId, double amount, Map payload = coerceMetadata(metadata); double newScore; - try (Jedis jedis = jedisPool.getResource()) { - Transaction tx = jedis.multi(); + try (AbstractTransaction tx = jedis.multi()) { Response scoreResponse = tx.zincrby(key, amount, userId); if (!payload.isEmpty()) { tx.hset(metadataKey(userId), payload); @@ -104,13 +101,11 @@ public List setMaxEntries(int maxEntries) { public List getTop(int count) { int normalizedCount = normalizePositiveInt(count, "count"); - try (Jedis jedis = jedisPool.getResource()) { - List entries = jedis.zrangeWithScores( - key, - ZRangeParams.zrangeParams(0, normalizedCount - 1).rev() - ); - return hydrateEntries(entries, 1); - } + List entries = jedis.zrangeWithScores( + key, + ZRangeParams.zrangeParams(0, normalizedCount - 1).rev() + ); + return hydrateEntries(entries, 1); } public List getAroundRank(int rank, int count) { @@ -133,34 +128,27 @@ public List getAroundRank(int rank, int count) { } int end = start + normalizedCount - 1; - try (Jedis jedis = jedisPool.getResource()) { - List entries = jedis.zrangeWithScores( - key, - ZRangeParams.zrangeParams(start, end).rev() - ); - return hydrateEntries(entries, start + 1); - } + List entries = jedis.zrangeWithScores( + key, + ZRangeParams.zrangeParams(start, end).rev() + ); + return hydrateEntries(entries, start + 1); } public Integer getRank(String userId) { - try (Jedis jedis = jedisPool.getResource()) { - Long rank = jedis.zrevrank(key, userId); - if (rank == null) { - return null; - } - return Math.toIntExact(rank) + 1; + Long rank = jedis.zrevrank(key, userId); + if (rank == null) { + return null; } + return Math.toIntExact(rank) + 1; } public Map getUserMetadata(String userId) { - try (Jedis jedis = jedisPool.getResource()) { - return jedis.hgetAll(metadataKey(userId)); - } + return jedis.hgetAll(metadataKey(userId)); } public LeaderboardEntry getUserEntry(String userId) { - try (Jedis jedis = jedisPool.getResource()) { - Pipeline pipeline = jedis.pipelined(); + try (Pipeline pipeline = jedis.pipelined()) { Response scoreResponse = pipeline.zscore(key, userId); Response rankResponse = pipeline.zrevrank(key, userId); Response> metadataResponse = pipeline.hgetAll(metadataKey(userId)); @@ -183,24 +171,19 @@ public LeaderboardEntry getUserEntry(String userId) { } public List listAll() { - try (Jedis jedis = jedisPool.getResource()) { - List entries = jedis.zrangeWithScores( - key, - ZRangeParams.zrangeParams(0, -1).rev() - ); - return hydrateEntries(entries, 1); - } + List entries = jedis.zrangeWithScores( + key, + ZRangeParams.zrangeParams(0, -1).rev() + ); + return hydrateEntries(entries, 1); } public long getSize() { - try (Jedis jedis = jedisPool.getResource()) { - return jedis.zcard(key); - } + return jedis.zcard(key); } public boolean deleteUser(String userId) { - try (Jedis jedis = jedisPool.getResource()) { - Transaction tx = jedis.multi(); + try (AbstractTransaction tx = jedis.multi()) { Response removed = tx.zrem(key, userId); tx.del(metadataKey(userId)); tx.exec(); @@ -209,15 +192,13 @@ public boolean deleteUser(String userId) { } public void clear() { - try (Jedis jedis = jedisPool.getResource()) { - List userIds = jedis.zrange(key, 0, -1); - List keysToDelete = new ArrayList<>(); - keysToDelete.add(key); - for (String userId : userIds) { - keysToDelete.add(metadataKey(userId)); - } - jedis.del(keysToDelete.toArray(String[]::new)); + List userIds = jedis.zrange(key, 0, -1); + List keysToDelete = new ArrayList<>(); + keysToDelete.add(key); + for (String userId : userIds) { + keysToDelete.add(metadataKey(userId)); } + jedis.del(keysToDelete.toArray(String[]::new)); } private String metadataKey(String userId) { @@ -232,34 +213,30 @@ private Map coerceMetadata(Map metadata) { } private List trimToMaxEntries() { - try (Jedis jedis = jedisPool.getResource()) { - long overflow = jedis.zcard(key) - maxEntries; - if (overflow <= 0) { - return List.of(); - } - - List trimmedUserIds = jedis.zrange(key, 0, overflow - 1); - if (trimmedUserIds.isEmpty()) { - return List.of(); - } + long overflow = jedis.zcard(key) - maxEntries; + if (overflow <= 0) { + return List.of(); + } - jedis.zremrangeByRank(key, 0, overflow - 1); - deleteMetadataForUsers(trimmedUserIds); - return List.copyOf(trimmedUserIds); + List trimmedUserIds = jedis.zrange(key, 0, overflow - 1); + if (trimmedUserIds.isEmpty()) { + return List.of(); } + + jedis.zremrangeByRank(key, 0, overflow - 1); + deleteMetadataForUsers(trimmedUserIds); + return List.copyOf(trimmedUserIds); } private void deleteMetadataForUsers(List userIds) { if (userIds.isEmpty()) { return; } - try (Jedis jedis = jedisPool.getResource()) { - List keysToDelete = new ArrayList<>(); - for (String userId : userIds) { - keysToDelete.add(metadataKey(userId)); - } - jedis.del(keysToDelete.toArray(String[]::new)); + List keysToDelete = new ArrayList<>(); + for (String userId : userIds) { + keysToDelete.add(metadataKey(userId)); } + jedis.del(keysToDelete.toArray(String[]::new)); } private List hydrateEntries(List entries, int startRank) { @@ -269,24 +246,23 @@ private List hydrateEntries(List entries, int startRank List hydratedEntries = new ArrayList<>(); - try (Jedis jedis = jedisPool.getResource()) { - Pipeline pipeline = jedis.pipelined(); - List>> metadataResponses = new ArrayList<>(); + List>> metadataResponses = new ArrayList<>(); + try (Pipeline pipeline = jedis.pipelined()) { for (Tuple entry : entries) { metadataResponses.add(pipeline.hgetAll(metadataKey(entry.getElement()))); } pipeline.sync(); + } - for (int index = 0; index < entries.size(); index++) { - Tuple entry = entries.get(index); - hydratedEntries.add(new LeaderboardEntry( - startRank + index, - entry.getElement(), - entry.getScore(), - metadataResponses.get(index).get(), - Collections.emptyList() - )); - } + for (int index = 0; index < entries.size(); index++) { + Tuple entry = entries.get(index); + hydratedEntries.add(new LeaderboardEntry( + startRank + index, + entry.getElement(), + entry.getScore(), + metadataResponses.get(index).get(), + Collections.emptyList() + )); } return hydratedEntries; diff --git a/content/develop/use-cases/leaderboard/java-jedis/_index.md b/content/develop/use-cases/leaderboard/java-jedis/_index.md index 6c6d80aac2..563dcc44f3 100644 --- a/content/develop/use-cases/leaderboard/java-jedis/_index.md +++ b/content/develop/use-cases/leaderboard/java-jedis/_index.md @@ -53,14 +53,14 @@ Add the Jedis dependency to your project: redis.clients jedis - 5.2.0 + 8.0.2 ``` * If you use **Gradle**: ```groovy - implementation 'redis.clients:jedis:5.2.0' + implementation 'redis.clients:jedis:8.0.2' ``` ## The Java leaderboard @@ -71,14 +71,14 @@ The `RedisLeaderboard` class wraps common leaderboard operations ```java import java.util.Map; -import redis.clients.jedis.JedisPool; +import redis.clients.jedis.RedisClient; public class Main { public static void main(String[] args) { - JedisPool jedisPool = new JedisPool("localhost", 6379); + RedisClient jedis = RedisClient.create("localhost", 6379); RedisLeaderboard board = new RedisLeaderboard( - jedisPool, + jedis, "leaderboard:demo", 100 ); @@ -136,8 +136,7 @@ public LeaderboardEntry upsertUser( ) { Map payload = coerceMetadata(metadata); - try (Jedis jedis = jedisPool.getResource()) { - Transaction tx = jedis.multi(); + try (AbstractTransaction tx = jedis.multi()) { tx.zadd(key, score, userId); if (!payload.isEmpty()) { tx.hset(metadataKey(userId), payload); @@ -174,17 +173,15 @@ public List getAroundRank(int rank, int count) { } int end = start + normalizedCount - 1; - try (Jedis jedis = jedisPool.getResource()) { - Set entries = jedis.zrangeWithScores( - key, - ZRangeParams.zrangeParams(start, end).rev() - ); - return hydrateEntries(entries, start + 1); - } + Set entries = jedis.zrangeWithScores( + key, + ZRangeParams.zrangeParams(start, end).rev() + ); + return hydrateEntries(entries, start + 1); } ``` -Jedis operations are synchronous and thread-safe when you use `JedisPool`, which manages a pool of connections internally. +Jedis operations are synchronous and thread-safe when you use `RedisClient`, which manages a pool of connections internally. ## Metadata design @@ -217,10 +214,12 @@ A local demo server ([source](https://github.com/redis/docs/blob/main/content/de ```bash # Compile -javac -cp jedis-5.2.0.jar RedisLeaderboard.java DemoServer.java +javac -cp jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar \ + RedisLeaderboard.java DemoServer.java # Run the demo server -java -cp .:jedis-5.2.0.jar DemoServer +java -cp .:jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar \ + DemoServer ``` The demo provides an interactive web interface where you can: diff --git a/content/develop/use-cases/rate-limiter/java-jedis/DemoServer.java b/content/develop/use-cases/rate-limiter/java-jedis/DemoServer.java index ae22edcac0..8bdbc2cf99 100644 --- a/content/develop/use-cases/rate-limiter/java-jedis/DemoServer.java +++ b/content/develop/use-cases/rate-limiter/java-jedis/DemoServer.java @@ -13,8 +13,7 @@ import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpExchange; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.RedisClient; import java.io.IOException; import java.io.InputStream; @@ -28,7 +27,7 @@ public class DemoServer { private static TokenBucket limiter; - private static JedisPool jedisPool; + private static RedisClient jedis; private static int capacity = 10; private static int refillRate = 1; private static double refillInterval = 1.0; @@ -55,8 +54,8 @@ public static void main(String[] args) { // Initialize Redis connection try { - jedisPool = new JedisPool(new JedisPoolConfig(), redisHost, redisPort); - jedisPool.getResource().close(); // Test connection + jedis = RedisClient.create(redisHost, redisPort); + jedis.ping(); // Test connection System.out.printf("✓ Connected to Redis at %s:%d%n", redisHost, redisPort); } catch (Exception e) { System.err.printf("✗ Failed to connect to Redis: %s%n", e.getMessage()); @@ -65,7 +64,7 @@ public static void main(String[] args) { } // Initialize the rate limiter - limiter = new TokenBucket(capacity, refillRate, refillInterval, jedisPool); + limiter = new TokenBucket(capacity, refillRate, refillInterval, jedis); // Start the server try { @@ -82,7 +81,7 @@ public static void main(String[] args) { Runtime.getRuntime().addShutdownHook(new Thread(() -> { System.out.println("\n✓ Server stopped"); - jedisPool.close(); + jedis.close(); server.stop(0); })); } catch (IOException e) { @@ -136,7 +135,7 @@ public void handle(HttpExchange exchange) throws IOException { refillInterval = Double.parseDouble(params.get("refill_interval")); } - limiter = new TokenBucket(capacity, refillRate, refillInterval, jedisPool); + limiter = new TokenBucket(capacity, refillRate, refillInterval, jedis); json = String.format( "{\"success\":true,\"config\":{\"capacity\":%d,\"refill_rate\":%d,\"refill_interval\":%.1f}}", diff --git a/content/develop/use-cases/rate-limiter/java-jedis/TokenBucket.java b/content/develop/use-cases/rate-limiter/java-jedis/TokenBucket.java index 37ec32f833..2dd3db76a5 100644 --- a/content/develop/use-cases/rate-limiter/java-jedis/TokenBucket.java +++ b/content/develop/use-cases/rate-limiter/java-jedis/TokenBucket.java @@ -4,8 +4,7 @@ import java.util.Collections; import java.util.List; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; +import redis.clients.jedis.RedisClient; import redis.clients.jedis.exceptions.JedisNoScriptException; /** @@ -20,8 +19,8 @@ * *

Example usage:

*
{@code
- * JedisPool pool = new JedisPool("localhost", 6379);
- * TokenBucket limiter = new TokenBucket(10, 1.0, 1.0, pool);
+ * RedisClient jedis = RedisClient.create("localhost", 6379);
+ * TokenBucket limiter = new TokenBucket(10, 1.0, 1.0, jedis);
  * RateLimitResult result = limiter.allow("user:123");
  * if (result.allowed()) {
  *     System.out.println("Request allowed. " + result.remaining() + " tokens remaining.");
@@ -85,7 +84,7 @@ public record RateLimitResult(boolean allowed, double remaining) {}
     private final int capacity;
     private final double refillRate;
     private final double refillInterval;
-    private final JedisPool jedisPool;
+    private final RedisClient jedis;
     private String scriptSha;
     private boolean scriptLoaded;
 
@@ -95,13 +94,13 @@ public record RateLimitResult(boolean allowed, double remaining) {}
      * @param capacity       maximum number of tokens in the bucket (default: 10)
      * @param refillRate     number of tokens added per refill interval (default: 1.0)
      * @param refillInterval time in seconds between refills (default: 1.0)
-     * @param jedisPool      Jedis connection pool for Redis operations
+     * @param jedis          pooled Jedis client for Redis operations
      */
-    public TokenBucket(int capacity, double refillRate, double refillInterval, JedisPool jedisPool) {
+    public TokenBucket(int capacity, double refillRate, double refillInterval, RedisClient jedis) {
         this.capacity = capacity;
         this.refillRate = refillRate;
         this.refillInterval = refillInterval;
-        this.jedisPool = jedisPool;
+        this.jedis = jedis;
         this.scriptSha = sha1Hex(TOKEN_BUCKET_SCRIPT);
         this.scriptLoaded = false;
     }
@@ -109,10 +108,10 @@ public TokenBucket(int capacity, double refillRate, double refillInterval, Jedis
     /**
      * Creates a token bucket with default parameters (capacity=10, refillRate=1.0, refillInterval=1.0).
      *
-     * @param jedisPool Jedis connection pool for Redis operations
+     * @param jedis pooled Jedis client for Redis operations
      */
-    public TokenBucket(JedisPool jedisPool) {
-        this(10, 1.0, 1.0, jedisPool);
+    public TokenBucket(RedisClient jedis) {
+        this(10, 1.0, 1.0, jedis);
     }
 
     /**
@@ -138,15 +137,13 @@ public RateLimitResult allow(String key) {
         );
 
         List result;
-        try (Jedis jedis = jedisPool.getResource()) {
-            try {
-                // Try EVALSHA first (faster if script is cached)
-                result = (List) jedis.evalsha(scriptSha, keys, args);
-            } catch (JedisNoScriptException e) {
-                // Script not in cache, fall back to EVAL and reload
-                result = (List) jedis.eval(TOKEN_BUCKET_SCRIPT, keys, args);
-                scriptLoaded = false;
-            }
+        try {
+            // Try EVALSHA first (faster if script is cached)
+            result = (List) jedis.evalsha(scriptSha, keys, args);
+        } catch (JedisNoScriptException e) {
+            // Script not in cache, fall back to EVAL and reload
+            result = (List) jedis.eval(TOKEN_BUCKET_SCRIPT, keys, args);
+            scriptLoaded = false;
         }
 
         boolean allowed = result.get(0) == 1L;
@@ -157,10 +154,8 @@ public RateLimitResult allow(String key) {
     /** Ensures the Lua script is loaded into Redis. */
     private void ensureScriptLoaded() {
         if (!scriptLoaded) {
-            try (Jedis jedis = jedisPool.getResource()) {
-                scriptSha = jedis.scriptLoad(TOKEN_BUCKET_SCRIPT);
-                scriptLoaded = true;
-            }
+            scriptSha = jedis.scriptLoad(TOKEN_BUCKET_SCRIPT);
+            scriptLoaded = true;
         }
     }
 
diff --git a/content/develop/use-cases/rate-limiter/java-jedis/_index.md b/content/develop/use-cases/rate-limiter/java-jedis/_index.md
index fac7fbb8ba..54de2ab9c3 100644
--- a/content/develop/use-cases/rate-limiter/java-jedis/_index.md
+++ b/content/develop/use-cases/rate-limiter/java-jedis/_index.md
@@ -26,7 +26,7 @@ Rate limiting is a critical technique for controlling the rate at which operatio
 * Ensuring fair resource allocation across multiple clients
 * Throttling background jobs or batch operations
 
-The **token bucket algorithm** is a popular rate limiting approach that allows bursts of traffic while maintaining an average rate limit over time. This guide covers the Java implementation using the [`Jedis`](/content/develop/clients/jedis/_index.md) client library, taking advantage of Java's `try-with-resources` for connection management, `JedisPool` for connection pooling, and checked exceptions for error handling.
+The **token bucket algorithm** is a popular rate limiting approach that allows bursts of traffic while maintaining an average rate limit over time. This guide covers the Java implementation using the [`Jedis`](/content/develop/clients/jedis/_index.md) client library, taking advantage of the `RedisClient` class for connection pooling and checked exceptions for error handling.
 
 ## How it works
 
@@ -126,14 +126,14 @@ Add the Jedis dependency to your project:
   
       redis.clients
       jedis
-      5.2.0
+      8.0.2
   
   ```
 
 * If you use **Gradle**:
 
   ```groovy
-  implementation 'redis.clients:jedis:5.2.0'
+  implementation 'redis.clients:jedis:8.0.2'
   ```
 
 ## Using the Java class
@@ -142,15 +142,15 @@ The `TokenBucket` class provides a thread-safe interface for rate limiting
 ([source](https://github.com/redis/docs/blob/main/content/develop/use-cases/rate-limiter/java-jedis/TokenBucket.java)):
 
 ```java
-import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.RedisClient;
 
 public class Main {
     public static void main(String[] args) {
-        // Create a Redis connection pool
-        JedisPool jedisPool = new JedisPool("localhost", 6379);
+        // Create a pooled Redis client
+        RedisClient jedis = RedisClient.create("localhost", 6379);
 
         // Create a rate limiter: 10 requests per second
-        TokenBucket limiter = new TokenBucket(10, 1, 1.0, jedisPool);
+        TokenBucket limiter = new TokenBucket(10, 1, 1.0, jedis);
 
         // Check if a request should be allowed
         TokenBucket.RateLimitResult result = limiter.allow("user:123");
@@ -166,7 +166,7 @@ public class Main {
 }
 ```
 
-Jedis operations are synchronous and thread-safe when using `JedisPool`, which manages a pool of connections internally. The `allow()` method returns a `RateLimitResult` record containing both the decision and the remaining token count.
+Jedis operations are synchronous and thread-safe when using `RedisClient`, which manages a pool of connections internally. The `allow()` method returns a `RateLimitResult` record containing both the decision and the remaining token count.
 
 ### Configuration parameters
 
@@ -205,7 +205,7 @@ The `TokenBucket` class is thread-safe. You can share a single instance across y
 
 ```java
 // Create a shared limiter instance
-TokenBucket limiter = new TokenBucket(10, 1, 1.0, jedisPool);
+TokenBucket limiter = new TokenBucket(10, 1, 1.0, jedis);
 
 // Safe to call from multiple threads
 ExecutorService executor = Executors.newFixedThreadPool(10);
@@ -240,10 +240,12 @@ A demonstration HTTP server is included to show the rate limiter in action
 
 ```bash
 # Compile
-javac -cp jedis-5.2.0.jar TokenBucket.java DemoServer.java
+javac -cp jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar \
+    TokenBucket.java DemoServer.java
 
 # Run the demo server
-java -cp .:jedis-5.2.0.jar DemoServer
+java -cp .:jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar \
+    DemoServer
 ```
 
 The demo provides an interactive web interface where you can:
@@ -262,7 +264,7 @@ It's common to include rate limit information in HTTP response headers:
 ```java
 int capacity = 10;
 double refillInterval = 1.0;
-TokenBucket limiter = new TokenBucket(capacity, 1, refillInterval, jedisPool);
+TokenBucket limiter = new TokenBucket(capacity, 1, refillInterval, jedis);
 
 TokenBucket.RateLimitResult result = limiter.allow("user:" + userId);
 
@@ -290,8 +292,8 @@ public class RateLimitFilter implements Filter {
 
     @Override
     public void init(FilterConfig config) {
-        JedisPool jedisPool = new JedisPool("localhost", 6379);
-        limiter = new TokenBucket(10, 1, 1.0, jedisPool);
+        RedisClient jedis = RedisClient.create("localhost", 6379);
+        limiter = new TokenBucket(10, 1, 1.0, jedis);
     }
 
     @Override
@@ -361,8 +363,7 @@ Simplest algorithm — one key per window, one `EVAL` round trip.
 ```java
 import java.util.List;
 
-import redis.clients.jedis.Jedis;
-import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.RedisClient;
 
 public class FixedWindowLimiter {
 
@@ -387,16 +388,14 @@ public class FixedWindowLimiter {
     public record Decision(boolean allowed, long retryAfterMs) {}
 
     @SuppressWarnings("unchecked")
-    public static Decision allow(JedisPool pool, String key, int limit, int windowSeconds) {
-        try (Jedis jedis = pool.getResource()) {
-            List result = (List) jedis.eval(
-                    SCRIPT,
-                    List.of(key),
-                    List.of(String.valueOf(limit), String.valueOf(windowSeconds)));
-
-            boolean allowed = result.get(0) == 1L;
-            return new Decision(allowed, allowed ? 0L : result.get(1));
-        }
+    public static Decision allow(RedisClient jedis, String key, int limit, int windowSeconds) {
+        List result = (List) jedis.eval(
+                SCRIPT,
+                List.of(key),
+                List.of(String.valueOf(limit), String.valueOf(windowSeconds)));
+
+        boolean allowed = result.get(0) == 1L;
+        return new Decision(allowed, allowed ? 0L : result.get(1));
     }
 }
 ```
@@ -413,8 +412,7 @@ Provides a true rolling window with no boundary bursts.
 import java.util.List;
 import java.util.UUID;
 
-import redis.clients.jedis.Jedis;
-import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.RedisClient;
 
 public class SlidingWindowLogLimiter {
 
@@ -450,17 +448,15 @@ public class SlidingWindowLogLimiter {
     public record Decision(boolean allowed, long retryAfterMs) {}
 
     @SuppressWarnings("unchecked")
-    public static Decision allow(JedisPool pool, String key, int limit, int windowSeconds) {
-        try (Jedis jedis = pool.getResource()) {
-            List result = (List) jedis.eval(
-                    SCRIPT,
-                    List.of(key),
-                    List.of(String.valueOf(limit),
-                            String.valueOf(windowSeconds),
-                            UUID.randomUUID().toString()));
-
-            return new Decision(result.get(0) == 1L, result.get(1));
-        }
+    public static Decision allow(RedisClient jedis, String key, int limit, int windowSeconds) {
+        List result = (List) jedis.eval(
+                SCRIPT,
+                List.of(key),
+                List.of(String.valueOf(limit),
+                        String.valueOf(windowSeconds),
+                        UUID.randomUUID().toString()));
+
+        return new Decision(result.get(0) == 1L, result.get(1));
     }
 }
 ```
@@ -478,8 +474,7 @@ to the same slot in Redis Cluster.
 ```java
 import java.util.List;
 
-import redis.clients.jedis.Jedis;
-import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.RedisClient;
 
 public class SlidingWindowCounterLimiter {
 
@@ -515,15 +510,13 @@ public class SlidingWindowCounterLimiter {
             """;
 
     @SuppressWarnings("unchecked")
-    public static boolean allow(JedisPool pool, String key, int limit, int windowSeconds) {
-        try (Jedis jedis = pool.getResource()) {
-            List result = (List) jedis.eval(
-                    SCRIPT,
-                    List.of("{" + key + "}"),
-                    List.of(String.valueOf(limit), String.valueOf(windowSeconds)));
-
-            return result.get(0) == 1L;
-        }
+    public static boolean allow(RedisClient jedis, String key, int limit, int windowSeconds) {
+        List result = (List) jedis.eval(
+                SCRIPT,
+                List.of("{" + key + "}"),
+                List.of(String.valueOf(limit), String.valueOf(windowSeconds)));
+
+        return result.get(0) == 1L;
     }
 }
 ```
@@ -540,8 +533,7 @@ policing variant — requests are allowed or denied instantly with no delay.
 ```java
 import java.util.List;
 
-import redis.clients.jedis.Jedis;
-import redis.clients.jedis.JedisPool;
+import redis.clients.jedis.RedisClient;
 
 public class LeakyBucketLimiter {
 
@@ -586,15 +578,13 @@ public class LeakyBucketLimiter {
     public record Decision(boolean allowed, long retryAfterMs) {}
 
     @SuppressWarnings("unchecked")
-    public static Decision allow(JedisPool pool, String key, int capacity, double leakRate) {
-        try (Jedis jedis = pool.getResource()) {
-            List result = (List) jedis.eval(
-                    SCRIPT,
-                    List.of(key),
-                    List.of(String.valueOf(capacity), String.valueOf(leakRate)));
-
-            return new Decision(result.get(0) == 1L, result.get(1));
-        }
+    public static Decision allow(RedisClient jedis, String key, int capacity, double leakRate) {
+        List result = (List) jedis.eval(
+                SCRIPT,
+                List.of(key),
+                List.of(String.valueOf(capacity), String.valueOf(leakRate)));
+
+        return new Decision(result.get(0) == 1L, result.get(1));
     }
 }
 ```
diff --git a/content/develop/use-cases/session-store/java-jedis/DemoServer.java b/content/develop/use-cases/session-store/java-jedis/DemoServer.java
index aae2f1cda7..77623eba6f 100644
--- a/content/develop/use-cases/session-store/java-jedis/DemoServer.java
+++ b/content/develop/use-cases/session-store/java-jedis/DemoServer.java
@@ -1,8 +1,7 @@
 import com.sun.net.httpserver.HttpExchange;
 import com.sun.net.httpserver.HttpHandler;
 import com.sun.net.httpserver.HttpServer;
-import redis.clients.jedis.JedisPool;
-import redis.clients.jedis.JedisPoolConfig;
+import redis.clients.jedis.RedisClient;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -20,14 +19,14 @@
  * 

Usage:

* *
{@code
- * javac -cp jedis-5.2.0.jar RedisSessionStore.java DemoServer.java
- * java -cp .:jedis-5.2.0.jar DemoServer --port 8080 --redis-host localhost --redis-port 6379
+ * javac -cp jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar RedisSessionStore.java DemoServer.java
+ * java -cp .:jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar DemoServer --port 8080 --redis-host localhost --redis-port 6379
  * }
*/ public class DemoServer { private static RedisSessionStore sessionStore; - private static JedisPool jedisPool; + private static RedisClient jedis; public static void main(String[] args) { int port = 8080; @@ -51,9 +50,9 @@ public static void main(String[] args) { } try { - jedisPool = new JedisPool(new JedisPoolConfig(), redisHost, redisPort); - jedisPool.getResource().close(); - sessionStore = new RedisSessionStore(jedisPool); + jedis = RedisClient.create(redisHost, redisPort); + jedis.ping(); + sessionStore = new RedisSessionStore(jedis); } catch (Exception e) { System.err.printf("Failed to connect to Redis at %s:%d: %s%n", redisHost, redisPort, e.getMessage()); System.exit(1); @@ -73,7 +72,7 @@ public static void main(String[] args) { System.out.printf("Session store demo listening on http://localhost:%d%n", port); Runtime.getRuntime().addShutdownHook(new Thread(() -> { - jedisPool.close(); + jedis.close(); server.stop(0); })); } catch (IOException e) { diff --git a/content/develop/use-cases/session-store/java-jedis/RedisSessionStore.java b/content/develop/use-cases/session-store/java-jedis/RedisSessionStore.java index 9c4ff168ed..006722d1a3 100644 --- a/content/develop/use-cases/session-store/java-jedis/RedisSessionStore.java +++ b/content/develop/use-cases/session-store/java-jedis/RedisSessionStore.java @@ -7,8 +7,7 @@ import java.util.Map; import java.util.Set; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; +import redis.clients.jedis.RedisClient; import redis.clients.jedis.Pipeline; import redis.clients.jedis.Response; @@ -30,22 +29,22 @@ public class RedisSessionStore { private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd'T'HH:mm:ssXXX"); - private final JedisPool jedisPool; + private final RedisClient jedis; private final String prefix; private final int ttl; - public RedisSessionStore(JedisPool jedisPool, String prefix, int ttl) { - if (jedisPool == null) { - throw new IllegalArgumentException("jedisPool is required"); + public RedisSessionStore(RedisClient jedis, String prefix, int ttl) { + if (jedis == null) { + throw new IllegalArgumentException("jedis is required"); } - this.jedisPool = jedisPool; + this.jedis = jedis; this.prefix = prefix == null || prefix.isEmpty() ? "session:" : prefix; this.ttl = normalizeTtl(ttl); } - public RedisSessionStore(JedisPool jedisPool) { - this(jedisPool, "session:", 1800); + public RedisSessionStore(RedisClient jedis) { + this(jedis, "session:", 1800); } private int normalizeTtl(Integer value) { @@ -105,8 +104,7 @@ public String createSession(Map data, Integer ttlOverride) { payload.put("last_accessed_at", now); payload.put("session_ttl", String.valueOf(sessionTtl)); - try (Jedis jedis = jedisPool.getResource()) { - Pipeline pipeline = jedis.pipelined(); + try (Pipeline pipeline = jedis.pipelined()) { pipeline.hset(key, payload); pipeline.expire(key, sessionTtl); pipeline.sync(); @@ -116,31 +114,28 @@ public String createSession(Map data, Integer ttlOverride) { } public Integer getConfiguredTtl(String sessionId) { - try (Jedis jedis = jedisPool.getResource()) { - String storedTtl = jedis.hget(sessionKey(sessionId), "session_ttl"); - if (storedTtl == null) { - return null; - } - - return normalizeTtl(Integer.parseInt(storedTtl)); + String storedTtl = jedis.hget(sessionKey(sessionId), "session_ttl"); + if (storedTtl == null) { + return null; } + + return normalizeTtl(Integer.parseInt(storedTtl)); } public Map getSession(String sessionId, boolean refreshTtl) { String key = sessionKey(sessionId); - try (Jedis jedis = jedisPool.getResource()) { - Map session = jedis.hgetAll(key); - if (!isValidSession(session)) { - return null; - } + Map session = jedis.hgetAll(key); + if (!isValidSession(session)) { + return null; + } - if (!refreshTtl) { - return session; - } + if (!refreshTtl) { + return session; + } - int sessionTtl = normalizeTtl(Integer.valueOf(session.get("session_ttl"))); - Pipeline pipeline = jedis.pipelined(); + int sessionTtl = normalizeTtl(Integer.valueOf(session.get("session_ttl"))); + try (Pipeline pipeline = jedis.pipelined()) { pipeline.hset(key, "last_accessed_at", timestamp()); pipeline.expire(key, sessionTtl); Response> refreshed = pipeline.hgetAll(key); @@ -153,45 +148,43 @@ public Map getSession(String sessionId, boolean refreshTtl) { public boolean updateSession(String sessionId, Map data) { String key = sessionKey(sessionId); - try (Jedis jedis = jedisPool.getResource()) { - Map session = jedis.hgetAll(key); - if (!isValidSession(session)) { - return false; - } + Map session = jedis.hgetAll(key); + if (!isValidSession(session)) { + return false; + } - Map payload = new HashMap<>(); - for (Map.Entry entry : data.entrySet()) { - if (!RESERVED_SESSION_FIELDS.contains(entry.getKey())) { - payload.put(entry.getKey(), String.valueOf(entry.getValue())); - } + Map payload = new HashMap<>(); + for (Map.Entry entry : data.entrySet()) { + if (!RESERVED_SESSION_FIELDS.contains(entry.getKey())) { + payload.put(entry.getKey(), String.valueOf(entry.getValue())); } + } - if (payload.isEmpty()) { - return true; - } + if (payload.isEmpty()) { + return true; + } - int sessionTtl = normalizeTtl(Integer.valueOf(session.get("session_ttl"))); - payload.put("last_accessed_at", timestamp()); + int sessionTtl = normalizeTtl(Integer.valueOf(session.get("session_ttl"))); + payload.put("last_accessed_at", timestamp()); - Pipeline pipeline = jedis.pipelined(); + try (Pipeline pipeline = jedis.pipelined()) { pipeline.hset(key, payload); pipeline.expire(key, sessionTtl); pipeline.sync(); - return true; } + return true; } public Long incrementField(String sessionId, String field, long amount) { String key = sessionKey(sessionId); - try (Jedis jedis = jedisPool.getResource()) { - Map session = jedis.hgetAll(key); - if (!isValidSession(session)) { - return null; - } + Map session = jedis.hgetAll(key); + if (!isValidSession(session)) { + return null; + } - int sessionTtl = normalizeTtl(Integer.valueOf(session.get("session_ttl"))); - Pipeline pipeline = jedis.pipelined(); + int sessionTtl = normalizeTtl(Integer.valueOf(session.get("session_ttl"))); + try (Pipeline pipeline = jedis.pipelined()) { Response value = pipeline.hincrBy(key, field, amount); pipeline.hset(key, "last_accessed_at", timestamp()); pipeline.expire(key, sessionTtl); @@ -204,34 +197,29 @@ public boolean setSessionTtl(String sessionId, int ttlOverride) { String key = sessionKey(sessionId); int sessionTtl = normalizeTtl(ttlOverride); - try (Jedis jedis = jedisPool.getResource()) { - Map session = jedis.hgetAll(key); - if (!isValidSession(session)) { - return false; - } + Map session = jedis.hgetAll(key); + if (!isValidSession(session)) { + return false; + } - Map payload = Map.of( - "session_ttl", String.valueOf(sessionTtl), - "last_accessed_at", timestamp() - ); + Map payload = Map.of( + "session_ttl", String.valueOf(sessionTtl), + "last_accessed_at", timestamp() + ); - Pipeline pipeline = jedis.pipelined(); + try (Pipeline pipeline = jedis.pipelined()) { pipeline.hset(key, payload); pipeline.expire(key, sessionTtl); pipeline.sync(); - return true; } + return true; } public boolean deleteSession(String sessionId) { - try (Jedis jedis = jedisPool.getResource()) { - return jedis.del(sessionKey(sessionId)) == 1L; - } + return jedis.del(sessionKey(sessionId)) == 1L; } public long getTtl(String sessionId) { - try (Jedis jedis = jedisPool.getResource()) { - return jedis.ttl(sessionKey(sessionId)); - } + return jedis.ttl(sessionKey(sessionId)); } } diff --git a/content/develop/use-cases/session-store/java-jedis/_index.md b/content/develop/use-cases/session-store/java-jedis/_index.md index 5db8535bb6..db97e47e73 100644 --- a/content/develop/use-cases/session-store/java-jedis/_index.md +++ b/content/develop/use-cases/session-store/java-jedis/_index.md @@ -50,12 +50,12 @@ The `RedisSessionStore` class wraps the basic session operations ```java import java.util.Map; -import redis.clients.jedis.JedisPool; +import redis.clients.jedis.RedisClient; public class Main { public static void main(String[] args) { - JedisPool jedisPool = new JedisPool("localhost", 6379); - RedisSessionStore store = new RedisSessionStore(jedisPool, "session:", 1800); + RedisClient jedis = RedisClient.create("localhost", 6379); + RedisSessionStore store = new RedisSessionStore(jedis, "session:", 1800); String sessionId = store.createSession(Map.of( "username", "andrew", @@ -67,12 +67,12 @@ public class Main { store.incrementField(sessionId, "page_views", 1); store.deleteSession(sessionId); - jedisPool.close(); + jedis.close(); } } ``` -Jedis operations are synchronous, and `JedisPool` gives you safe connection reuse across requests. +Jedis operations are synchronous, and `RedisClient` gives you safe connection reuse across requests. ### Data model @@ -121,8 +121,7 @@ public String createSession(Map data, Integer ttl) { payload.put("last_accessed_at", now); payload.put("session_ttl", String.valueOf(sessionTtl)); - try (Jedis jedis = jedisPool.getResource()) { - Pipeline pipeline = jedis.pipelined(); + try (Pipeline pipeline = jedis.pipelined()) { pipeline.hset(key, payload); pipeline.expire(key, sessionTtl); pipeline.sync(); @@ -138,18 +137,17 @@ When the application reads a session, it refreshes the configured TTL so active public Map getSession(String sessionId, boolean refreshTtl) { String key = sessionKey(sessionId); - try (Jedis jedis = jedisPool.getResource()) { - Map session = jedis.hgetAll(key); - if (!isValidSession(session)) { - return null; - } + Map session = jedis.hgetAll(key); + if (!isValidSession(session)) { + return null; + } - if (!refreshTtl) { - return session; - } + if (!refreshTtl) { + return session; + } - int sessionTtl = normalizeTtl(Integer.valueOf(session.get("session_ttl"))); - Pipeline pipeline = jedis.pipelined(); + int sessionTtl = normalizeTtl(Integer.valueOf(session.get("session_ttl"))); + try (Pipeline pipeline = jedis.pipelined()) { pipeline.hset(key, "last_accessed_at", timestamp()); pipeline.expire(key, sessionTtl); Response> refreshed = pipeline.hgetAll(key); @@ -175,14 +173,14 @@ Before running the demo, make sure that: redis.clients jedis - 5.2.0 + 8.0.2 ``` * If you use **Gradle**: ```groovy - implementation 'redis.clients:jedis:5.2.0' + implementation 'redis.clients:jedis:8.0.2' ``` If your Redis server is running elsewhere, start the demo with `--redis-host` and `--redis-port`. @@ -205,8 +203,10 @@ curl -O $BASE/DemoServer.java From that directory: ```bash -javac -cp jedis-5.2.0.jar RedisSessionStore.java DemoServer.java -java -cp .:jedis-5.2.0.jar DemoServer +javac -cp jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar \ + RedisSessionStore.java DemoServer.java +java -cp .:jedis-8.0.2.jar:commons-pool2-2.13.1.jar:slf4j-api-1.7.36.jar:gson-2.14.0.jar:json-20260719.jar:redis-authx-core-0.1.1-beta2.jar \ + DemoServer ``` The demo server uses standard Java libraries for HTTP handling: