-
Notifications
You must be signed in to change notification settings - Fork 3.9k
fix: Fix out-of-order responses in manual transactions with commands before MULTI #4441
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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
|
||
|
|
||
| @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); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
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.