Статьи

Математика и SQL. Часть 3. MVCC, неизменяемость и функциональное программирование.

Хотя первая нормальная форма в значительной степени ограничена реляционными операциями, эта часть является более общей. В нем рассматриваются основные перспективы функционального программирования и то, как это применимо к PostgreSQL. В то же время мы также рассмотрим полностью функциональный подход к хранилищу значений ключей в Perl в качестве близкого аналога.

Я: основное обещание функции

Мы должны начать с напоминания себе о том, что представляет собой функция: обещание, что если вы знаете одну или несколько частей информации, вы можете получить или найти другую часть информации.

Это обещание лежит за всем функциональным программированием, и в областях, которые близки к математике (например, реляционные базы данных), это особенно важно. Это основное обещание важно, но оно также меняет наше представление о программах. Вместо того, чтобы делать компьютерные программы ,  они просто вычисляют вещи.   Все, что делается в реальном мире в результате, является побочным продуктом этого расчета.

В общем, простой способ думать о функциональном программировании состоит в том, зависит ли выход исключительно от ввода, но в реляционной математике это фактически становится удивительно размытой линией. С данными, которые можно искать или сохранять, эта линия часто рисуется в точке изменчивости, потому что в противном случае подпрограммам очень трудно совместно использовать структуры данных. Это означает, что данные и состояние, которые не могут измениться за время существования функции, не засчитываются как функция.

Теперь, очевидно, функциональный взгляд на программирование довольно ограничен. Хотя сетевой сервер может прослушивать запросы и возвращать функцию ввода, побочные эффекты в потоке управления становятся невозможными при чисто функционально-ориентированном подходе. По этой причине во многих средах функционального программирования, с которыми я работал, есть императивные программы, которые связывают вычисления с реальными действиями и побочными эффектами.

II: как государство усложняет программирование и тестирование

Состояние приложения является основным источником ошибок в любой программе. Очень часто непредвиденные обстоятельства возникают из-за медленных изменений, которым подвергаются данные внутреннего контроля приложения (называемые «состоянием»). Эти изменения являются кумулятивными и, если их тщательно не контролировать, могут привести к возможным несоответствиям, которые приводят к эксплуатационным проблемам.

Тестирование функции намного проще, чем тестирование изменяемого отношения. Количество точек данных для тестирования меньше, если вы можете выполнять тесты границ для каждого изменения состояния, а не только для начальной проверки. Параллельность также проще, если вы применяете модель копирования при записи (создание новых объектов или других структур данных при их изменении). Это позволяет избежать беспокойства о блокировках при совместном доступе, потому что записи не блокируют чтения.

III: хранилище значений функциональных ключей в Perl

В прошлом посте мы рассмотрели простое хранилище значений ключей для Perl. Теперь пришло время конкретизировать это и взглянуть на неизменность, чтобы сделать его более функциональным. Эта новая версия имеет ряд важных компромиссов. Хотя он использует больше памяти, чем предыдущая версия, он гораздо более безопасен для параллелизма, потому что мы копируем при записи, и легче давать гарантии, поскольку все является неизменным.

package MyKVS;
use overload 
    '+' => 'add';
    '-'  => 'remove';
sub new {
     my ($class, $initial) = @_;
     $initial = {} unless ref $initial =~ /HASH/ and not keys %$initial;
     bless $initial, $class;
     return $initial;
};
sub add {
    my ($self, $other) = @_;
    my $new = {};
    $new->{$_} = $self->{$_} for keys %$self;
   $new->{$_} = $other->{$_} for keys %$other;
    return $self->new($new);
}
sub remove {
    my ($self, @other) = @_;
    my $new = {};
    for my $k (keys %$self){
        $new->{$k} = $self->{$k} unless grep {$_ eq $k}, @other;
    }
   return $self->new($new);
}
sub get_value {
    my ($self, $key) = @_;
    return $self->{$key};
}

Затем вы заметите, что хранилище значений ключей создает новое хранилище значений ключей при каждом изменении. Это означает, что если мы пройдем через открытый API, хранилище значений ключей будет изменчивым в течение всего срока его службы. Приведенный выше код не был протестирован, вероятно, не является лучшим Perl-кодом в мире и т. Д., Но довольно хорошо иллюстрирует концепции копирования при записи. Ничто из вышеперечисленного не требует блокировки при любых обстоятельствах.

Затем мы можем использовать его как:

use MyKVS;
use 5.010;

my $kvs = MyKVS->new({foo => 'bar', 1 => 1});
$kvs += {2 => 'foobar', cat => 'dog'};
my $kvs2 = $kvs + {dog => 'lion'};
$kvs2 -= 2;
say $kvs->get_value('cat');
say $kvs2->get_value('dog');

Это напечатает:

собака

лев

Ключами в $ kvs теперь являются foo, 1, 2 и cat. В kvs2 есть foo, 1, cat и dog.

IV: проблемы изменчивости

Одна из основных проблем здесь заключается в том, что состояние в конечном итоге должно меняться со временем, и часто его приходится делить. Функциональное программирование предлагает ряд решений этой проблемы, но все они включают в себя раздачу неизменяемых структур и копирование их при изменении. Эти принципы не противоречат общему состоянию.

Это имеет ряд возможностей, которые выходят за рамки данной части, но это понятие копирования при записи является основополагающим для понимания MVCC на PostgreSQL.

Итак, вернемся к примеру хранилища значений ключей. Предположим, мы хотим добавить возможность совместно использовать хранилища значений ключей для всего приложения. Затем мы можем добавить еще два метода, необязательно в замыкании, вместе с лексически ограниченным kvs для модуля или методов.

Теперь, поскольку мы копируем при записи, единственное место, которое нам нужно заблокировать, — это обновление. Поскольку наша стратегия блокировки будет зависеть от нашего подхода. В этом модуле хранилище является статическим по отношению к модулю и не является общим. Это означает, что нам не нужно блокировать. Если бы мы это сделали, мы бы заблокировали только функцию обновления. Никакие другие функции не нуждаются в блокировке. Таким образом, писатели могут блокировать писателей, но писатели никогда не блокируют читателей (или наоборот).

{    # start closure
my $kvs = __PACKAGE__->new({});
sub get_store {
     return $kvs;
}
sub update {
     my($self, $update) = @_;
     $kvs += $update;
}
}    # end closure

 Теперь с этими методами происходит нечто волшебное. Когда я обновляю, хэш-ссылка локальной ссылки обновляется, но каждый, кто получает хэш-ссылку, получает личную копию, которая фактически неизменна (если не нарушить инкапсуляцию). Более того, если кто-то хочет обновить глобальный магазин и получить новый обновленный магазин, это так просто:

 $kvs = $kvs->update({key => 'value'});

И теперь у нас есть грубый многоверсионный контроль параллелизма для нашего хранилища значений ключей. Если бы мы хотели пойти дальше, мы бы сохранили kvs в разделяемой памяти и обеспечили соответствующую блокировку. 

Обратите внимание, что блокировки потребуются только при записи и получении мастер-копии.

V: Как MVCC работает в PostgreSQL

MVCC — это главная особенность современных систем управления реляционными базами данных, которая относится к способности поддерживать согласованность между запросами, выдавать более старые версии данных, когда это необходимо, для отображения согласованного снимка в определенный момент времени. Этот конкретный момент времени отличается в зависимости от уровня изоляции.

MVCC работает по тем же базовым принципам, которые мы обсуждали здесь. Данные копируются при записи (поэтому нет разницы между обновлением и удалением, за которым следует выбор, за исключением того, что проверка целостности не выполняется в части удаления). База данных хранит необходимую бухгалтерскую информацию, необходимую для правильного просмотра информации, а затем предоставляет ее по запросу. Таким образом, вставки не видны до фиксации, удаляются просто пометить данные как устаревшие, а обновления делают оба. Это позволяет избежать большого количества проблем с параллелизмом, поскольку данные обрабатываются как неизменные и подлежащие сборке мусора.

Эта основная идея, конечно, реализована по-другому в других системах баз данных. Некоторые физически перемещают строки в ожидании фиксации или отката (с использованием сегмента отката), но PostgreSQL просто сохраняет строки в таблицах, как устаревшие, так и нет, а затем собирает их с помощью автовакуума (или периодического вакуумирования).

Тот же принцип во всех случаях, тем не менее, является логической копией данных, которые создаются, когда приложение читает их, и другой, которые создаются при записи данных. Уровень изоляции транзакции влияет на то, когда происходит эта копия и какую копию видит приложение, но не на основной процесс.