$30 off During Our Annual Pro Sale. View Details »

Как и зачем 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 Задача: нужно сбросить всем пользователям флаг подтверждения

    почты 1
  2. DmitryTsepelev DUMP 2022 2 UPDATE users SET email_conf i rmed

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

    SELECT pg_size_pretty(pg_total_relation_size('users')); - - 200 Gb
  4. 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 ( . . . );
  5. DmitryTsepelev DUMP 2022 5 Ой, места на диске не осталось

    SELECT pg_size_pretty(pg_total_relation_size('users')); - - 400 Gb
  6. DmitryTsepelev DUMP 2022 6 @dmitrytsepelev 🌎 dmitrytsepelev.dev

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

    почему это не плохо
  8. DmitryTsepelev DUMP 2022 8

  9. DmitryTsepelev DUMP 2022 БД и разработчики • есть таблички с

    данными; • данные можно изменять и запрашивать; • таблички можно объединять (JOINs) в запросах; • таблички связаны с помощью foreign keys; • есть constraints для валидации данных. 9
  10. 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
  11. ACID

  12. DmitryTsepelev DUMP 2022 ACID • Atomicity • Consistency • Isolation

    • Durability 12
  13. 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 выбросит исключение? 🤔
  14. 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
  15. DmitryTsepelev DUMP 2022 Транзакция • переводит БД из одного состояния

    в другое; • может содержать несколько операций; • фиксируется только если все операции прошли успешно; • фиксация изменений происходит одновременно; • работает независимо от других транзакций. 15
  16. 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
  17. DmitryTsepelev DUMP 2022 ACID • Atomicity • Consistency • Isolation

    • Durability 17
  18. DmitryTsepelev DUMP 2022 Целостность и консистентность • целостные данные соответствуют

    ограничениям БД (UNIQUE, NOT NULL, …); • корректные данные соответствуют всем ограничениям приложения; • БД не всегда может гарантировать корректность. 18
  19. DmitryTsepelev DUMP 2022 ACID • Atomicity • Consistency • Isolation

    • Durability 19
  20. 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
  21. DmitryTsepelev DUMP 2022 ACID • Atomicity • Consistency • Isolation

    • Durability 21
  22. Как обеспечить изоляцию транзакций?

  23. DmitryTsepelev DUMP 2022 Изоляция на локах • можно блокировать все,

    что читает либо пишет транзакция; • двухфазный лок (2PL): сначала всё блокируем, затем всё освобождаем; • в этом случае две транзакции, касающиеся одних строк работают последовательно, таким образом они изолированы. 23
  24. Спасибо! @dmitrytsepelev 🌎 dmitrytsepelev.dev 24

  25. DmitryTsepelev DUMP 2022 25

  26. Как БД хранит данные

  27. 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
  28. 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)
  29. DmitryTsepelev DUMP 2022 Типы форков • основной (хранит данные); •

    инициализационный (для нежурналируемых таблиц); • карта свободного пространства (отслеживает объем свободного места на страницах); • карта видимости (для определения необходимости очистки страницы или заморозки). 29
  30. 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
  31. DmitryTsepelev DUMP 2022 Tuple header 31 BEGIN; SELECT txid_current(); ┌──────────────┐

    │ txid_current │ ├──────────────┤ │ 8249614 │ └──────────────┘ INSERT INTO users (email) VALUES ('john@example.com'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 1 │ john@example.com │ (0,1) │ 8249614 │ 0 │ └────┴──────────────────┴───────┴─────────┴──────┘
  32. 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 │ ¤ │ ¤ │ . . . │ └───────────────┴─────────┴────────┴──────────┴────────┴─────────────┴────────────┴────────┴────────┴───────┴────────┘
  33. DmitryTsepelev DUMP 2022 Update tuple 33 BEGIN; SELECT txid_current(); ┌──────────────┐

    │ txid_current │ ├──────────────┤ │ 8249618 │ └──────────────┘ UPDATE users SET email = 'john.doe@example.com' WHERE id = 1; SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────────┼───────┼─────────┼──────┤ │ 1 │ john.doe@example.com │ (0,2) │ 8249618 │ 0 │ └────┴──────────────────────┴───────┴─────────┴──────┘
  34. 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) │ └─────────┴─────────┴────────┘
  35. 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) │ └─────────┴─────────┴────────┘
  36. Multi Version Concurrency Control

  37. DmitryTsepelev DUMP 2022 MVCC • чтение не блокирует запись; •

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

    • «видимые» данные определяются исходя из условий. 38 SELECT * FROM users WHERE xmin < = txid_current() AND (xmax = 0 OR txid_current() < xmax); - - - ^^ ^^ - - - запись еще не удалена запись удалена позже
  39. 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 ('john@example.com'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 2 │ john@example.com │ (0,3) │ 8257515 │ 0 │ xmin xmax список активных транзакций
  40. 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 ('john@example.com'); SELECT *, ctid, xmin, xmax FROM users; ┌────┬──────────────────┬───────┬─────────┬──────┐ │ id │ email │ ctid │ xmin │ xmax │ ├────┼──────────────────┼───────┼─────────┼──────┤ │ 2 │ john@example.com │ (0,3) │ 8257516 │ 0 │
  41. DmitryTsepelev DUMP 2022 Savepoints • механизм для "вложенных" транзакций; •

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

    (InnoDB/Falcon/Archive storage) • MariaDB (XtraDB/PBXT) • MongoDB (WiredTiger storage) • Oracle (> 4) • CouchDB • Couchbase • … 42
  43. Спецэффекты

  44. DmitryTsepelev DUMP 2022 Аномалии изоляции • Dirty Read; • Nonrepeatable

    Read; • Phantom Read; • Serialization Anomaly. 44
  45. 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
  46. DmitryTsepelev DUMP 2022 Как работают уровни изоляции? 46 • в

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

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

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

    INSERT INTO users (email) VALUES ('jane@example.com'); INSERT INTO users (email) VALUES ('mike@example.com'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ john@example.com │ │ 2 │ jane@example.com │ │ 3 │ mike@example.com │ └────┴──────────────────┘ ⏱ SELECT * FROM users; ❓ ❓ ❓
  50. DmitryTsepelev DUMP 2022 INSERT INTO users (email) VALUES ('john@example.com'); INSERT

    INTO users (email) VALUES ('jane@example.com'); INSERT INTO users (email) VALUES ('mike@example.com'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ john@example.com │ │ 2 │ jane@example.com │ │ 3 │ mike@example.com │ └────┴──────────────────┘ ⏱ SELECT * FROM users; ┌────┬─────────────────────┐ │ id │ email │ ├────┼─────────────────────┤ │ 2 │ jane@example.com │ │ 3 │ michael@example.com │ │ 1 │ ivan@example.com │ └────┴─────────────────────┘ Проблема: нет явного порядка 50
  51. DmitryTsepelev DUMP 2022 INSERT INTO users (email) VALUES ('john@example.com'); INSERT

    INTO users (email) VALUES ('jane@example.com'); INSERT INTO users (email) VALUES ('mike@example.com'); SELECT * FROM users; ┌────┬──────────────────┐ │ id │ email │ ├────┼──────────────────┤ │ 1 │ john@example.com │ │ 2 │ jane@example.com │ │ 3 │ mike@example.com │ └────┴──────────────────┘ UPDATE users SET email = 'ivan@example.com' WHERE id = 1; SELECT * FROM users; ┌────┬─────────────────────┐ │ id │ email │ ├────┼─────────────────────┤ │ 2 │ jane@example.com │ │ 3 │ michael@example.com │ │ 1 │ ivan@example.com │ └────┴─────────────────────┘ Проблема: нет явного порядка 51
  52. 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 * если планировщик воспользуется индексом, поддерживающим сортировку — порядок будет
  53. DmitryTsepelev DUMP 2022 Проблема: нет явного порядка 53

  54. DmitryTsepelev DUMP 2022 Фича: определяем была ли запись создана в

    upsert 54 INSERT INTO users (name, email) VALUES ('John', 'john@example.com'); INSERT INTO users (name, email) VALUES ('John!', 'john@example.com'), ('Jane', 'jane@example.com') 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 │ └─────────┴──────────┴──────────┘
  55. DmitryTsepelev DUMP 2022 Фича: определяем была ли запись создана в

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

    upsert 56 select *, xmin, xmax, ctid from users; ┌─────────┬──────────────────┬───────┬─────────┬─────────┬───────┐ │ id │ email │ name │ xmin │ xmax │ ctid │ ├─────────┼──────────────────┼───────┼─────────┼─────────┼───────┤ │ 1 │ john@example.com │ John! │ 8843025 │ 8843025 │ (0,2) │ │ 2 │ jane@example.com │ Jane │ 8843025 │ 0 │ (0,3) │ └─────────┴──────────────────┴───────┴─────────┴─────────┴───────┘ 🤔🤔🤔 В некоторых случаях xmax может означать всего лишь лок: https:/ /stackover fl ow.com/a/39204667
  57. 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')); ❓ ❓ ❓
  58. 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
  59. 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
  60. DmitryTsepelev DUMP 2022 Проблема: ID транзакций могут «кончиться» • тип

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

    транзакций; • на низком уровне данные не обновляются, а записываются; • реализация MVCC иногда приводит к неожиданным сайд–эффектам. 61
  62. 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
  63. Спасибо! @dmitrytsepelev 🌎 dmitrytsepelev.dev 63