21.05.2014
В данном разделе рассматриваются некоторые особенности внутреннего устройства и работы механизмов файловой базы данных «1С:Предприятия 8», которые не освещены в документации, но могут быть интересны пользователям и разработчикам прикладных решений на платформе «1С:Предприятие 8». Приведенное описание соответствует платформе «1С:Предприятие» версии 8.3.4.
На самом нижнем уровне файл *.1CD или файл базы данных содержит внутри своего рода файловую систему, включающую в себя так называемые внутренние файлы. Файл *.1CD имеет страничную организацию, то есть состоит из страниц размером 4096 байт (4 К). Размер файла *.1CD всегда кратен 4 К.
Страницы адресуются их номерами. Номер страницы представлен 4-байтовым целым числом без знака. Следовательно, файл *.1CD может содержать не более чем 4 294 967 296 страниц.
Страница с номером 0 содержит служебные данные файла *.1CD, такие как версия формата файла базы данных, общее число страниц в файле и т. п.
Страница с номером 1 используется менеджером свободных страниц.
Каждая из остальных страниц может либо принадлежать какому-либо из внутренних файлов, либо находиться в списке свободных страниц.
Страницы, относящиеся к внутреннему файлу, бывают трех видов:
Эти страницы образуют дерево, корнем которого является корневая страница, промежуточными узлами являются индексные страницы, а листьями – страницы данных.
Корневая страница содержит служебную информацию внутреннего файла, такую как длина файла, номер версии данных файла и т. п. Кроме того, на корневой странице содержится до 1018 номеров индексных страниц.
Индексные страницы образуют промежуточный уровень дерева. Индексная страница содержит число страниц данных, адресуемых данной индексной страницей, и до 1023 номеров страниц данных.
Страница данных содержит только данные.
Из сказанного выше следует, что внутренний файл может включать не более чем 1 041 414 (1018 * 1023) страниц данных. Следовательно, максимальный размер внутреннего файла не может превышать 4 265 631 744 (1018 * 1023 * 4096) байта. Для адресации отдельных байтов внутреннего файла используются 4-байтовые целые числа без знака.
Для представления внутреннего файла нулевой длины достаточно одной только корневой страницы. Если размер внутреннего файла составляет от 1 до 4096, то он представляется тремя страницами: одной корневой, одной индексной и одной страницей данных. При дальнейшем росте размера файла добавляются новые страницы данных, и их номера помещаются в индексную страницу. Как только индексная страница перестает вмещать номера страниц данных, добавляется новая индексная страница и ее номер добавляется в корневую страницу. И так далее.
Внутренние файлы не имеют имен. Для идентификации внутренних файлов используются номера их корневых страниц.
Страницы, не относящиеся к какому-либо из внутренних файлов, находятся в списке свободных страниц. Свободные страницы могут образоваться при сокращении размера или удалении внутреннего файла. Любые освободившиеся страницы внутренних файлов помещаются в список свободных страниц.
При необходимости увеличения размера или создании нового внутреннего файла по возможности используются страницы из списка свободных страниц.
Внутренние файлы в конечном счете предназначены для хранения базы данных. База данных представляет собой совокупность таблиц. Каждой таблице может соответствовать от двух до четырех внутренних файлов:
Файл описания и файл данных присутствуют обязательно для каждой таблицы. Файл индексов присутствует, если в таблице определен хотя бы один индекс. Файл данных неограниченной длины присутствует, если в структуре таблицы определена хотя бы одна колонка неограниченной длины.
Кроме того, имеется файл описания базы данных. Данный файл содержит информацию о локали базы данных, а также номера корневых страниц внутренних файлов описания для каждой из таблиц базы данных.
Файл описания таблицы содержит полное описание таблицы, которое включает:
При открытии базы данных считывается файл описания базы данных и адресуемые им файлы описания таблиц. На основании этой информации инициализируются внутренние структуры данных, необходимые во время выполнения. Прочие файлы таблиц на этом этапе не открываются. Их открытие выполняется по мере обращения к таблицам. Это сделано из соображения ускорения процесса открытия, а также из предположения, что в данном сеансе могут быть обращения не ко всем таблицам базы данных.
Файл данных содержит записи таблицы. Каждая запись содержит значения всех колонок таблицы, кроме значений колонок неограниченной длины. Записи имеют фиксированную длину. Поэтому адрес записи может быть легко вычислен по номеру записи (N) и длине (L) как N * L.
Номера записи представлены 4-байтовыми целыми числами. Запись с номером 0 используется для служебных целей. Номера «настоящих» записей начинаются с 1.
Длина записи может быть вычислена как сумма длин всех колонок плюс от 1 до 17 байт служебной информации. Ограничений на длину записи не накладывается.
Ниже приведена информация о типах данных и соответствующем размере колонок:
Numeric(p,s)
Десятичное число с фиксированной точкой. Хранится в десятичном виде по две десятичные цифры на один байт. Зарезервировано место для знака. Размер может быть вычислен по формуле:
(p + 1) / 2 + (p + 1) % 2
Char(n)
Строка фиксированной длины, состоящая из n однобайтовых символов. Размер колонки равен n.
NChar(n)
Строка Unicode фиксированной длины, состоящая из n символов в кодировке UTF-16. Размер колонки равен n * 2.
Binary(n)
Двоичные данные фиксированной длины. Размер колонки равен n.
VarChar(n)
Строка переменной длины, состоящая не более чем из n однобайтовых символов. Размер колонки равен n + 2 байта. Дополнительные 2 байта используются для хранения фактической длины.
NVarChar(n)
Строка Unicode переменной длины, состоящая не более чем из n символов в кодировке UTF-16. Размер колонки равен n * 2 + 2. Дополнительные 2 байта используются для хранения фактической длины.
VarBinary(n)
Двоичные данные переменной длины не более n байт. Размер колонки равен n + 2 байта. Дополнительные 2 байта используются для хранения фактической длины.
Logical
Значение логического типа (true или false). Размер колонки равен одному байту.
Date
Дата без времени. Размер колонки – 4 байта.
DateTime
Дата и время. Размер колонки – 7 байт.
Text
Текст неограниченной длины, состоящий из однобайтовых символов. В структуре записи колонка занимает два 4-байтовых целых числа: фактическая длина значения и адрес в файле данных неограниченной длины. Фактические значения хранятся в файле данных неограниченной длины.
NText
Текст Unicode неограниченной длины, состоящий из символов в кодировке UTF-16. В структуре записи колонка занимает два 4-байтовых целых числа: фактическая длина значения и адрес в файле данных неограниченной длины. Фактические значения хранятся в файле данных неограниченной длины.
Image
Двоичные данные неограниченной длины. В структуре записи колонка занимает два 4-байтовых целых числа: фактическая длина значения и адрес в файле данных неограниченной длины. Фактические значения хранятся в файле данных неограниченной длины.
Кроме того, к размеру колонок, которые могут содержать NULL, добавляется еще один байт.
В файле индексов находятся все индексы, определенные для таблицы. Детальное рассмотрение структуры индексов не входит в цели данной статьи. Отметим только, что индекс представляет собой сбалансированное дерево. С точки зрения использования файловой базы данных важным является то, что, в отличие от размера записи, на длину ключа индекса наложено ограничение: длина не может превышать 1920 байт. Ключ представляет собой конкатенацию значений всех индексируемых колонок записи плюс 4-байтовый номер записи.
Индексироваться могут колонки типов Numeric, Char, NChar, Binary, VarChar, NVarChar, VarBinary, Logical, Date и DateTime. Значение каждой из индексируемых колонок типов Numeric, Binary, VarBinary, Logical, Date и DateTime помещается в ключ как есть. Соответственно, каждая из таких колонок добавляет к длине ключа свой собственный размер. А вот для колонок типов Char, NChar, VarChar и NVarChar вместо самой строки в ключ помещается ее ключ сортировки (collation key). Поэтому вклад колонок указанных типов в длину ключа определяется как n * 3 + 2 для колонок, не чувствительных к регистру букв. И n * 4 + 3 для колонок, чувствительных к регистру.
В файле данных неограниченной длины хранятся фактические значения колонок типов Text, NText и Image. Для хранения таких значений файл организован как набор блоков длиной 256 байт. Каждое значение хранится как односвязный список блоков. В каждом блоке содержатся:
Блок с адресом 0 используется для служебных нужд, а если точнее он содержит адрес списка свободных блоков. В список свободных блоков помещаются освободившиеся блоки, которые могут быть использованы в дальнейшем.
Следует различать чтение данных, выполняемое вне транзакции, и чтение в рамках транзакции. Операция чтения (например, SQL-запрос SELECT), выполняемая вне транзакции, получает данные, соответствующие состоянию базы данных на момент выполнения операции. При использовании SELECT вне транзакции поведение файловой базы данных подобно поведению версионных СУБД, таких как Oracle. То есть все данные, полученные запросом SELECT, относятся к одному согласованному состоянию базы данных, имевшему место на начало выполнения операции. Чтение данных не может быть заблокировано никакой другой операцией чтения или записи. Но нужно понимать, что состояние, имевшее место на начало чтения, может быть изменено. Соответственно, считываемые данные могут оказаться устаревшими.
Если чтение выполняется в рамках транзакции, то гарантируется, что считанные данные не могут быть изменены никем другим до завершения транзакции. Для обеспечения этой неизменности используется механизм транзакционных блокировок. При первом обращении к таблице на чтение в рамках транзакции на таблицу накладывается Read-блокировка. И эта блокировка не снимается до завершения транзакции.
Запись данных всегда предполагает наличие транзакции. Если операция записи была вызвана вне объемлющей транзакции, то транзакция будет создана неявно в процессе выполнения операции. При выполнении операции записи на таблицы, в которые вносятся изменения, накладывается транзакционная Write-блокировка, препятствующая чтению или записи, выполняемой другими соединениями.
Если на таблицу уже была наложена Read-блокировка, то выполняется ее эскалация до Write-блокировки.
Операции записи данных, выполняемые в рамках транзакции, не приводят к немедленной записи изменений в файл *.1CD. Изменения, вызванные операциями записи, накапливаются в кеше модифицированных страниц и сбрасываются в файл базы данных при фиксации (commit) транзакции.
Таким образом, если в процессе выполнения транзакции, до ее фиксации, произойдет сбой и/или падение приложения, то все изменения, произведенные в транзакции, окажутся потерянными и файл базы данных останется в неизмененном состоянии.
Для повышения эффективности операций чтения механизмы файловой базы данных стараются кешировать считанные данные и тем самым минимизировать число физических операций чтения из файла базы данных. Кеш считанных страниц содержит прочитанные страницы данных внутренних файлов. Общий размер кеша для каждого из соединений с файловой базой данных является ограниченным и может в зависимости от различных условий составлять от 2 до 200 Мбайт. Кеш наибольшего размера создается при работе с файлом базы данных, расположенным на сетевом диске.
Кеш организован по принципу LRU. То есть страницы, к которым дольше всего не было обращений, могут быть вытеснены из кеша вновь считанными страницами.
Другой причиной, по которой страницы могут быть исключены из кеша, является его обновление. Каждое зафиксированное состояние данных внутреннего файла имеет соответствующий номер версии. Все кешируемые страницы внутреннего файла соответствуют определенной версии внутреннего файла. Процесс обновления состоит в том, что из файла базы данных считывается текущая версия внутреннего файла и сравнивается с версией кешируемых страниц. Если выясняется, что версия кешируемых страниц устарела, то страницы соответствующего внутреннего файла исключаются из кеша.
Для каждой операции чтения, выполняемой вне транзакции, обновление кеша производится для внутренних файлов данных, индексов и данных неограниченной длины каждой из таблиц, задействованных в операции чтения.
В рамках транзакции обновление кеша производится непосредственно после наложения на таблицу Read-блокировки. В дальнейшем до завершения транзакции кеш остается актуальным, так как таблица не может быть модифицирована другими транзакциями. Соответственно, для последующих операций чтения в той же транзакции обновления кеша не требуется.
Следует также заметить, что в исключительном режиме доступа к базе данных кеш считанных страниц всегда остается актуальным и его обновление не производится.
Еще одной причиной для исключения страницы из кеша считанных страниц является попадание страницы в кеш модифицированных страниц.
В процессе выполнения транзакции при внесении изменений в базу данных изменения никогда не записываются непосредственно в файл. Вместо этого они буферизуются в кеше модифицированных страниц. Страница, находящаяся в этом кеше, содержит все данные страницы, как модифицированные участки, так и оставшиеся неизменными с момента считывания. При этом ведется учет модифицированных участков, чтобы в момент выполнения физической записи в файл по возможности минимизировать объем записываемых данных.
Страница, попавшая в кеш модифицированных страниц, исключается из кеша считанных страниц.
При запросе на чтение данных из внутреннего файла соответствующая страница сначала ищется в кеше модифицированных страниц. Если не найдена, то производится поиск в кеше считанных страниц. И если не найдена там, то производится считывание страницы из файла с помещением в кеш считанных страниц.
Сброс кеша модифицированных страниц в файл производится только при выполнении фиксации (commit) транзакции. При фиксации транзакции все измененные страницы всех внутренних файлов собираются в общий массив, упорядоченный по номерам страниц в файле базы данных, и запись в файл базы данных производится от больших номеров страниц к меньшим. Это делается из следующих соображений:
Время жизни кеша модифицированных страниц ограничено временем выполнения транзакции. После завершения транзакции кеш полностью освобождается.
На размер кеша модифицированных страниц не накладывается никаких ограничений. Единственным ограничителем является размер свободной оперативной памяти.
Для обеспечения согласованности и целостности данных при разделенном режиме доступа к базе используются блокировки. Так как механика файловой базы данных работает в режиме файл-сервер, то есть отсутствует выделенный сервер баз данных, то блокировки в базе данных реализованы с использованием функций блокировки участков файла. Для блокировок используется файл с расширением .1CL.
Этот вид блокировок уже упоминался выше. Транзакционные блокировки предназначены главным образом для обеспечения логической целостности и изоляции транзакций. Транзакционные блокировки бывают двух видов:
Read-блокировки не конфликтуют между собой, но конфликтуют с Write-блокировками. Write-блокировки конфликтуют с любыми блокировками: Read и Write. Единицей блокировки является таблица. Единица довольно крупная, особенно с учетом того, что в большинстве современных СУБД поддерживаются блокировки на уровне записи. Однако реализация блокировки на уровне записи потребовала бы большого числа файловых блокировок, что привело бы к существенному снижению производительности.
Транзакционные блокировки накладываются с ожиданием. По умолчанию время ожидания транзакционной блокировки равно 20 сек.
Также имеется ряд блокировок фиксации состояния. Данный вид блокировок относится к системным блокировкам и предназначен для обеспечения согласованного доступа к файлу базы данных на физическом уровне. При использовании файловой базы данных крайне редко приходится сталкиваться с какими-либо внешними проявлениями, связанными с этим видом блокировок. В данной статье они упоминаются главным образом для полноты картины.
Поясним место этих блокировок на примере фиксации транзакции. Как было сказано выше, при фиксации результатов транзакции все изменения записываются в файл базы данных. Естественно, что пока процесс записи изменений не завершен, файл базы данных находится в рассогласованном состоянии. Соответственно, попытка чтения приведет к получению рассогласованных данных. Но записываемые данные относятся не ко всем таблицам, а только к измененным. Соответственно, нужно сделать так, чтобы никакие данные, имеющие отношение к модифицируемым таблицам, не считывались, пока запись изменений не завершена. Для обеспечения этого предусмотрена блокировка фиксации таблицы для записи и для чтения.
На время записи изменений, произведенных транзакцией, устанавливается фиксация для записи всех модифицированных транзакцией таблиц. А на время чтения данных, связанных с таблицей, устанавливается фиксация для чтения. Фиксация для записи конфликтует с фиксацией для чтения. Фиксации для чтения не конфликтуют между собой, но конфликтуют с фиксацией для записи. Соответственно, гарантируется, что, пока запись не завершена, никакие операции чтения не могут быть выполнены. А также, пока не завершено чтение, запись изменений не может быть начата.
Данный вид блокировок накладывается на очень непродолжительное время. Время ожидания захвата блокировки составляет 120 сек. Такое время ожидания выбрано из расчета, чтобы любая операция, прикрытая блокировкой фиксации состояния, успела завершиться. Исключительные ситуации с сообщениями «Не удалось зафиксировать таблицу для записи» или «Не удалось зафиксировать таблицу для чтения» крайне редки и возникают в основном в условиях сильной загрузки сети или компьютера, выполняющего функции файл-сервера.