Skip to content

Ask the HotKV docs

Answers come only from these docs and can be wrong; check the linked pages before you rely on them.

Transactions

Last updated

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.

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> MULTI
OK
127.0.0.1:6379(TX)> INCR account:42:balance
QUEUED
127.0.0.1:6379(TX)> LPUSH account:42:history "deposit"
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (integer) 1
2) (integer) 1

DISCARD drops the queued commands and ends the transaction without running anything:

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET draft "not saved"
QUEUED
127.0.0.1:6379(TX)> DISCARD
OK
127.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.

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> MULTI
OK
127.0.0.1:6379(TX)> SET a 1
QUEUED
127.0.0.1:6379(TX)> INCR
(error) ERR wrong number of arguments for 'incr' command
127.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"
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> INCR name
QUEUED
127.0.0.1:6379(TX)> SET visited "yes"
QUEUED
127.0.0.1:6379(TX)> EXEC
1) (error) ERR value is not an integer or out of range
2) OK

EXEC 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.

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:widget
OK
127.0.0.1:6379> GET stock:widget
"5"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379(TX)> SET stock:widget 4
QUEUED
127.0.0.1:6379(TX)> EXEC
1) OK

If 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 WatchError

client.transaction() watches the keys, runs your function, and retries (up to max_retries, 5 by default) when a watched key changed.

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.

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.

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.

MULTI, EXEC, DISCARD, WATCH and UNWATCH behave as in Redis 7, including the EXECABORT replies and the absence of rollback.