ХабрПостыНовостиМегапосты?

Транзакции в redis

Предыдущие:

Когда начинают говорить о транзакциях в redis некоторым представляется в голове что‑то типа «postgresql»: «...Суть транзакции в том, что она объединяет последовательность действий в одну операцию „всё или ничего“. Промежуточные состояния внутри последовательности не видны другим транзакциям, и если что‑то помешает успешно завершить транзакцию, ни один из результатов этих действий не сохранится в базе данных...». Если говорить в ЭТОМ контексте, то транзакций в redis НЕТ и мы имеем дело с морской свинкой («не морская и не свинка»). А что же есть и как это можно мониторить?

Начнём с мануала:

«All the commands in a transaction are serialized and executed sequentially. A request sent by another client will never be served in the middle of the execution of a Redis Transaction.
This guarantees that the commands are executed as a single isolated operation....Errors happening after EXEC instead are not handled in a special way: all the other commands will be executed even if some command fails during the transaction.» Действительно, атомарность из ACID проходит мимо кассы. Далее слово «транзакция» правильнее писать в кавычках, но буду без.

WATCHED

Для начала рассмотрим как устроен аналог "оптимистичной" блокировки.
у каждой БД redis есть хештаблица

typedef struct redisDb {
...
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
...

в ключе которой ключ базы, в значении список клиентов, которым посредством команды WATCHED «интересен» этот ключ. Поэтому watched он сугубо в контексте базы. Под клиентом понимается каждое конкретное подключение клиентского приложения к серверу redis.

В режиме MULTI команда watched недопустима — она в этом режиме бессмысленна. Так же если клиент помечен как DIRTY — ключ в WATCHED не добавится. Хотя ответ будет ОК.

Список watched ключей так же хранится в структуре каждого подключения

```
typedef struct client {
...
list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */
...
}

typedef struct watchedKey {
	listNode node;
	robj *key;
	redisDb *db;
	client *client;
	unsigned expired:1; /* Flag that we're watching an already expired key. */
} watchedKey;
```

Наличие этой структуры учитывается в расчёте показателя tot-mem в выдаче client list для конкретного клиента

При любой модификации ключа производится вызов метода touchWatchedKey или touchAllWatchedKeysInDb (для команд типа flushdb, которые аффектят все ключи базы). Метод проходится по клиентам, которые в режиме watch указанных (или всех) кючей и помечает их флагом CLIENT_DIRTY_CAS. Если клиент помечен как CLIENT_DIRTY_CAS, то его команды в режиме MULTI (!!!!!!) не выполняются.

127.0.0.1:6379> watch k1
OK
/// В ПАРАЛЛЕЛЬНОМ клиенте меняем set k1 r1
127.0.0.1:6379> set k1 r2
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 r3
QUEUED
127.0.0.1:6379(TX)> exec
(nil)
127.0.0.1:6379> get k1
"r2"

Как видим, если команда не в режиме multi, не в «транзакции» — она выполнится. Вторая команда set выполнена в режиме multi — оптимистичная блокировка сработала, exec выдал nil.

Поскольку watch реализована в виде хеш‑таблицы, то на эффективность её работы могут накладывать факты перехеширования, если она будет часто и радикально меняться в размерах (об это рассказано в предыдущей статье). Но касательно этой таблицы метрик скорости перехеширования нет.

MULTI

Итак, транзакция начинается с команды MULTI. Это просто флаг, что далее все команды, кроме завершающих транзакцию от клиента будут «накапливаться» на сервере в буфере.

Вложенных транзакций быть не может и при попытке повторной multi возвращается ошибка.

void multiCommand(client *c) {
	if (c->flags & CLIENT_MULTI) {
	addReplyError(c,"MULTI calls can not be nested");
	return;
	}
	c->flags |= CLIENT_MULTI;
	addReply(c,shared.ok);
}

Так же использование в режиме MULTI команды WATCHED — запрещено ибо опять таки бессмысленно. Можно либо добавить в пачку команд новую на выполнение, либо завершить её (exec, discard, reset). При выставленном флаге multi производит накопление команд в структуре multiState:

typedef struct multiState {
    multiCmd *commands;     /* Array of MULTI commands */
    int count;              /* Total number of MULTI commands */
    int cmd_flags;          /* The accumulated command flags OR-ed together.
                               So if at least a command has a given flag, it
                               will be set in this field. */
    int cmd_inv_flags;      /* Same as cmd_flags, OR-ing the ~flags. so that it
                               is possible to know if all the commands have a
                               certain flag. */
    size_t argv_len_sums;    /* mem used by all commands arguments */
    int alloc_count;         /* total number of multiCmd struct memory reserved. */
} multiState;

Каждая новая команда сохраняется в массиве команд при следующих условиях:

Для просмотра что физически происходит «на той стороне» можно использовать команду `client list id {clientid}. Нас интересуют следующая информация

Пример (вывод `client list id 4` при вводе команд в режиме MULTI в соседнем подключении):

//имели на старте
id=4 addr=127.0.0.1:53114 ...  flags=N ... multi=-1 watch=0 ... multi-mem=0
// в клиенте id=4 ввели команду multi
id=4 addr=127.0.0.1:53114 ... flags=x ... multi=0 watch=0 ... multi-mem=0
//добавили в очередь set k1 1 и set k2 2
id=4 addr=127.0.0.1:53114 ... flags=x .. multi=2 watch=0 ... multi-mem=60

Добавление в массив команд для последующего выполнения в батче всегда отдаст QUEUED. Даже если клиент помечен как DIRTY и реально в массив ничего не положено.
Если мы складываем команды в multi-режиме достаточно долго или конкуренция за watched ключи высока, может так статься, что эти манипуляции c redis мы будем проводить вхолостую.

EXEC

Существует определённый набор проверок при процессинге команды при непрохождении которых, клиенту выставляется флаг CLIENT_DIRTY_EXEC.

Флаг действует аналогично CLIENT_DIRTY_CAS, но как-то посмотреть «снаружи», что он выставлен мы не можем.

EXEC выполняется с предварительными проверками на возможность выставления клиенту CLIENT_DIRTY_EXEC. При предварительно забитой очереди команд выполняется по следующей схеме:

т.о. в как минимум в 4х случаях в ответе по каждой queued команде будет ошибка:

Пример с блокирующей командой blpop:

127.0.0.1:6379> multi
OK
127.0.0.1:6379(TX)> set k1 2
QUEUED
127.0.0.1:6379(TX)> blpop l1 300
QUEUED
127.0.0.1:6379(TX)> exec
1) OK
2) (nil)
127.0.0.1:6379>

Команда DISCARD: просто заново инициализирует multiState и, что важно, очищает WATCHED ключи. RESET и QUIT в контексте multi-режима делают то же самое.

Мониторинг

Что имеем в мониторинге помимо clients list показанного выше? Почти ничего
`Info clients`, но в данном случае общая статистика по всем базам сервера:

@XelaVopelk
27.05.2025 20:18 UTC
Первоисточник

Комментарии

@LeshaRB
28.05.2025 06:49 UTC
-1

Часть 1: Дизайн hash-таблиц в redis
Часть 2: Удаление в Redis сущностей с истекшим сроком действия
Транзакции в redis

Вот например я новый.... Хочу обучиться Redis .... ПО какому принципу в одной статье пишется redis, а в другой Redis?

28.05.2025 07:59 UTC
0

не задавался таким вопросом. вероятнее всего в следующей будет с маленькой буквы.

28.05.2025 08:59 UTC
+1

если Вы новичок в Redis, то советую начать с каких-то других материалов.