Предыдущие:

Когда начинают говорить о транзакциях в 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 недопустима — она в этом режиме бессмысленна. Так же если клиент помечен как DURTY — ключ в 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;
Каждая новая команда сохраняется в массиве команд при следующих условиях:
клиент не помечен как dirty. Действительно, какой смысл ставить команды в очередь, если в конечном итоге они не будут выполнены.
В кластерном режиме происходит проверка, что ВСЕ ключи новой команды попадут в тот же слот, что и остальные команды уже лежащие в multiState.commands. Это ограничение кластерного режима для работы в режиме MULTI. При непопадании инкрементим метрику: cluster_incompatible_ops — в
INFO STATS
— ну и естественно эта команда выполнится неуспешно.не вышли за ограничение размера накопления для клиента:
client-query-buffer-limit
1gb по умолчанию. Сложно представить, что мы выйдем за ограничение, но тем не менее.
Для просмотра что физически происходит «на той стороне» можно использовать команду `client list id {clientid}. Нас интересуют следующая информация
flags — в режиме multi в нем появляется флаг
x
, один из WATCHED ключей клиента изменен —d
— показывает выставление CLIENT_DIRTY_CASmulti — количество накопленных команд в multiState.commands
multi‑mem — multiState.argv_len_sums — память использованная под хранение аргументов команд.
Пример (вывод `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.
нужна аутентификация
достигли предела использования памяти и процедура очистки не увенчалась успехом
не удовлетворяем выставленному параметру min-slaves-to-write
попытка неразрешённой записи в slave
БД в процессе загрузки
утрата реплики связи с мастером
exec или multi поступают во время выполнения lua скрипта
Флаг действует аналогично CLIENT_DIRTY_CAS, но как-то посмотреть «снаружи», что он выставлен мы не можем.
EXEC выполняется с предварительными проверками на возможность выставления клиенту CLIENT_DIRTY_EXEC. При предварительно забитой очереди команд выполняется по следующей схеме:

т.о. в как минимум в 4х случаях в ответе по каждой queued команде будет ошибка:
ошибка выполнения (например инкремент строки)
вызов блокирующей команды
ACL
команда попала не свой слот если у нас кластерный режим
Пример с блокирующей командой 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`, но в данном случае общая статистика по всем базам сервера:
watching_clients — всего в инстенсе клиентов в режиме watching
total_watched_keys — сумма длинн хештаблиц watched_keys всех баз