Transactions
A transaction runs a group of commands as a single step: no other client’s command runs in the middle of it, and other clients see either none or all of its writes. HotKV transactions use the same commands as Redis: MULTI, EXEC, DISCARD, WATCH and UNWATCH.
Queue, then execute
Section titled “Queue, then execute”MULTI starts a transaction on the connection. Every command after it is
checked and queued (the server replies QUEUED) instead of being run. EXEC
runs the queued commands in order and replies with an array holding each
command’s reply.
127.0.0.1:6379> MULTIOK127.0.0.1:6379(TX)> INCR account:42:balanceQUEUED127.0.0.1:6379(TX)> LPUSH account:42:history "deposit"QUEUED127.0.0.1:6379(TX)> EXEC1) (integer) 12) (integer) 1DISCARD drops the queued commands and ends the transaction without running
anything:
127.0.0.1:6379> MULTIOK127.0.0.1:6379(TX)> SET draft "not saved"QUEUED127.0.0.1:6379(TX)> DISCARDOK127.0.0.1:6379> GET draft(nil)The commands of a transaction are sent to replicas and written to the append-only file together, as one unit.
Errors
Section titled “Errors”A transaction can fail in two different ways, and they behave differently.
An error while queuing. If a command is rejected when you send it (an
unknown command, the wrong number of arguments, a command that is not allowed
in a transaction, or a key on another cluster node), the server replies with
the error at once and marks the transaction as failed. EXEC then runs
nothing and replies EXECABORT:
127.0.0.1:6379> MULTIOK127.0.0.1:6379(TX)> SET a 1QUEUED127.0.0.1:6379(TX)> INCR(error) ERR wrong number of arguments for 'incr' command127.0.0.1:6379(TX)> EXEC(error) EXECABORT Transaction discarded because of previous errors.An error while running. A command can be well formed but fail when it
runs, for example INCR on a key that holds text. That command’s slot in the
EXEC reply holds the error, and every other command still runs. There is no
rollback: writes made by the other commands stay.
127.0.0.1:6379> SET name "Ada"OK127.0.0.1:6379> MULTIOK127.0.0.1:6379(TX)> INCR nameQUEUED127.0.0.1:6379(TX)> SET visited "yes"QUEUED127.0.0.1:6379(TX)> EXEC1) (error) ERR value is not an integer or out of range2) OKEXEC can also discard the whole transaction when the server cannot accept
writes at that moment, for example when it is over maxmemory with nothing
left to evict, or it is a read-only replica. The reply is then
EXECABORT Transaction discarded because of: followed by the reason.
Optimistic locking with WATCH
Section titled “Optimistic locking with WATCH”WATCH key [key ...] makes the next EXEC conditional: if any watched key is
changed by anyone (or expires) between the WATCH and the EXEC, the
transaction does not run and EXEC replies with a null array. Use it to
implement check-and-set: read a value, decide what to write, and write it only
if nobody changed the value in between.
127.0.0.1:6379> WATCH stock:widgetOK127.0.0.1:6379> GET stock:widget"5"127.0.0.1:6379> MULTIOK127.0.0.1:6379(TX)> SET stock:widget 4QUEUED127.0.0.1:6379(TX)> EXEC1) OKIf another client had changed stock:widget before the EXEC, the reply
would have been (nil) and nothing would have been written. Retry the whole
read-decide-write cycle when that happens.
EXEC and DISCARD clear the watched keys; UNWATCH clears them without
ending anything. WATCH must come before MULTI: inside a transaction it
replies ERR WATCH inside MULTI is not allowed.
import hotkv
client = hotkv.HotKV(host="localhost", port=6379)
def take_one(pipe): stock = int(pipe.get("stock:widget") or 0) # runs at once, after WATCH pipe.multi() # queue from here on pipe.set("stock:widget", stock - 1)
client.transaction(take_one, "stock:widget") # retries on WatchErrorclient.transaction() watches the keys, runs your function, and retries (up
to max_retries, 5 by default) when a watched key changed.
import { HotKV } from "@hotkv/client";
const client = new HotKV({ url: "hotkv://127.0.0.1:6379" });
const tx = await client.watch("stock:widget");const stock = Number(await client.get("stock:widget"));tx.set("stock:widget", String(stock - 1));
const results = await tx.exec();if (results === null) { // stock:widget changed after watch(): retry}client.multi() starts a transaction without WATCH.
pipe := client.TxPipeline()pipe.Incr(ctx, "account:42:balance")pipe.LPush(ctx, "account:42:history", "deposit")if _, err := pipe.Exec(ctx); err != nil { log.Fatal(err)}TxPipeline() wraps the queued commands in MULTI/EXEC.
import com.hotkv.Transaction;
try (Transaction tx = client.transaction()) { tx.watch("stock:widget"); long stock = Long.parseLong(client.get("stock:widget")); tx.set("stock:widget", Long.toString(stock - 1));
List<Object> results = tx.exec(); if (results == null) { // stock:widget changed after watch(): retry }}What a transaction may not contain
Section titled “What a transaction may not contain”These are refused while queuing, which makes EXEC reply EXECABORT:
| Command | Reply inside MULTI |
|---|---|
SAVE, SHUTDOWN, SYNC, PSYNC |
ERR Command not allowed inside a transaction |
MULTI |
ERR MULTI calls can not be nested |
WATCH |
ERR WATCH inside MULTI is not allowed |
QUIT and RESET run immediately even inside a transaction (and RESET
discards it).
Blocking commands such as BLPOP or
XREAD with BLOCK never wait inside a transaction: they
answer at once, as if their timeout had already passed, with the data that is
there or a null reply.
Transactions in a cluster
Section titled “Transactions in a cluster”All the keys of a transaction must hash to the same slot, and that slot must
be served by the node you are connected to. A command whose key is on another
node is refused while queuing with MOVED (or ASK), so the transaction
aborts; keys of different slots make EXEC reply CROSSSLOT. Give related
keys the same hash tag, for example account:{42}:balance and
account:{42}:history, to keep them together. See Clustering.
Transactions or scripts?
Section titled “Transactions or scripts?”A Lua script or function also
runs as one isolated step, and it can read values and branch on them inside
that step, without the WATCH and retry loop. Prefer a script when the
writes depend on what you read; prefer MULTI/EXEC for a fixed batch of
writes.
Compatibility
Section titled “Compatibility”MULTI, EXEC, DISCARD, WATCH and UNWATCH behave as in Redis 7,
including the EXECABORT replies and the absence of rollback.
Was this page helpful?
Thanks for the feedback.