Как выбрать уровни изоляции транзакций?

У меня есть таблица в базе данных, которая отвечает за хранение упорядоченных/переупорядочиваемых списков. Он имеет следующую форму:

| id | listId | index | title | ... |

где id — первичный ключ, listId — внешний ключ, определяющий, к какому списку принадлежит элемент, title, а другие столбцы — содержимое элементов. Свойство index отвечает за позицию элемента в списке. Это целочисленный счетчик (начиная с 0), который уникален в рамках списка, но может повторяться в разных списках. Пример данных:

| id      | listId  | index | title    | ...
---------------------------------------------
| "item1" | "list1" | 0     | "title1" | ...
| "item2" | "list1" | 1     | "title2" | ...
| "item3" | "list1" | 2     | "title3" | ...
| "item4" | "list2" | 0     | "title4" | ...
| "item5" | "list2" | 1     | "title5" | ...

Пользователи могут создавать/удалять элементы, перемещать их внутри списка или между списками. Чтобы обеспечить согласованность индексов при выполнении этих операций, я делаю следующее:

Создать элемент:

  1. Подсчитать элементы в этом списке
SELECT COUNT(DISTINCT "Item"."id") as "cnt" 
FROM "item" "Item" 
WHERE "Item"."listId" = ${listId}
  1. Вставьте новый элемент с индексом, установленным для подсчета с шага 1:
INSERT INTO "item"("id", "listId", "index", "title", ...) 
VALUES (${id}, ${listId}, ${count}, ${title})

Таким образом, индекс растет с каждым элементом, вставленным в список.

Переместить элемент:

  1. Получить текущий listId и индекс элемента:
SELECT "Item"."listId" AS "Item_listId", "Item"."index" AS "Item_index" 
FROM "item" "Item" 
WHERE "Item"."id" = ${id}
  1. При необходимости измените индекс «смещенных» элементов, чтобы порядок был последовательным, например. если элемент перемещается вперед, все элементы между его текущей позицией (исключительно) и его следующей позицией (включительно) должны уменьшить свой индекс на 1:
UPDATE "item" 
  SET "index" = "index" - 1 
WHERE "listId" = ${listId} 
  AND "index" BETWEEN ${sourceIndex + 1} AND ${destinationIndex}

Вариант с перемещением по спискам я опущу, потому что он очень похож.

  1. Обновите сам элемент:
UPDATE "item" 
   SET "index" = ${destinationIndex} 
WHERE "id" = ${id}

Удалить элемент:

  1. Получить индекс элемента и listId

  2. Переместите все элементы в том же списке, которые находятся рядом с этим элементом, на 1 шаг назад, чтобы убрать пробел

UPDATE "item" 
  SET "index" = "index" - 1 
WHERE "listId" = ${listId} 
  AND "index" > ${itemIndex}
  1. Удалить пункт:
DELETE FROM "item" 
WHERE "id" = ${id}

Вопрос:

Какие уровни изоляции транзакций я должен предоставить для каждой из этих операций? Для меня очень важно, чтобы столбец индекса был последовательным, без пробелов и, самое главное, без дубликатов. Правильно ли я понимаю, что операция создать элемент подлежит фантомному чтению, потому что она подсчитывает элементы по некоторым критериям и должна быть сериализуема? А другие операции?


person Kit Isaev    schedule 24.07.2019    source источник
comment
Независимо от того, какой уровень изоляции вы выберете и как именно вы изменяете данные в своей таблице, вы должны обеспечить соблюдение своего самого важного ограничения — отсутствие дубликатов — с помощью уникального индекса на (ListID, Index). Ограничение уникального индекса несложно реализовать, и оно всегда будет выполняться.   -  person Vladimir Baranov    schedule 29.07.2019
comment
Когда запись удаляется, понижение идентификатора всех записей с идентификатором больше, чем удаленная запись 1, может вызвать проблемы с производительностью при большом объеме данных. Мое предложение: 1. Создайте объект последовательности и используйте его для нового идентификатора. Не делайте «автоматическое увеличение» столбца Id и управляйте им вручную с помощью Sequence. 2. Когда запись удалена, уменьшите идентификаторы записей с идентификатором, превышающим идентификатор удаленной записи, на 1. И уменьшите значение последовательности на 1 в той же транзакции. 3. В качестве уровня изоляции используйте тип «Чтение незафиксированных».   -  person Gurcan    schedule 01.08.2019
comment
И я предупреждаю вас: если вы используете идентификатор для операций в пользовательском интерфейсе, обновление идентификатора при удалении записи создаст для вас огромные операционные проблемы. Потому что кто-то может редактировать другую запись, у которой в то же время больше значение идентификатора.   -  person Gurcan    schedule 01.08.2019
comment
@Gurcan Мне нужно, чтобы идентификаторы были постоянными / независимыми от порядка, а также предпочитали, чтобы они были uuid, а не целыми числами. Таким образом, идентификатор и индекс являются отдельными столбцами. Увеличение/уменьшение индексов записей для меня не проблема, поскольку столбец не предназначен для более чем 10 элементов.   -  person Kit Isaev    schedule 01.08.2019
comment
Итак, в базовом дизайне все выглядит нормально. Механизм индексной последовательности может быть таким, как я предложил. Ты согласен ?   -  person Gurcan    schedule 01.08.2019


Ответы (2)


Не зная больше о вашем конкретном приложении, безопаснее всего использовать serializable в качестве уровня изоляции всякий раз, когда вы обращаетесь к этой таблице, но даже этого уровня может быть недостаточно для вашего конкретного случая.

Ограничение уникальность для (listId, index) предотвратило бы дублирование (как насчет заголовка? Может ли он повторяться в одном списке?), некоторые точно созданные " сторожевые запросы могут еще больше смягчить проблемы, а последовательности базы данных или хранимые процедуры могут гарантировать отсутствие пробелов, но правда в том, что сам механизм кажется хрупким.

Зная лишь часть вашей конкретной проблемы, вы, похоже, столкнулись с проблемой параллелизма на уровне пользователя в том смысле, что несколько пользователей могут одновременно обращаться к одним и тем же объектам и вносить в них изменения. Предполагая, что это ваше типичное веб-приложение с серверной частью без сохранения состояния (следовательно, изначально распределенное), это может иметь большое количество последствий с точки зрения взаимодействия с пользователем, отражая архитектуру и даже функциональные требования. Скажем, например, что пользователь Foo перемещает элемент Car в List B, над которым в настоящее время работает пользователь Bar. Тогда можно предположить, что Bar должен будет увидеть элемент Car, как только операция будет завершена, но этого не произойдет, если не будет какого-то механизма для немедленного уведомления. пользователи списка B изменения. Чем больше пользователей работает с одним и тем же набором списков, тем хуже становится даже с уведомлениями, поскольку их будет все больше и больше, вплоть до того момента, когда пользователи видят, что что-то постоянно меняется, и просто не могут за этим уследить. .

Есть много предположений, которые любой сделает, чтобы дать вам ответ. Мои собственные наводят меня на мысль, что вам, вероятно, необходимо пересмотреть требования к этому приложению или убедиться, что руководство знает о некоторых ограничениях и принимает их. Этот тип проблемы довольно распространен в распределенных приложениях. Обычно «замки» на определенные наборы данных помещаются (либо через базу данных, либо пулы общей памяти), так что только один пользователь может изменить их в любой момент времени, или, в качестве альтернативы, предоставляется рабочий процесс для управления конфликтующими операциями (во многом как системы управления версиями). . Когда ничего не делается, ведется журнал операций, чтобы понять, что произошло, и исправить проблемы позже, если они будут обнаружены.

person Filippo Possenti    schedule 02.08.2019
comment
Приложение представляет собой трелло-доску. Обновления доставляются во внешний интерфейс в реальном времени через веб-сокет, а поскольку доски ограничены ~ 20 пользователями, поэтому я не ожидаю большого количества столкновений, когда одна и та же карта перемещается несколькими пользователями одновременно. С другой стороны, логика внешнего интерфейса/отрисовки пользовательского интерфейса опирается на согласованный порядок индексов, дублирование индексов или пробелов может привести к неопределенному поведению. И кажется более правильным гарантировать эту согласованность бэкэнда, чем пытаться сделать так, чтобы фронтенд знал о возможной несогласованности. - person Kit Isaev; 07.08.2019
comment
В этом случае сериализуемость может быть чрезмерной. Попробуйте использовать Repeatable Read, но в то же время реализуйте некоторую логику на стороне сервера (не на стороне базы данных) для сериализации операций, выполняемых на одной плате. Одним из способов может быть создание объекта для каждой активной в данный момент доски, а затем использование synchronized для этого объекта, чтобы запретить пользователям получать доступ одновременно, тем самым сериализуя операции. Другой (вероятно, лучший) способ — использовать какой-то механизм очереди, который сериализует операции и возвращает обратную связь по веб-сокету. - person Filippo Possenti; 07.08.2019

В соответствии с вашими ограничениями вы можете создать уникальный индекс для двух столбцов: listId,index можно определить как уникальный. Это позволит избежать дубликатов.

Дополнительно, чтобы избежать пробелов, я бы рекомендовал:

select listId, index, (select min(index) from Item i2 where listId = :listId and i2.index > i1.index) as nextIndex from Item i1 where nextIndex - index > 1 and listId = :listId

в конце каждой транзакции. Вместе с уровнем изоляции транзакций: «Повторяемое чтение» и откатом и повторением транзакции, если либо уникальное ограничение не работает, либо оператор, как я предположил, вернул запись, это должно соответствовать вашим требованиям.

person aschoerk    schedule 29.07.2019
comment
Идея использовать условие выбора в качестве некоего «эмулируемого ограничения» — умна, я не думал об этом. Хотя, к сожалению, уникальный индекс не будет работать, потому что столбцы не совсем уникальны. Я перемещаю/удаляю их за 2 операции записи (сначала перемещаю смежные элементы, затем перемещаю/удаляю сам элемент), и операция 1 приведет к нарушению этого индекса. - person Kit Isaev; 31.07.2019
comment
В зависимости от СУБД ограничения уникального ключа, вероятно, проверяются в конце транзакции (см.: postgresql.org/docs/9.1/index-unique-checks.html), тогда у вас не будет проблем с переездом. В противном случае расширьте предлагаемый оператор выбора, чтобы обнаружить идентичные индексы, но разные идентификаторы. - person aschoerk; 31.07.2019