Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров3.8K

Предыдущие:

Когда начинают говорить о транзакциях в 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_CAS

  • multi — количество накопленных команд в 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 всех баз

Теги:
Хабы:
+8
Комментарии3

Публикации

Ближайшие события