Знімки замість блокування
Традиційне керування паралелізмом з блокуванням розглядає рядок бази даних як єдиний спільний ресурс: якщо автор хоче змінити його, всі інші, включаючи читачів, повинні чекати, поки автор закінчить. Це полегшує речі, але руйнує пропускну здатність, коли робоче навантаження змішує довгі читання з частими записами. MVCC використовує принципово інший підхід. Замість захисту єдиної копії рядка за допомогою блокування, база даних підтримує кілька версій цього рядка одночасно, кожна з яких позначена інформацією про час її створення та, зрештою, час її заміни. Коли транзакція починається, вона ефективно робить ментальний знімок бази даних у тому вигляді, в якому вона була в цей момент. Усі запити, які виконує ця транзакція протягом свого життя, консультуються з цим знімком, а не з поточною, постійно змінюваною таблицею. Автор, який оновлює рядок, не торкається або не знищує версію, на яку посилається знімок читача; він просто створює новий версій поряд зі старою. Читач продовжує бачити свій послідовний образ світу, не порушуючись, навіть коли основні дані змінюються навколо нього. Це ключовий концептуальний стрибок: керування паралелізмом переходить від «захищати одну копію» до «підтримувати достатньо копій, щоб усі могли бачити послідовну».
Translation Notes:
Чому неблокування читання та запису має значення
Практична вигода від MVCC величезна: читачі ніколи не блокують записників, а записники ніколи не блокують читачів. У чисто заблокованій системі одна повільна аналітична запитка може зупинити всі оновлення, які торкаються тих самих рядків, а спалах записів може змусити звітувальні запити чекати доtimeouts. MVCC повністю руйнує цю залежність. Дашборд, який запитує мільйони рядків, і платіжна служба, яка оновлює невелику кількість з них, можуть працювати одночасно в тій же таблиці без того, щоб один чекав на інший. Це саме тому MVCC став стандартним дизайном у сучасних реляційних системах: PostgreSQL використовує його з найраніших версій, двигун зберігання InnoDB MySQL покладається на нього, Oracle запровадив багато з цих ідей десятиліття тому, а бази даних, такі як SQL Server (у режимі ізоляції знімка) та CockroachDB, використовують варіації того ж принципу. Збільшення паралельності не є незначною оптимізацією; це часто різниця між базою даних, яка комфортно масштабується під змішані навантаження, і базою даних, яка зупиняється, коли стикаються звітувальне та транзакційне трафіки. Двофазне блокування все ще існує і гарантує правильність, але це робиться за рахунок пропускної здатності, яку більшість сучасних додатків просто не можуть собі дозволити.
Відстеження версій з використанням транзакційних ID
Технічно MVCC працює шляхом приєднання облікової інформації до кожної версії рядка. Коли транзакція створює або змінює рядок, нова версія позначається транзакційним ID (або відміткою часу), який її створив. Якщо цей рядок пізніше оновлюється або видаляється, стара версія не знищується негайно; замість цього вона позначається ID транзакції, яка замінила її, фактично кажучи «Я був дійсним до моменту, коли ця транзакція замінила мене». Таким чином, кожна версія рядка несе віконце видимості: маркер створення та, після завершення, маркер вичерпання. Коли починається нова транзакція і приймає свій знімок, база даних записує, які транзакції вже були підтверджені в цей момент. Відтоді, коли ця транзакція читає рядок, вона переглядає доступні версії та вибирає ту, яка створена транзакцією, що вже була підтверджена до початку знімка, і чия завершена транзакція (якщо така є) ще не була підтверджена в цей момент. Це просте правило робить знімок послідовним: воно відфільтровує зміни, внесені транзакціями, які все ще перебувають у процесі виконання, або які розпочалися після того, як було зроблено знімок, одночасно виявляючи все, що було безпечно підтверджено раніше.
Мертві рядки та компроміс з вакуумуванням
Зберігання старих версій рядків замість перезапису в місці – це те, що робить MVCC можливим, але це не безкоштовно. Кожне оновлення або видалення залишає за собою застарілу версію, яку деякий моментний знімок все ще може потребувати, і коли жодна активна транзакція вже не може потребувати її, вона стає чистою марною тратою. PostgreSQL називає ці залишки рядків, які більше нікому не потрібні, «мертвими рядками»; інші системи використовують подібну термінологію для того ж поняття. Якщо цього не робити, мертві рядки накопичуються всередині таблиць і індексів, збільшуючи обсяг зберігання на диску, сповільнюючи послідовні сканування та погіршуючи ефективність індексів, оскільки база даних все ще повинна пропускати всі ці мертві дані, щоб знайти живі дані. Рішення полягає в тому, що фоновий процес збору сміття, відомий у PostgreSQL як vacuum, періодично сканує таблиці, ідентифікує версії, які жодна транзакція вже не може бачити, та звільняє це місце для повторного використання. Це фундаментальний компроміс MVCC: він забезпечує паралелізм шляхом відкладання прибирання, і якщо вакуумування відстає – будь то через неправильну конфігурацію, надзвичайно довгі транзакції, які тримають відкритими старі знімки, або просто великий обсяг записів – накопичення залишків та погіршення продуктивності виникають. Налаштування autovacuum є рутинною, невід’ємною частиною експлуатації бази даних MVCC у масштабі.
Обробка конфліктів читання-запис
MVCC елегантно вирішує проблему конфлікту між читачами та записувальниками, але не може усунути конфлікти між записами, оскільки дві транзакції не можуть одночасно перемогти при спробі оновити одну й ту саму рядок в один і той же момент. Бази даних вирішують цю зіткнення одним із двох способів, залежно від рівня ізоляції, що використовується. Зазвичай застосовується підхід, коли друга запис затримано: перша транзакція, яка торкається рядка, отримує виключний блокування на цей конкретний рядок (набагато вужче блокування, ніж блокування всіх читачів), і друга транзакція чекає до моменту коміту або відмови першої транзакції, після чого продовжує роботу з оновленими даними. Альтернатива, яка використовується за більш жорстких рівнів ізоляції, таких як serializable або snapshot isolation, полягає у виявленні конфлікту та припинення однієї з транзакцій із повідомленням про серіалізацію або конфлікт, що змушує додаток повторно спробувати. Цей компроміс відображає класичний вибір між оптимістичним і песимістичним concurrency: очікування тримає обидві транзакції живими, але ризикує виникненням deadlock та затримки, а припинення забезпечує передбачувану затримку, але перекладає логіку повторних спроб на додаток. Яку стратегію обрано даною базою даних і за якого рівня ізоляції, є одним із найважливіших дизайнерських рішень, що впливає на її поведінку при одночасному навантаженні з великою кількістю записів.
Часті запитання
Чи означає MVCC, що база даних ніколи не використовує блокування?
Ні. MVCC усуває необхідність у блокуваннях читання та запобігає блокуванню читачів записувачами, але все ще використовуються блокування рядків для серіалізації одночасних записів в один і той самий рядок, а також інші типи блокувань (наприклад, блокування таблиць DDL) існують для структурних змін.
Яка різниця між MVCC та двофазним блокуванням?
Двофазне блокування змушує кожну транзакцію отримувати та утримувати блокування на будь-яких даних, які вона торкається, таким чином читачі та записувальники можуть блокувати один одного. MVCC надає кожній транзакції узгоджений знімок, побудований з кількох версій рядків, тому читання ніколи не потрібно чекати на запис.
Що таке мертвотвірний рядок у PostgreSQL?
Мертвотвірний рядок – це стара версія рядка, що залишається після оновлення або видалення, яка більше не видима жодному активному або майбутньому знімок транзакції. Він все ще займає місце на диску доки процес вакууму його не відновить.
Чи може тривала транзакція викликати проблеми під MVCC?
Так. Тривала транзакція утримує свій знімок відкритим, що змушує базу даних зберігати кожну версію рядка, яка цей знімок все ще може потребувати. Це може затримати вакуум і спричинити значний набір та індексний роздув, якщо він триває протягом тривалого часу.
Чому MySQL InnoDB та PostgreSQL обидва використовують MVCC?
Обидва були розроблені для підтримки висококонкурентних робочих навантажень, де запити звітності та транзакційні записи відбуваються одночасно. MVCC дозволяє кожному движку забезпечувати узгоджені читання без затримки записувачів, що є важливим для такого змішаного навантаження у великому масштабі.
Спробуйте наживо
Усе, що вище, працює прямо у вашому браузері — відкрийте MVCC: How Databases Let Readers and Writers Work Without Blocking Each Other і змінюйте параметри під час роботи. Нічого не встановлюється, нічого не завантажується на сервер, уся модель живе в одній вкладці.
▶ Відкрити симуляцію MVCC: How Databases Let Readers and Writers Work Without Blocking Each Other