ClickHouse: транзакции и блокировки¶
ClickHouse выполняет транзакции над таблицами семейства MergeTree на сервере с ClickHouse Keeper. Это
экспериментальная возможность сервера, поэтому hare пользуется ею, только когда об этом просит
подключение. Блокировок строк в ClickHouse нет.
Транзакции¶
config = {
"connections": {
"analytics": "clickhouse+clickhouse-connect://default:secret@clickhouse.local:8123/analytics?transactions=true"
}
}
С transactions=true Transactions.atomic() выполняет транзакцию ClickHouse. Сервер должен работать с
ClickHouse Keeper и включённой allow_experimental_transactions: подключение проверяет это при открытии
командами BEGIN TRANSACTION и ROLLBACK, а иначе даёт ConfigurationError. Без transactions=true
atomic() даёт UnSupportedError. Transactions.autonomous() с параметром (isolation, read_only, ...)
тоже выполняет транзакцию — на своём новом соединении, с откатом, если блок бросает исключение.
async with Transactions.atomic():
await Loan.objects.create(id=1, book_id=7, reader_id=3)
await Inventory.objects.filter(book_id=7).update(available=0)
await Reservation.objects.filter(book_id=7).delete()
- Собственное соединение. Транзакция держит HTTP-сессию (clickhouse-connect) или TCP-соединение
(clickhouse-driver) от
BEGIN TRANSACTIONдоCOMMIT/ROLLBACK; её команды выполняются на нём по одной, команды параллельных задач ждут своей очереди. - Что в ней выполняется. Вставки — и двоичные вставки
bulk_create(), —SELECT, мутации и лёгкийDELETEтаблицMergeTree. Команда, которую сервер не выполняет в транзакции, — DDL, запрос к таблицеsystem, хранилище которой не участвует в транзакциях (system.parts;system.oneиsystem.tablesчитаются), любая команда таблицыReplicatedилиDistributed(модели сdistributed_over) — даётUnSupportedError. - Первая неудачная команда завершает транзакцию. Сервер откатывает её и принимает только
ROLLBACK: следующая команда даётTransactionManagementError, как иCOMMIT— после отката. Команда, которую клиент перестал ждать (отменённая задача,asyncio.wait_for), продолжает выполняться в HTTP-сессии транзакции; откат ждёт её — не дольше таймаута сессии сервера, — и строки транзакции не остаются видимыми. - Точек сохранения нет. Вложенный
atomic()присоединяется к транзакции, в которую вложен. Ошибку, покинувшую вложенный блок, нельзя отменить отдельно от остального: даже если внешний блок её перехватит, транзакция при завершении откатывается и даётTransactionManagementErrorс этой ошибкой. - Снимок. Транзакция читает строки такими, какими они были при её начале, вместе со своими записями:
isolationдоREPEATABLE_READвыполняется на этом уровне,SERIALIZABLEдаётUnSupportedError.read_only=True,statement_timeoutиlock_timeoutдаютUnSupportedError— у ClickHouse нет ничего из этого для транзакции и нет блокировок, которых можно ждать. - Чтение вне транзакции видит незафиксированные строки. Команда вне всякой транзакции читает строки,
которые другие транзакции записали, но ещё не зафиксировали. Читатель, которому их видеть нельзя,
выполняется в собственной транзакции или с настройкой
implicit_transaction=1. - Часть, которую держит другая транзакция. Мутация транзакции (
update(), изменение при захвате изменений) может наткнуться на часть таблицы, которую держит фоновое слияние сервера; сервер отклоняет её ошибкой сериализации, которую hare бросает какTransactionRetryError: транзакция откатывается, а её повтор удаётся, когда слияние закончится (это делаетatomic(retries=...)). Запись модели сMeta.change_captureидёт в такой транзакции и безatomic().
Генерируемые ключи¶
Модель, ключ которой генерирует база, берёт ключи из серии ClickHouse Keeper (generateSerialID) —
см. Модели. Серии не нужен transactions=true.
Блокировки строк¶
ClickHouse не блокирует строк: select_for_update() даёт UnSupportedError до отправки SQL — в транзакции
и вне её, — и так же RowChanges.get_for_update() запроса из запроса API. Запись не может дождаться
окончания другой: два update() одной строки применяются оба, и остаются значения более поздней мутации.
ClickHouse Keeper (keeper_hosts) хранит блокировку миграций
migrate, а удаление базы (db_delete()) удаляет её узлы там — серии её ключей и узел её
блокировки; hare сам говорит с ним по протоколу ZooKeeper — библиотека для этого не ставится.