# Transactions

> Run a group of commands as one isolated step with MULTI and EXEC, and use WATCH for optimistic locking.

URL: https://docs.hotkv.com/develop/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](https://docs.hotkv.com/commands/multi/index.md), [EXEC](https://docs.hotkv.com/commands/exec/index.md),
[DISCARD](https://docs.hotkv.com/commands/discard/index.md), [WATCH](https://docs.hotkv.com/commands/watch/index.md) and
[UNWATCH](https://docs.hotkv.com/commands/unwatch/index.md).

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

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

## 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: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`.

**Python**

```python
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.
**Node.js**

```ts
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`.
**Go**

```go
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`.
**Java**

```java
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

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](https://docs.hotkv.com/commands/blpop/index.md) or
[XREAD](https://docs.hotkv.com/commands/xread/index.md) 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

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](https://docs.hotkv.com/clustering/index.md).

## Transactions or scripts?

A [Lua script](https://docs.hotkv.com/develop/lua-scripting/index.md) or [function](https://docs.hotkv.com/develop/functions/index.md) 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

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