Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Как и зачем MVCC мешает нам пользоваться базой данных и почему это не плохо

Как и зачем MVCC мешает нам пользоваться базой данных и почему это не плохо

Несмотря на распространение различных NoSQL решений для хранения данных в последние годы, реляционные базы данных остаются популярным выбором среди разработчиков из-за своей универсальности и предсказуемости. Большинство из них следуют принципам ACID, который дает нам определенные гарантии. Однако сделать так, чтобы БД им соответствовала, не так-то просто, и мы рассмотрим один из подходов к этому — MVCC. Иногда его работа приводит к неожиданным сайд-эффектам, и в докладе мы рассмотрим несколько примеров.

В качестве примеров я буду использовать PostgreSQL, но MVCC работает одинаково практически везде, так что адаптировать новые знания к своей любимой БД будет довольно просто. Доклад будет полезен как начинающим, так и продвинутым бэкэндерам.

Слушатели узнают больше о том, как работают базы данных "под капотом", на примере реализации изоляции транзакций. Это полезно на практике (так как иногда этот механизм может сыграть злую шутку), легко переносится на другие СУБД (механизм не уникален) и, наконец, просто интересно!

Dmitry Tsepelev

May 27, 2022
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

Transcript

  1. DmitryTsepelev DUMP 2022 3 А что если данных много? 🤔

    SELECT pg_size_pretty(pg_total_relation_size('users')); - - 200 Gb
  2. DmitryTsepelev DUMP 2022 Обновляем в цикле 4 User.in_batches do |batch|

    batch.update_all(email_conf i rmed: false) end SELECT users.id FROM users WHERE users.id > 0 ORDER BY users.id ASC LIMIT 1000; UPDATE users SET email_conf i rmed = FALSE WHERE users.id IN ( . . . ); SELECT users.id FROM users WHERE users.id > 1000 ORDER BY users.id ASC LIMIT 1000; UPDATE users SET email_conf i rmed = FALSE WHERE users.id IN ( . . . );
  3. DmitryTsepelev DUMP 2022 5 Ой, места на диске не осталось

    SELECT pg_size_pretty(pg_total_relation_size('users')); - - 400 Gb
  4. DmitryTsepelev DUMP 2022 БД и разработчики • есть таблички с

    данными; • данные можно изменять и запрашивать; • таблички можно объединять (JOINs) в запросах; • таблички связаны с помощью foreign keys; • есть constraints для валидации данных. 9
  5. DmitryTsepelev DUMP 2022 Делаем биллинг def handle_payment(user, payment) payment.update(processed: true)

    subscription_active_until = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end UPDATE payments SET processed = true WHERE id = ?; UPDATE users SET subscription_active_until = '2021-10-23' WHERE id = ?; 10
  6. DmitryTsepelev DUMP 2022 Делаем биллинг def handle_payment(user, payment) payment.update(processed: true)

    subscription_active_until = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end 13 А вдруг user.update выбросит исключение? 🤔
  7. DmitryTsepelev DUMP 2022 Atomicity (доделываем биллинг) def handle_payment(user, payment) ActiveRecord

    : : Base.transaction do payment.update(processed: true) subscription_active_until = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end end BEGIN; UPDATE payments SET processed = true WHERE id = ?; UPDATE users SET subscription_active_until = '2021-10-23' WHERE id = ?; COMMIT; 14
  8. DmitryTsepelev DUMP 2022 Транзакция • переводит БД из одного состояния

    в другое; • может содержать несколько операций; • фиксируется только если все операции прошли успешно; • фиксация изменений происходит одновременно; • работает независимо от других транзакций. 15
  9. DmitryTsepelev DUMP 2022 Спин–офф: локи def handle_payment(user, payment) user.with_lock do

    return if payment.reload.processed? # на случай если платеж уже запроцессили payment.update(processed: true) subscription_active_until = [user.subscription_active_until, Date.current].max + payment.days_covered user.update(subscription_active_until: subscription_active_until) end end BEGIN; SELECT * FROM payments WHERE id = ? FOR UPDATE; UPDATE payments SET processed = true WHERE id = ?; UPDATE users SET subscription_active_until = '2021-10-23' WHERE id = ?; COMMIT; 16
  10. DmitryTsepelev DUMP 2022 Целостность и консистентность • целостные данные соответствуют

    ограничениям БД (UNIQUE, NOT NULL, …); • корректные данные соответствуют всем ограничениям приложения; • БД не всегда может гарантировать корректность. 18
  11. DmitryTsepelev DUMP 2022 Isolation 20 BEGIN; UPDATE payments SET processed

    = true WHERE id = 1; SELECT processed FROM payments WHERE id = 1; - - true COMMIT; BEGIN; SELECT processed FROM payments WHERE id = 1; - - false COMMIT; BEGIN; SELECT processed FROM payments WHERE id = 1; - - true
  12. DmitryTsepelev DUMP 2022 Изоляция на локах • можно блокировать все,

    что читает либо пишет транзакция; • двухфазный лок (2PL): сначала всё блокируем, затем всё освобождаем; • в этом случае две транзакции, касающиеся одних строк работают последовательно, таким образом они изолированы. 23
  13. DmitryTsepelev DUMP 2022 Как хранятся данные на диске 27 users

    id name 1 John 2 Jane 42 (< 1GB) users forks Database orders id user_id 1 2 123 (< 1GB) orders forks Disk
  14. DmitryTsepelev DUMP 2022 Как хранятся данные на диске 28 users

    id name 1 John 2 Jane Disk 42 (< 1GB) users forks Database orders id user_id 1 2 123 (1GB) orders forks 123.1 (< 1GB) Fork fi le page 1 (8 kB) page 2 (8 kB) page 3 (8 kB)
  15. DmitryTsepelev DUMP 2022 Типы форков • основной (хранит данные); •

    инициализационный (для нежурналируемых таблиц); • карта свободного пространства (отслеживает объем свободного места на страницах); • карта видимости (для определения необходимости очистки страницы или заморозки). 29
  16. DmitryTsepelev DUMP 2022 Tuple • tuple = header + user

    data; • tuple почти никогда не обновляется (БД может, мы нет); • DELETE = UPDATE xmax; • UPDATE = INSERT + DELETE. 30 xmin xmax ctid infomask id name 123 125 (3, 15) 111 1 Jon 123 0 (4, 1) 111 2 Jane 125 0 (3, 15) 111 1 Jonh
  17. DmitryTsepelev DUMP 2022 Tuple header 31 BEGIN; SELECT txid_current(); ┌──────────────┐

    │ txid_current │ ├──────────────┤ │ 8249614 │ └──────────────┘ INSERT INTO users (email) VALUES ('[email protected]'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 1 │ [email protected] │ (0,1) │ 8249614 │ 0 │ └────┴──────────────────┴───────┴─────────┴──────┘
  18. DmitryTsepelev DUMP 2022 Tuple header 32 CREATE EXTENSION pageinspect; SELECT

    (t_infomask & 256) > 0 AS xmin_commited, * FROM heap_page_items(get_raw_page('users', 0)); ┌───────────────┬─────────┬────────┬──────────┬────────┬─────────────┬────────────┬────────┬────────┬───────┬────────┐ │ xmin_commited │ t_xmin │ t_xmax │ t_f i eld3 │ t_ctid │ t_infomask2 │ t_infomask │ t_hoff │ t_bits │ t_oid │ t_data │ ├───────────────┼─────────┼────────┼──────────┼────────┼─────────────┼────────────┼────────┼────────┼───────┼────────┤ │ f │ 8249614 │ 0 │ 11 │ (0,1) │ 2 │ 2050 │ 24 │ ¤ │ ¤ │ . . . │ └───────────────┴─────────┴────────┴──────────┴────────┴─────────────┴────────────┴────────┴────────┴───────┴────────┘ COMMIT; ┌───────────────┬─────────┬────────┬──────────┬────────┬─────────────┬────────────┬────────┬────────┬───────┬────────┐ │ xmin_commited │ t_xmin │ t_xmax │ t_f i eld3 │ t_ctid │ t_infomask2 │ t_infomask │ t_hoff │ t_bits │ t_oid │ t_data │ ├───────────────┼─────────┼────────┼──────────┼────────┼─────────────┼────────────┼────────┼────────┼───────┼────────┤ │ t │ 8249614 │ 0 │ 11 │ (0,1) │ 2 │ 2050 │ 24 │ ¤ │ ¤ │ . . . │ └───────────────┴─────────┴────────┴──────────┴────────┴─────────────┴────────────┴────────┴────────┴───────┴────────┘
  19. DmitryTsepelev DUMP 2022 Update tuple 33 BEGIN; SELECT txid_current(); ┌──────────────┐

    │ txid_current │ ├──────────────┤ │ 8249618 │ └──────────────┘ UPDATE users SET email = '[email protected]' WHERE id = 1; SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────────┼───────┼─────────┼──────┤ │ 1 │ [email protected] │ (0,2) │ 8249618 │ 0 │ └────┴──────────────────────┴───────┴─────────┴──────┘
  20. DmitryTsepelev DUMP 2022 Update tuple 34 SELECT t_xmin, t_xmax, t_ctid

    FROM heap_page_items(get_raw_page('users', 0)); ┌─────────┬─────────┬────────┐ │ t_xmin │ t_xmax │ t_ctid │ ├─────────┼─────────┼────────┤ │ 8249616 │ 8249618 │ (0,2) │ │ 8249619 │ 0 │ (0,2) │ └─────────┴─────────┴────────┘
  21. DmitryTsepelev DUMP 2022 Delete tuple 35 BEGIN; SELECT txid_current(); ┌──────────────┐

    │ txid_current │ ├──────────────┤ │ 8249620 │ └──────────────┘ DELETE FROM users WHERE id = 1; SELECT t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('users', 0)); ┌─────────┬─────────┬────────┐ │ t_xmin │ t_xmax │ t_ctid │ ├─────────┼─────────┼────────┤ │ 8249616 │ 8249619 │ (0,2) │ │ 8249619 │ 8249621 │ (0,2) │ └─────────┴─────────┴────────┘
  22. DmitryTsepelev DUMP 2022 MVCC • чтение не блокирует запись; •

    запись не блокирует чтение; • каждая транзакция видит свой «слепок» данных. 37
  23. DmitryTsepelev DUMP 2022 Snapshot • физическая копия данных не делается;

    • «видимые» данные определяются исходя из условий. 38 SELECT * FROM users WHERE xmin < = txid_current() AND (xmax = 0 OR txid_current() < xmax); - - - ^^ ^^ - - - запись еще не удалена запись удалена позже
  24. DmitryTsepelev DUMP 2022 Snapshot 39 BEGIN; SELECT txid_current(); ┌──────────────┐ │

    txid_current │ ├──────────────┤ │ 8257515 │ └──────────────┘ SELECT txid_current_snapshot(); ┌──────────────────────────────┐ │ txid_current_snapshot │ ├──────────────────────────────┤ │ 8257515 : 8257515 : 8257515 │ └──────────────────────────────┘ INSERT INTO users (email) VALUES ('[email protected]'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 2 │ [email protected] │ (0,3) │ 8257515 │ 0 │ xmin xmax список активных транзакций
  25. DmitryTsepelev DUMP 2022 Savepoints 40 BEGIN; SELECT txid_current(); ┌──────────────┐ │

    txid_current │ ├──────────────┤ │ 8257515 │ └──────────────┘ SELECT txid_current_snapshot(); ┌──────────────────────────────┐ │ txid_current_snapshot │ ├──────────────────────────────┤ │ 8257515 : 8257515 : 8257515 │ └──────────────────────────────┘ INSERT INTO users (email) VALUES ('[email protected]'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 2 │ [email protected] │ (0,3) │ 8257516 │ 0 │
  26. DmitryTsepelev DUMP 2022 Savepoints • механизм для "вложенных" транзакций; •

    позволяет откатить часть изменений, не откатывая всю транзакцию; • txid_current возвращает идентификатор главной транзакции! 41 BEGIN; SAVEPOINT s1; INSERT INTO users (email) VALUES ('[email protected]'); ROLLBACK TO s1;
  27. DmitryTsepelev DUMP 2022 Где кроме PSQL используется MVCC? • MySQL

    (InnoDB/Falcon/Archive storage) • MariaDB (XtraDB/PBXT) • MongoDB (WiredTiger storage) • Oracle (> 4) • CouchDB • Couchbase • … 42
  28. DmitryTsepelev DUMP 2022 Уровни изоляции транзакций 45 Isolation Level Dirty

    Read Nonrepeatable Reads Phantom Reads Serialization Anomaly Read Uncommited (not PG) (not PG) x x Read Commited x x x Repeatable Read (not PG) x Serializable
  29. DmitryTsepelev DUMP 2022 Как работают уровни изоляции? 46 • в

    Read Committed снепшот создается перед каждым запросом; • в Repeatable Read и Serializable снепшот создается только один раз.
  30. DmitryTsepelev DUMP 2022 Как работает Serializable 47 • гарантируется, что

    результат работы транзакций не зависит от порядка выполнения; • для полностью корректной работы нужно обходить граф зависимостей; • упрощенно: ищем ситуации, когда одна транзакция изменила строку, а другая — прочитала предыдущую версию этой строки; • простой подход может вызывать ложно–положительные срабатывания.
  31. DmitryTsepelev DUMP 2022 Почему не использовать Serializable всегда? 48 •

    придется использовать его для ВСЕХ транзакций; • придется всегда ловить исключение и перезапускать операцию; • Serializable работает медленнее других уровней.
  32. DmitryTsepelev DUMP 2022 49 INSERT INTO users (email) VALUES ('[email protected]');

    INSERT INTO users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ [email protected] │ │ 2 │ [email protected] │ │ 3 │ [email protected] │ └────┴──────────────────┘ ⏱ SELECT * FROM users; ❓ ❓ ❓
  33. DmitryTsepelev DUMP 2022 INSERT INTO users (email) VALUES ('[email protected]'); INSERT

    INTO users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ [email protected] │ │ 2 │ [email protected] │ │ 3 │ [email protected] │ └────┴──────────────────┘ ⏱ SELECT * FROM users; ┌────┬─────────────────────┐ │ id │ email │ ├────┼─────────────────────┤ │ 2 │ [email protected] │ │ 3 │ [email protected] │ │ 1 │ [email protected] │ └────┴─────────────────────┘ Проблема: нет явного порядка 50
  34. DmitryTsepelev DUMP 2022 INSERT INTO users (email) VALUES ('[email protected]'); INSERT

    INTO users (email) VALUES ('[email protected]'); INSERT INTO users (email) VALUES ('[email protected]'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ [email protected] │ │ 2 │ [email protected] │ │ 3 │ [email protected] │ └────┴──────────────────┘ UPDATE users SET email = '[email protected]' WHERE id = 1; SELECT * FROM users; ┌────┬─────────────────────┐ │ id │ email │ ├────┼─────────────────────┤ │ 2 │ [email protected] │ │ 3 │ [email protected] │ │ 1 │ [email protected] │ └────┴─────────────────────┘ Проблема: нет явного порядка 51
  35. DmitryTsepelev DUMP 2022 SELECT t_xmin, t_xmax, t_ctid FROM heap_page_items(get_raw_page('users', 0));

    ┌─────────┬─────────┬────────┐ │ t_xmin │ t_xmax │ t_ctid │ ├─────────┼─────────┼────────┤ │ 8257533 │ 8257536 │ (0,4) │ │ 8257534 │ 0 │ (0,2) │ │ 8257535 │ 0 │ (0,3) │ │ 8257536 │ 0 │ (0,4) │ └─────────┴─────────┴────────┘ Проблема: нет явного порядка * 52 * если планировщик воспользуется индексом, поддерживающим сортировку — порядок будет
  36. DmitryTsepelev DUMP 2022 Фича: определяем была ли запись создана в

    upsert 54 INSERT INTO users (name, email) VALUES ('John', '[email protected]'); INSERT INTO users (name, email) VALUES ('John!', '[email protected]'), ('Jane', '[email protected]') ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name RETURNING id, (xmax = '0') AS inserted, name AS new_name; ┌─────────┬──────────┬──────────┐ │ id │ inserted │ new_name │ ├─────────┼──────────┼──────────┤ │ 1 │ f │ John! │ │ 2 │ t │ Jane │ └─────────┴──────────┴──────────┘
  37. DmitryTsepelev DUMP 2022 Фича: определяем была ли запись создана в

    upsert 55 select *, xmin, xmax, ctid from users; ┌─────────┬──────────────────┬───────┬─────────┬─────────┬───────┐ │ id │ email │ name │ xmin │ xmax │ ctid │ ├─────────┼──────────────────┼───────┼─────────┼─────────┼───────┤ │ 1 │ [email protected] │ John! │ 8843025 │ 8843025 │ (0,2) │ │ 2 │ [email protected] │ Jane │ 8843025 │ 0 │ (0,3) │ └─────────┴──────────────────┴───────┴─────────┴─────────┴───────┘ 🤔🤔🤔
  38. DmitryTsepelev DUMP 2022 Фича: определяем была ли запись создана в

    upsert 56 select *, xmin, xmax, ctid from users; ┌─────────┬──────────────────┬───────┬─────────┬─────────┬───────┐ │ id │ email │ name │ xmin │ xmax │ ctid │ ├─────────┼──────────────────┼───────┼─────────┼─────────┼───────┤ │ 1 │ [email protected] │ John! │ 8843025 │ 8843025 │ (0,2) │ │ 2 │ [email protected] │ Jane │ 8843025 │ 0 │ (0,3) │ └─────────┴──────────────────┴───────┴─────────┴─────────┴───────┘ 🤔🤔🤔 В некоторых случаях xmax может означать всего лишь лок: https:/ /stackover fl ow.com/a/39204667
  39. DmitryTsepelev DUMP 2022 57 INSERT INTO users (email) SELECT random()

    : : text FROM generate_series(1, 1000000); SELECT pg_size_pretty(pg_total_relation_size('users')); - - 72 MB UPDATE users SET email = 'something'; SELECT pg_size_pretty(pg_total_relation_size('users')); ❓ ❓ ❓
  40. DmitryTsepelev DUMP 2022 Проблема: большой апдейт может раздуть таблицу 58

    INSERT INTO users (email) SELECT random() : : text FROM generate_series(1, 1000000); SELECT pg_size_pretty(pg_total_relation_size('users')); - - 72 MB UPDATE users SET email = 'something'; SELECT pg_size_pretty(pg_total_relation_size('users')); - - 135 MB
  41. DmitryTsepelev DUMP 2022 Проблема: как освободить место на диске после

    удаления данных? 59 VACUUM users; SELECT pg_size_pretty( pg_total_relation_size('users') ); - - 135 MB VACUUM FULL users; SELECT pg_size_pretty( pg_total_relation_size('users') ); - - 64 MB
  42. DmitryTsepelev DUMP 2022 Проблема: ID транзакций могут «кончиться» • тип

    ID транзакции XID, 32 бита (~4 000 000 000 значений); • больший тип увеличит место для хранения заголовка тапла; • БД начинает счет заново при достижении максимального значения; • VACUUM «замораживает» (freeze) старые таплы. 60
  43. DmitryTsepelev DUMP 2022 Выводы • MVCC нужен для обеспечения изоляции

    транзакций; • на низком уровне данные не обновляются, а записываются; • реализация MVCC иногда приводит к неожиданным сайд–эффектам. 61
  44. DmitryTsepelev DUMP 2022 Куда пойти дальше • PostgreSQL изнутри —

    https:/ /postgrespro.ru/education/books/ internals; • The Internals of PostgreSQL — http:/ /www.interdb.jp/pg/; • Serializable Snapshot Isolation in PostgreSQL — https:/ /arxiv.org/pdf/ 1208.4179.pdf; • Database Internals — https:/ /www.databass.dev. 62