Comments 14
Раньше таблицы расширялись только на одну страницу за раз. Это вызывало проблемы при одновременной вставке строк несколькими процессами, поэтому в версии PostgreSQL 9.6 сделали так, чтобы к таблицам добавлялось сразу несколько страниц (пропорционально числу ожидающих блокировку процессов, но не более 512).
Интересен тогда кейс append-only:
* Пусть у нас идет интенсивная вставка значений в таблицу, допустим 1000 строк в секунду.
* Пусть так получилось, что это не batch — именно 1000 в секунду, причем это делают разные процессы. Извиняюсь за натянутость кейса, он специально преувеличен, чтобы понять суть.
* Пусть строки достаточно «тяжелые».
То есть вероятен описанный выше кейс, когда разные процессы начнут расширять таблицу на новую страницу. Пусть мы достигли предела и расширились на 512 страниц за раз из разных процессов.
А потом продолжили вставку новых значений (она не останавливалась).
Значит ли это все, что строки будут лежать в файлах сильно фрагментировано? То есть мы ожидаем append-only и вставку «строка за строкой, id за id». А из за такого вот расширения упорядоченность строк будет сильно нарушена. Что, в частности, помешает эффективному использованию индекса BRIN
Владимир, добрый день!
Если строки добавляются в таблицу многими процессами параллельно, то "строка за строкой" все равно не получится, независимо от того, на сколько страниц за раз расширяется таблица. Все равно строки будут как-то произвольно перемешаны.
Но BRIN-у это не должно мешать, потому что локальность все равно будет соблюдаться. Ведь даже если в пределах страницы строки перемешаны, все id страницы P окажутся меньше любого id страницы Q, если Q немного отстоит от P.
и расширились на 512 страниц за раз из разных процессов
Только не из разных, а из одного. Для того и блокировка.
В приведенном примере блокировка действует до конца сеанса, а не транзакции, как обычно.
У меня был случай, когда в силу недостатков архитектуры нельзя было явно поставить уникальность на вставку нового значения. Чтобы защититься от дубликатов, я реализовал REDIS lock. Перед тем как осуществить действие, происходит попытка захвата блокировки действия. После успешного завершения действия (или ошибки) — блокировка снимается. Или же она снимается по таймауту.
Получается, что вместо этого можно было бы использовать рекомендательные блокировки? Заблокировать условный ресурс и быть уверенным, что другой сеанс будет ждать освобождения ресурса? Разве что кажется, что есть недостаток. Если сеанс внезапно завершается (обрывается соединение по какой то причине, если я понимаю правильно, что такое сеанс) — то блокировка мгновенно опускается. С другой стороны, раз блокировки нет, то действие откатилось и вроде все хорошо.
Однако как быть, если сеанс зависнет и блокировка тоже застрянет? Тут тогда таймаут снятия блокировки будет равен таймауту завершения сеанса?
Я правильно понял, что смысл этой блокировки был в том, чтобы сериализовать вставку, то есть чтобы никто не вставлял новые значения, пока идет проверка уникальности? А как была устроена проверка? И что мешало просто объявить уникальность в базе?
Изначально была сделана просто вставка событий id + JSONB. Среди этих событий было голосование за публикации. Конечно, в итоге была сделана отдельная таблица для событий голосования с подходящим уникальным индексом. Но до этого, в качестве быстрого решения была сделана распределенная блокировка на REDIS. Она впоследствии осталась для соблюдения условия «пользователь может сделать только одно социальное действие (голосование либо публикацию комментария и т.п.)», чтобы усложнить написания злоумышленниками скрипта, который будет от лица пользователя делать множество запросов (лайков, например) параллельно.
А можно было бы сделать это на рекомендательных блокировках? Например заблокировать id пользователя (или хеш от него)
Если так, то да, конечно можно. Но как вы проверяете отсутствие дубликата без подходящего индекса? А если есть такой индекс, то почему не объявить его уникальным?
test=# CREATE TABLE events(id serial, content jsonb);
test=# CREATE UNIQUE INDEX ON events( (content->>'referenced_id') )
WHERE content->>'type' = 'vote';
test=# INSERT INTO events(content) VALUES (
'{ "type": "vote", "score": 1, "referenced_id": 12345 }'::jsonb
);
test=# INSERT INTO events(content) VALUES (
'{ "type": "comment", "text": "I like it!", "referenced_id": 12345 }'::jsonb
);
test=# INSERT INTO events(content) VALUES (
'{ "type": "vote", "score": -1, "referenced_id": 12345 }'::jsonb
);
ERROR: duplicate key value violates unique constraint "events_expr_idx"
DETAIL: Key ((content ->> 'referenced_id'::text))=(12345) already exists.
Затем появился кейс запрета параллельных действий пользователя. Приведенным Вами описанием выше это тоже можно решить, при условии, что каждое действие пишется в таблицу событий. А это так и есть. Спасибо. Кстати, еще один аргумент в пользу необходимости в проекте такой вот таблицы событий. Для rate limiters можно очень успешно использовать без необходимости REDIS
К чему может приводить обнаружение взаимоблокировок при большом количестве сеансов: https://www.postgresql.org/message-id/flat/c9f840f4-b7fe-19c6-76e6-65c02a0c230c%40postgrespro.ru
На мой взгляд не хватает конкретных примеров использования рекомендательных блокировок.
Это вообще класс явных блокировок, с которым у меня возникли наибольшие сложности в понимании.
Т.е. даже после прочтения документации все равно неясно, в каких случаях разработчику приложения может понадобится такой тип блокировок
Я бы так сказал: про рекомендательные блокировки надо вспомнить, когда потребуется сделать что-то такое, что на обычные блокировки ложится плохо. Не всегда может найтись подходящая таблица.
И еще - рекомендательные блокировки работают быстрее блокировок строк, поскольку никак не меняют страницы данных. В каких-то случаях это может оказаться важным для быстродействия.
Понятней не стало, но на всякий случай буду знать, что такие блокировки есть)
Блокировки в PostgreSQL: 3. Блокировки других объектов