ОНОВЛЕННЯ постгресів… ГРАНИЦІ 1


77

У мене є база даних Postgres, яка містить відомості про кластери серверів, такі як стан сервера ("активний", "очікування" тощо). Активним серверам в будь-який час може знадобитися перехід на режим очікування, і мені все одно, який режим очікування використовується зокрема.

Я хочу, щоб запит до бази даних міг змінити стан очікування - ПІДОБРИЙ - та повернути IP-адресу сервера, яку потрібно використовувати. Вибір може бути довільним: оскільки стан сервера змінюється із запитом, не має значення, який режим очікування буде обраний.

Чи можна обмежити запит лише одним оновленням?

Ось що я маю досі:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;

Постгресу це не подобається. Що я міг зробити інакше?


Просто виберіть сервер у коді та додайте його як обмежене місце. Це також дозволяє вам додатково перевірити додаткові умови (найстаріший, найновіший, останній живий, найменш завантажений, той самий постійний струм, різні стійки, найменші помилки). Більшість протоколів відмовлення так чи інакше потребують певної форми детермінізму.
eckes

@eckes Це цікава ідея. У моєму випадку "вибір сервера в коді" означав би спочатку прочитати список доступних серверів з db, а потім оновити запис. Оскільки багато екземплярів програми можуть виконати цю дію, існує гоночний стан і потрібна атомна операція (або це було 5 років тому). Вибір не потребував детермінації.
надзвичайно суперіорман

Відповіді:


125

Без одночасного доступу до запису

Матеріалізуйте виділення в CTE та приєднайтесь до нього в FROMпункті UPDATE.

WITH cte AS (
   SELECT server_ip          -- pk column or any (set of) unique column(s)
   FROM   server_info
   WHERE  status = 'standby'
   LIMIT  1                  -- arbitrary pick (cheapest)
   )
UPDATE server_info s
SET    status = 'active' 
FROM   cte
WHERE  s.server_ip = cte.server_ip
RETURNING server_ip;

Спочатку у мене був простий підзапрос, але це може призвести до LIMITпевних планів запитів, як зазначив Фейке :

Планувальник може вибрати , щоб створити план , який виконується вкладений цикл по LIMITingпідзапиту, в результаті чого більш UPDATEsніж LIMIT, наприклад:

 Update on buganalysis [...] rows=5
   ->  Nested Loop
         ->  Seq Scan on buganalysis
         ->  Subquery Scan on sub [...] loops=11
               ->  Limit [...] rows=2
                     ->  LockRows
                           ->  Sort
                                 ->  Seq Scan on buganalysis

Відтворення тестового випадку

Способом виправити вищезазначене було загортати LIMITпідзапит у власний CTE, оскільки CTE матеріалізується, він не повертає різних результатів на різних ітераціях вкладеного циклу.

Або використовувати низько корельований підзапит для простого випадку зLIMIT 1. Простіше, швидше:

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         )
RETURNING server_ip;

З одночасним доступом на запис

Припускаючи рівень ізоляції за замовчуваннямREAD COMMITTED для всього цього. Більш жорсткі рівні ( REPEATABLE READта SERIALIZABLE) ізоляції все ще можуть призвести до помилок серіалізації. Побачити:

Під одночасним завантаженням запису додайте FOR UPDATE SKIP LOCKEDдля блокування рядка, щоб уникнути перегонів. SKIP LOCKEDдодано в Postgres 9.5 , для старих версій див. нижче. Посібник:

З SKIP LOCKED, будь-які вибрані рядки, які неможливо негайно заблокувати, пропускаються. Пропуск заблокованих рядків забезпечує непослідовність перегляду даних, тому це не підходить для роботи загального призначення, але його можна використовувати, щоб уникнути суперечок із блокуванням, коли декілька споживачів звертаються до таблиці, що нагадує чергу.

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE SKIP LOCKED
         )
RETURNING server_ip;

Якщо відсутній кваліфікований розблокований рядок, у цьому запиті нічого не відбувається (жодна рядок не оновлюється), і ви отримуєте порожній результат. Для некритичних операцій, це означає, що ви зробили.

Однак, одночасні транзакції можуть мати заблоковані рядки, але потім не закінчуйте оновлення ( ROLLBACKабо інші причини). Щоб впевнитись, проведіть остаточну перевірку:

SELECT NOT EXISTS (
   SELECT 1
   FROM   server_info
   WHERE  status = 'standby'
   );

SELECTтакож бачить заблоковані рядки. Wile, яка не повертається true, один або кілька рядків все ще обробляються, і транзакції все ще можуть бути повернуті назад. (Або нові рядки були додані тим часом.) Почекайте трохи, а потім обведіть два кроки: ( UPDATEпоки не отримаєте жодного ряду назад; SELECT...), поки не отримаєте true.

Пов'язані:

Без SKIP LOCKEDв PostgreSQL 9.4 або новіших версій

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

Одночасні транзакції, що намагаються заблокувати той самий рядок, блокуються, поки перша не звільнить його блокування.

Якщо перша була відкатана назад, наступна транзакція бере блокування і проходить нормально; інші в черзі продовжують чекати.

Якщо перший здійснено, WHEREумова переоцінюється, і якщо це більше не TRUEбуло ( statusзмінилося), CTE (дещо дивно) не повертає жодного рядка. Нічого не відбувається. Це бажана поведінка, коли всі транзакції хочуть оновити один і той же рядок .
Але не тоді , коли кожна транзакція хоче оновити на наступну рядок . А оскільки ми просто хочемо оновити довільний (або випадковий ) рядок , чекати взагалі немає сенсу.

Ми можемо розблокувати ситуацію за допомогою дорадчих блокувань :

UPDATE server_info
SET    status = 'active' 
WHERE  server_ip = (
         SELECT server_ip
         FROM   server_info
         WHERE  status = 'standby'
         AND    pg_try_advisory_xact_lock(id)
         LIMIT  1
         FOR    UPDATE
         )
RETURNING server_ip;

Таким чином, наступний ряд, який ще не заблокований, буде оновлений. Кожна транзакція отримує новий рядок, з яким потрібно працювати. Я мав допомогу з чеського Postgres Wiki для цього фокусу.

idбудь-який унікальний bigintстовпець (або будь-який тип із неявним символом типу int4або int2).

Якщо довідкові блокування використовуються для декількох таблиць у вашій базі даних одночасно, розмежуйте їх pg_try_advisory_xact_lock(tableoid::int, id)- idце унікальний integerтут.
Оскільки tableoidце bigintкількість, вона теоретично може переповнювати integer. Якщо ви є параноїком, використовуйте (tableoid::bigint % 2147483648)::intзамість цього - залишаючи теоретичне "хеш-зіткнення" для справді параноїка ...

Також Postgres може безкоштовно перевіряти WHEREумови в будь-якому порядку. Він може перевірити pg_try_advisory_xact_lock()та придбати замок раніше status = 'standby' , що може призвести до додаткових дорадчих блокувань на непов'язаних рядках, де status = 'standby'це неправда. Пов’язане запитання щодо SO:

Як правило, ви можете просто проігнорувати це. Щоб гарантувати, що лише класифіковані рядки заблоковані, ви можете вкласти предикат (-ів) у CTE, як вище, або підзапит з OFFSET 0хаком (запобігає вбудованому) . Приклад:

Або (дешевше для послідовного сканування) вкладіть такі умови у CASEвиписці, як:

WHERE  CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

ОднакCASE трюк буде також тримати Postgres використовувати індекс на status. Якщо такий індекс доступний, для початку вам не потрібні додаткові вставки: у скануванні індексу будуть заблоковані лише кваліфіковані рядки.

Оскільки ви не можете бути впевнені, що індекс використовується в кожному дзвінку, ви можете просто:

WHERE  status = 'standby'
AND    CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

CASEЛогічно зайвий, але це сервера обговорювана мети.

Якщо команда є частиною довгої транзакції, розгляньте блокування на рівні сеансу, які можна (і повинні бути) звільнені вручну. Таким чином, ви можете розблокувати, як тільки ви закінчите з заблокованим рядком: pg_try_advisory_lock()іpg_advisory_unlock() . Посібник:

Після придбання на рівні сеансу дорадчий блокування утримується, поки явно не буде відпущено або сесія закінчиться.

Пов'язані:

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.