Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions docs/transactions-multi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# Transactions/Multi

## Overview

Transactions guarantee that all the included commands will execute to completion without being interrupted by commands from other clients. See the [Transactions](https://redis.io/docs/latest/develop/interact/transactions/) page for more information.

To execute commands in a transaction, create a transaction object with the `multi()` command, call command methods on that object, and then call the transaction object's `exec()` method to execute it. You can access the results from commands in the transaction using `Response` objects. The `exec()` method also returns a `List<Object>` value that contains all the result values in the order the commands were executed.


## Immediate Transaction Start

The simplest way to use transactions is to call `multi()` on your Jedis client, which immediately starts a transaction by sending the `MULTI` command to Redis. All subsequent commands are queued until `exec()` is called.

```java
import redis.clients.jedis.RedisClient;
import redis.clients.jedis.AbstractTransaction;
import redis.clients.jedis.Response;
import java.util.List;

RedisClient jedis = RedisClient.create("redis://localhost:6379");

// Create a transaction that immediately sends MULTI
try (AbstractTransaction tx = jedis.multi()) {
// Commands are queued
Response<String> set1 = tx.set("counter:1", "0");
Response<Long> incr1 = tx.incrBy("counter:1", 1);
Response<Long> incr2 = tx.incrBy("counter:1", 2);

// Execute the transaction
List<Object> results = tx.exec();

// Access results via Response objects
System.out.println(incr1.get()); // 1
System.out.println(incr2.get()); // 3

// Or via the results list
System.out.println(results.get(0)); // OK
System.out.println(results.get(1)); // 1
System.out.println(results.get(2)); // 3
}

jedis.close();
```

### Response Handling

Commands invoked within a transaction return `Response<T>` objects. These responses become available only after `exec()` is called:

- Before `exec()`: Calling `response.get()` will throw `IllegalStateException` with the message "Please close pipeline or multi block before calling this method."
- After `exec()`: Response objects contain the actual results from Redis

The `exec()` method returns a `List<Object>` containing all command results in the order they were queued.

## Manual Transaction Start

For more control, you can create a transaction without immediately sending `MULTI`. This is useful when you need to:

- Execute commands before starting the transaction
- Use `WATCH` to implement optimistic locking
- Conditionally start the transaction

Create a manual transaction by passing `false` to the `transaction()` method:

```java
RedisClient jedis = RedisClient.create("redis://localhost:6379");

// Create transaction without sending MULTI
try (AbstractTransaction tx = jedis.transaction(false)) {

// Commands before multi() are executed immediately
Response<String> setBeforeMulti = tx.set("mykey", "initial_value");
Response<String> getBeforeMulti = tx.get("mykey");

// These responses are available immediately
System.out.println(setBeforeMulti.get()); // OK
System.out.println(getBeforeMulti.get()); // initial_value

// Now start the transaction
tx.multi();

// Commands after multi() are queued
Response<String> set = tx.set("mykey", "new_value");
Response<String> get = tx.get("mykey");

// Execute the transaction
List<Object> results = tx.exec();

// Results from queued commands
System.out.println(set.get()); // OK
System.out.println(get.get()); // new_value
}

jedis.close();
```

### Using WATCH for Optimistic Locking

The `WATCH` command monitors keys for changes. If any watched key is modified before `EXEC`, the transaction is aborted and `exec()` returns `null`.

```java
try (AbstractTransaction tx = jedis.transaction(false)) {
tx.watch("counter");

// Read current value (executed immediately)
String current = jedis.get("counter");
int newValue = Integer.parseInt(current) + 1;

// Start transaction and queue update
tx.multi();
tx.set("counter", String.valueOf(newValue));

// Returns null if key was modified by another client
List<Object> results = tx.exec();

if (results == null) {
System.out.println("Transaction aborted - key was modified");
}
}
```

## Connection Lifecycle

A transaction acquires a dedicated connection that is held until `close()` is called. Ensure the transaction is closed on error:

```java
try (AbstractTransaction tx = jedis.multi()) {
tx.set("key", "value");
tx.exec();
}
```

## Transaction Completion

Complete a transaction by calling either:

- **`exec()`** - Executes all queued commands atomically and returns a `List<Object>` with the results
- **`discard()`** - Discards all queued commands without executing them

### Automatic Cleanup

When using try-with-resources, `close()` automatically sends `DISCARD` (if in `MULTI` state) or `UNWATCH` (if in `WATCH` state) to ensure the connection is returned to the pool in a clean state.

2 changes: 2 additions & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ markdown_extensions:
nav:
- Home: index.md
- Jedis Maven: jedis-maven.md
- User Guide:
- Transactions/Multi: transactions-multi.md
- Migrating to newer versions:
- Jedis 7: migration-guides/v6-to-v7.md
- Jedis 6: migration-guides/v5-to-v6.md
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,7 @@
<include>**/*Hotkeys*.java</include>
<include>**/TestDataUtil*.java</include>
<include>**/*JedisClientConfig*.java</include>
<include>**/RedisClientTransactionIT.java</include>
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This formatter include looks redundant because the configuration already includes **/*IT.java, which should match RedisClientTransactionIT.java. Keeping both makes the include list harder to maintain; consider removing this specific include unless there is a formatter-matching edge case.

Suggested change
<include>**/RedisClientTransactionIT.java</include>

Copilot uses AI. Check for mistakes.
<include>**/resps/LibraryInfoTest.java</include>
</includes>
</configuration>
Expand Down
27 changes: 27 additions & 0 deletions src/main/java/redis/clients/jedis/Response.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,31 @@ public String toString() {
return "Response " + builder.toString();
}

static class DecodedResponse<T> extends Response<T> {
private final T response;
private final JedisDataException exception;

DecodedResponse(T response, JedisDataException exception) {
super(null);
this.exception = exception;
this.response = response;
}
Comment on lines +82 to +86
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DecodedResponse calls super(null), which leaves Response.builder null. Since Response.toString() dereferences builder, calling toString() on a decoded Response will throw a NullPointerException (e.g., in logs/debugging or assertion failure output). Consider keeping builder non-null (e.g., an identity Builder) and using the normal Response#set/build path, or override toString() in DecodedResponse to avoid NPEs.

Copilot uses AI. Check for mistakes.

@Override
public T get() {
if (exception != null) {
throw exception;
}

return response;
}
}

static <T> Response<T> of(T response) {
return new DecodedResponse<>(response, null);
}

static <T> Response<T> error(JedisDataException exception) {
return new DecodedResponse<>(null, exception);
}
}
23 changes: 19 additions & 4 deletions src/main/java/redis/clients/jedis/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,25 @@ public String unwatch() {

@Override
protected final <T> Response<T> appendCommand(CommandObject<T> commandObject) {
connection.sendCommand(commandObject.getArguments());
// processAppendStatus(); // do nothing
Response<T> response = new Response<>(commandObject.getBuilder());
pipelinedResponses.add(response);
Response<T> response = null;
if (!inMulti) {
response = execute(commandObject);
} else {
connection.sendCommand(commandObject.getArguments());
response = new Response<>(commandObject.getBuilder());
pipelinedResponses.add(response);
}
return response;
}

private <T> Response<T> execute(CommandObject<T> commandObject) {
Response<T> response;
try {
T result = connection.executeCommand(commandObject);
response = Response.of(result);
} catch (JedisDataException e) {
response = Response.error(e);
}
return response;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.ArrayList;
Expand All @@ -19,7 +18,6 @@
import org.junit.jupiter.params.provider.MethodSource;

import redis.clients.jedis.AbstractPipeline;
import redis.clients.jedis.AbstractTransaction;
import redis.clients.jedis.RedisProtocol;
import redis.clients.jedis.Response;
import redis.clients.jedis.UnifiedJedis;
Expand Down Expand Up @@ -87,66 +85,6 @@ public void pipeline() {
}
}

@Test
public void transaction() {
final int count = 10;
int totalCount = 0;
for (int i = 0; i < count; i++) {
jedis.set("foo" + i, "bar" + i);
}
totalCount += count;
for (int i = 0; i < count; i++) {
jedis.rpush("foobar" + i, "foo" + i, "bar" + i);
}
totalCount += count;

List<Object> responses;
List<Object> expected = new ArrayList<>(totalCount);

try (AbstractTransaction transaction = jedis.multi()) {
for (int i = 0; i < count; i++) {
transaction.get("foo" + i);
expected.add("bar" + i);
}
for (int i = 0; i < count; i++) {
transaction.lrange("foobar" + i, 0, -1);
expected.add(Arrays.asList("foo" + i, "bar" + i));
}
responses = transaction.exec();
}

for (int i = 0; i < totalCount; i++) {
assertEquals(expected.get(i), responses.get(i));
}
}

@Test
public void watch() {
try (AbstractTransaction tx = jedis.transaction(false)) {
assertEquals("OK", tx.watch("mykey", "somekey"));
tx.multi();

jedis.set("mykey", "bar");

tx.set("mykey", "foo");
assertNull(tx.exec());

assertEquals("bar", jedis.get("mykey"));
}
}

@Test
public void publishInTransaction() {
try (AbstractTransaction tx = jedis.multi()) {
Response<Long> p1 = tx.publish("foo", "bar");
Response<Long> p2 = tx.publish("foo".getBytes(), "bar".getBytes());
tx.exec();

assertEquals(0, p1.get().longValue());
assertEquals(0, p2.get().longValue());
}
}

@Test
public void broadcast() {

Expand All @@ -164,10 +102,10 @@ public void broadcast() {
}

@Test
@SinceRedisVersion(value="7.0.0")
@SinceRedisVersion(value = "7.0.0")
public void broadcastWithError() {
JedisDataException error = assertThrows(JedisDataException.class,
() -> jedis.functionDelete("xyz"));
() -> jedis.functionDelete("xyz"));
assertEquals("ERR Library not found", error.getMessage());
}

Expand Down
Loading
Loading