На прошлой лекции велся разговор о производительности и то, каким способами она добивается (ну или нет)
На этой лекции поговорим о надежности, но казалось бы, что еще обсудить, если мы обсуждали нормализацию моделей данных. Но дело в том, что надежность - это обеспечение целостности не только в момент хранения данных
Различают два вида сохранности (целостности):
1) Физическая сохранность - обеспечение сохранности данных в случае аппаратных и физических ошибок
2) Логическая сохранность - обеспечение сохранности в ходе чтения и изменения данных
При этом мы пытаемся достичь надежности хранения (данные сохранились) и доступа (данные сохранились и к ним можно получить доступ)
Чтобы достичь физической сохранности, нужно всего лишь воспользоваться простым советский методом: дублирование данных. Например, рассмотрим массив из 2 дисков - давайте один файл записывать параллельно целиком на два диска. В итоге мы, конечно же, не исключили аварию, но знатно уменьшили ее вероятность и получили массив RAID1
И если в случае с оперативной памятью, когда, начав работать, она почти с нулевой вероятностью сломается, то у жестких дисков даже есть специальная метрика - наработка на отказ: количество часов, в ходе которых работающий диск сломается с некой вероятностью. Причем это значение получают очень интересно: диски заставляют работать в невыносимых условиях ( повышенная частота запросов, температура и т.д.), затем полученные данные умножают на некий коэффициент - и дело в шляпе
Можем сделать так: взять три диска, один из которых будет под избыточные данные; сами данные разделим на две части, которые параллельно будем записывать на два диска, а на избыточный диск записывать побитовый XOR этих двух частей. Таким образом, получается, что при отказе одного диска, мы можем восстановить данные. Помимо этого роль избыточного диска могут и принимают другие диски, чтобы балансировать нагрузку - мы получаем массив RAID5
Кроме этих двух типов RAID есть и другие, отличающиеся своей надежностью и количеством затраченных средств
Следующим шагом является создание системы хранения данных (СХД) - специального компьютера со своей операционной системой и десятками дисков, который анализирует, что ему приходит и куда это положить - например, более востребованные к доступу данные класть на более быстрый диск
Далее человечество пришло к дата-центрам - специальным зданиям, в которых в специальных условиях крутятся сотни болванок и хранят данные многих бизнесов. Дата-центры разделяют на тиры, которые раздает Uptime Institute, где 4-ый тир - самый крутой с самым большим временем безотказной работы
Допустим, что мы работаем в банке:
Пользователь | Остаток на счете |
---|---|
A | 100 |
B | 200 |
Чтобы пользователю A перевести пользователю B 50 шекелей, нужно совершить четыре (или больше) действия:
Заметим, что между 3 и 4 действиями база данных теряет свой инвариант - тогда объединим все эти действия в одну группу.
Транзакция - это последовательность действий с базой данных, в которой либо все действия выполняются успешно, либо не выполняется ни одно из них
Транзакция переводит базу данных из одного логически согласованное состояния в другое логически согласованное Результатом транзакции может быть либо совершение изменений (commit), либо откат к исходному состоянию (rollback)
Чтобы ваша последовательность действий называлась транзакцией, нужно соблюдение этих свойств ACID:
Atomicity - атомарность: транзакция неделима - либо выполняются все действия, либо ни одного
Consistency - согласованность: транзакция переводит одно согласованное состояние базы данных в другое согласованное состояние базы данных без соблюдения поддержки согласованности в промежуточных точках
Isolation - изоляция: если запущено несколько конкурирующих транзакций, то любой обновление, выполненное одной транзакцией, скрыто от других до ее завершения
Durability - долговечность: когда транзакция завершена, ее результаты сохраняются, даже если в следующий момент произойдет сбой
Ситуация: пользователи A и C захотели перевести пользователю B по 100 шекелей
Пользователь | Остаток на счете |
---|---|
A | 100 |
B | 200 |
С | 100 |
В итоге может произойти такая последовательность действий:
В итоге произошла гонка обновлений (race-condition) - вторая транзакция слишком рано прочитала баланс у пользователя B и изменила его баланс на 300, когда он и так уже был 300 в ходе работы первой транзакции. Это проблема получила название проблемы потерянного обновления
Решение - 1 уровень изоляции “Незавершенное чтение”: требуем, что бы только одна транзакция могла записывать данные
Другой кейс: C решил перевести 100 шекелей к A, а A, получив от C деньги, захотел перевести 200 шекелей B
В итоге первая транзакция может где-то после начисления денег A откатиться, но вторая транзакция выполнилась до отката первой - у A уже нулевой баланс, поэтому у A в ходе отката получится -100
Возникает проблема грязного чтения, ее решение - 2 уровень изоляции “Завершенное чтение”: если какая-то транзакция изменяет данные, то никакая другая не может их читать
Третий пример: наш банк зачисляет проценты от остатка на счету в конце месяца - есть необходимость прочитать всю таблицу, сгруппировать ее и сделать другую магию
Номер счета | Пользователь | Остаток на счете |
---|---|---|
123456 | A | 800 |
243698 | A | 200 |
190428 | С | 100 |
В этот момент, когда все счета A были уже прочитаны, происходит перевод, который меняет баланс пользователя A, после этого зачисляются проценты от остатка - но они выходят неактуальными
Произошла проблема неповторяемого чтения. Чтобы решить ее, обложим себя 3 уровнем изоляции - “Воспроизводимое чтение”: если транзакция считывает данные, то никакая другая транзакция не может изменить их
Четвертый случай - пользователь A решил открыть новый счет:
Номер счета | Пользователь | Остаток на счете |
---|---|---|
123456 | A | 800 |
243698 | A | 100 |
190428 | С | 100 |
824082 | A | 1000 |
Расчет процентов начался перед созданием счета - транзакция не заблокировала действие на создание счета, поэтому начисленный процент опять остался неверным
Появилась проблема фантомного чтения и 4 уровень изоляции, который решает ее - “Сериализуемость” (или “Сериализация”): если транзакция выполняется к данным, то никакая другая транзакция не может изменять или добавлять записи, если они могут быть прочитаны изначальной транзакцией
Все эти уровни изоляции, как можем заметить, вредят производительности - в конце концов 4-ый уровень изоляции мало где используют, так как он может заблокировать доступ ко всей базе данных. Все решения сводятся к блокировкам: явным - разработчики сами определяют, где заблокировать запись, прямо внутри транзакции; и неявным - общим правилам для всей базы данных, например, блокировка на запись при изменении любого поля