Оптимистические уровни изоляции в MS SQL Server

30.11.17

База данных - Инструменты администратора БД

Оптимистические уровни изоляции транзакций были введены в SQL Server 2005 как новый способ борьбы с проблемами блокировок и согласованности данных. В отличие от пессимистических уровней изоляции, при использовании оптимистических уровней запросы не могут считать данные, которые были изменены другими транзакциями, но еще не были зафиксированы (читаются "старые" данные). При этом не происходит конфликта совмещаемых (S) и монопольных (X) блокировок.

Введение в управление версиями строк

Когда происходит обновление, то при использовании оптимистических уровней изоляции SQL Server сохраняет старые версии строк в специальной области базы tempdb, которое называется хранилище версий. Для исходной строки в базе данных добавляется 14-ти байтный указатель, который ссылается на старую версию этой строки (в зависимости от ситуации, может быть несколько версий). Рисунок 1 иллюстрирует это поведение.


Рисунок 1. Хранилище версий

Теперь, когда операция чтения (иногда записи) обратится к строке, для которой установлена монопольная (X) блокировка, то конфликта блокировки не произойдет, а будет считана старая версия строки из хранилища версий, как показано на рисунке 2.


Рисунок 2. Операции чтения и хранилище версий

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

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

Наконец, использование оптимистических уровней изоляции увеличивает фрагментации индексов. При изменении строки SQL Server увеличивает размер строки на 14 байт, чтобы хранить указатель на версию. Если страница полностью заполнена, и измененная строка не помещается на страницу, то страница разбивается, что приводит к фрагментации. Эти 14 байт будут занимать место, до тех пор, пока индекс не перестроится.

Совет. При использовании оптимистических уровней изоляции рекомендуется устанавливать параметр FILLFACTOR меньше 100, чтобы оставалось свободное место на страницах индекса. Это уменьшит разбиение страниц, когда добавляется указатель на версию и увеличивается размер строки.

Оптимистические уровни изоляции транзакций

Есть два оптимистических уровня изоляции транзакций: READ COMMITTED SNAPSHOT и SNAPSHOT. Если быть точнее, то SNAPSHOT это полноценный уровень изоляции, в то время как READ COMMITTED SNAPSHOT это параметр базы даных, который влияет на поведение операций чтения с уровнем изоляции READ COMMITTED.

Рассмотрим эти уровнее подробнее.

Уровень изоляции READ COMMITTED SNAPSHOT

Оба оптимистических уровня изоляции включаются на уровне базы данных. READ COMMITTED SNAPSHOT (RCSI) включается командой ALTER DATABASE SET READ_COMMITTED_SNAPSHOT ON.

Примечание. Изменение этого параметра требует монопольного доступа к базе. Команда не выполнится, если есть другие подключения к базе. Вы можете переключить базу данных в однопользовательский режим или использовать команду ALTER DATABASE SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK AFTER X SECONDS. При этом откатятся все активные транзакции и завершаться все подключения к базе.

Как уже говорилось, RCSI изменяет поведение операций чтения с уровнем изоляции READ COMMITTED. Но при этом не влияет на операции записи.

Как видно из рисунка 3, вместо установки совмещаемой (S) блокировки, которая привела бы к конфликту с монопольной (X) блокировкой, происходит чтение старой версии данных из хранилища версий. Операции записи устанавливают монопольные (X) блокировки и блокировки обновления (U) в пессиместических уровнях изоляции, поэтому они могут блокировать друг друга. Но при этом они уже не блокируют операции чтения, как это происходит с уровнем изоляции READ UNCOMMITTED.


Рисунок 3. Поведение уровня изоляции Read Committed Snapshot

Однако, существует большая разница между уровнями изоляции READ UNCOMMITTED и READ COMMITTED SNAPSHOT. Уровень изоляции READ UNCOMMITTED не устанавливает совмещаемые (S) блокировки, при этом возможны разные проблемы несогласованности данных (грязное чтение, неповторяющееся чтение и др.). С другой стороны, уровень изоляции READ COMMITTED SNAPSHOT дает нам полную согласованность данных на уровне команды (операции). В этом случае невозможно чтение незафиксированных данных, а также данных, которые изменялись с момента начала транзакции.

Совет. Переключение базы данных в режим использования уровня изоляции READ COMMITTED SNAPSHOT может быть экстренным выходом из ситуации, когда в системе много проблем с блокировками данных. Это позволит избежать блокировок для операций чтения/записи, если чтение происходит с уровнем изоляции READ COMMITTED. Но вы должны понимать, что лишь временная мера. Необходимо обнаружить и устранить причину возникновения таких блокировок.

Уровень изоляции SNAPSHOT

SNAPSHOT является отдельным уровнем изоляции. Он должен быть явно задан в коде с помощью команды SET TRANSACTION ISOLATION LEVEL SNAPSHOT или с помощью табличного указания WITH (SNAPSHOT).

По умолчанию, использование уровня изоляции SNAPSHOT запрещено. Его необходимо включить с помощью команды ALTER DATABASE SET ALLOW_SNAPSHOT_ISOLATION ON. Эта команда не требует монопольного доступа к базе и ее можно выполнять когда есть активные пользователи.

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

В примере, показанном на рисунке 4, Сессия 1 начинает транзакцию и читает строку в момент времени T1. В момент времени T2 Сессия 2 изменяет строку в неявной транзакции. При этом старая (оригинальная) версия строки помещается во временное хранилище базы tempdb.


Рисунок 4. Уровень изоляции Snapshot и операции чтения

Следующим этапом появляется Сессия 3, которая начинает еще одну транзакцию и читает ту же строку в момент T3. Она читает версию строки зафиксированную Сессией 2 (в момент времени T2). Сессия 4 снова меняет строку в неявной транзакции в момент времени T4. В итоге в хранилище окажутся две версии строки - одна, которая была записана между моментами T2 и T4, и другая, что была записана до момента T2. Теперь, если Сессия 3 опять прочитает те же самые данные, то будет считана версия строки из хранилища, которая была записана между моментами T2 и T4, т.к. эта версия была создана при начале транзакции Сессии 3. Аналогично, Сессия 1 будет использовать версию строки, которая существовала до момента T2. После фиксации транзакций в Сессии 1 и Сессии 3 хранилище версий запустит задачу по удалению обеих версий строк, конечно, если эти данные не нужны другим транзакциям.

Уровень изоляции SNAPSHOT предоставляет аналогичную согласованность данных, что и уровень SERIALIZABLE, но без установки блокировок, хотя может генерировать огромное количество данных в базе tempdb. Если у вас есть сессия, которая удаляет миллионы строк из таблиц, то все они будут скопированы в хранилище версий. Даже если сама инструкция выполняется с пессиместическим уровнем изоляции. Данные будут сохраняться для возможных транзакций, использующий уровень изоляции snapshot  или RCSI.

Теперь рассмотри поведение операций записи.Предположим, что Сессия 1 начинает транзакцию и обновляет одну из строк. Эта сессия удерживает монопольную (X) блокировку, как показано на рисунке 5.


Рисунок 5. Уровень изоляции snapshot и операции записи (1)

Сессия 2 хочет обновить все строки, у которых колонка Cancelled = 1. Она начинает сканирование таблицы, и когда должны быть считаться данные с orderid = 10, то читаются строки из хранилища версий, то есть последней зафиксированной версии до  начала транзакции Сессии 2. Это оригинальная версия строки (не обновленная), у нее значение колонки Cancelled = 0, поэтому Сессия 2 не обновляет ее. Сессия 2 продолжает сканирование таблицы, не устанавливая на эту строку блокировки обновления (U) и монопольные (X) блокировки.

Аналогично, Сессия 3 хочет обновить все строки, у которых Amount = 29.95. Когда она читает версию строки из хранилища версий, то определяет, что строка должна быть обновлена. При этом неважно, что Сессия 1 в это время меняет колонку Amount. "Новая версия" строки еще не зафиксирована, и ее не видят другие сессии. Сессия 3 хочет обновить строку в базе данных, пытается установить монопольную (X) блокировку, но получает отказ, потому что Сессия 1 уже установила на этой строке аналогичную блокировку.

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

В примере, показанном на рисунке 6, Сессия 1 начинает транзакцию и меняет одну из строк. Следующим шагом Сессия 2 начинает другую транзакцию. На самом деле, не так уж и важно, что сессия начинается раньше транзакции, при условии, что новая версия строки с OrderId = 10 не фиксируется.


Рисунок 6. Уровень изоляции snapshot и операции записи (2)

В любом случае Сессия 1 завершит транзакцию. В этот момент монопольная (X) блокировка строки снимается. Если Сессия 2 попытается прочитать эту строку, то по прежнему будет получать версию из хранилища версий, потому что это последняя зафиксированная версия при начале транзакции Сессией 2. Но если транзакция попытается изменить эту версию, то получит ошибку 3960 и транзакция откатится. Пример ошибки показан на рисунке 7.


Рисунок 7. Ошибка 3960

Совет. Ошибку 3960 можно обрабатывать с помощью операторов TRY/CATCH.

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

Листинг 1. Предотвращение ошибки 3960 с помощью табличного указания READCOMMITTED

set transaction isolation level snapshot
	begin tran
	select count(*) from Delivery.Drivers
	update Delivery.Orders with (readcommitted)
	set Cancelled = 1
	where OrderId = 10
rollback

Уровень изоляции SNAPSHOT  может изменить поведение системы. Предположим, мы имеем таблицу dbo.Colors, в которой две строки: Black и White. Код создания таблицы приведен в листинге 2.

Листинг 2. Поведение операции обновления с уровнем изоляции SNAPSHOT: Создание таблицы

create table dbo.Colors
(
	Id int not null,
	Color char(5) not null
)
go
insert into dbo.Colors(Id, Color) values(1,'Black'),(2,'White')

Теперь запустим две сессии одновременно. В первой сессии мы запустим обновление, которое установит в колонке Color цвет White для строк, в которых текущее значение равно Black. Код показан в листинге 3.

Листинг 3. Поведение операции обновления с уровнем изоляции SNAPSHOT: Код Сессии 1

begin tran
	update dbo.Colors
	set Color = 'White'
	where Color = 'Black'
commit

Во второй сессии, выполним противоположные операции, как показано в листинге 4.

Листинг 4. Поведение операции обновления с уровнем изоляции SNAPSHOT: Код Сессии 2

begin tran
	update dbo.Colors
	set Color = 'Black'
	where Color = 'White'
commit

Давайте запустим обе сессии одновременно с уровнем изоляции READ COMMITTED или любым другим пессиместическим уровнем. На первом шаге, как показано на рисунке 8, у нас есть конкуренция за ресурс. Одна из сессий установит монопольную (X) блокировку на строку и обновит ее. В тоже время другая сессия не сможет установить блокировку обновления (U) на эту же строку.


Рисунок 8. Пессимистические уровни изоляции: Шаг 1

Когда первая сессия зафиксирует транзакцию, то снимет монопольную (X) блокировку. В таблице окажутся две записи с одинаковым значением в колонке Color, потому что первая сессия уже произвела изменения (как показано на рисунке 9). В итоге в таблице всегда окажутся две одинаковые строки (Black или White), в зависимости от того, какая сессия первой успеет установить блокировку.


Рисунок 9. Пессимистические уровни изоляции: Шаг 2

С уровнем изоляции snapshot все работает немного по-другому (рисунок 10). Когда сессия обновит строку, то она помещает старую версию в хранилище версий. Вторая сессия при этом будет считывать строки из хранилища. В результате цвета поменяются местами.


Рисунок 10. Уровень изоляции snapshot

Вы должны быть в курсе, как работают уровни изоляции RSCI и SNASPSHOT, особенно если имеется код, который работает с блокировками. Например, имеется триггер, реализующий ссылочную целостность. Это может быть триггер ON DELETE, удаляющий данные из связанных таблиц. Этот триггер использует операцию select, чтобы проверить, имеются ли строки, связанные с удаляемой строкой. Используя оптимистические уровни изоляции можно пропустить строки, которые были изменены после начала транзакции. В этом случае правильнее использовать пессиместические уровни изоляции, например, READCOMMITTED.

Примечание. SQL Server использует уровень изоляции READ COMMITTED при проверке ограничения внешнего ключа. Это означает, что вы можете получить блокировку между операциями записи и чтения даже с оптимистическими уровнями изоляции, особенно если нет индексов ссылающихся столбцов, что приводит к сканированию таблицы.

Хранилище версий

Как уже говорилось ранее, необходимо следить за тем, как оптимистические уровни изоляции влияют на систему. Для примера, выполните следующий код, который удаляет все строки из таблицы Delivery.Orders (листинг 5).

Листинг 5. Удаление всех заказов из таблицы

set transaction isolation level read uncommitted
begin tran
	delete from Delivery.Orders
commit

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

На рисунке 11 показано свободное место tempdb и размер хранилища версий. Видно, что после начала операции удаления размер хранилища версий растет, занимая все пространство tempdb.


Рисунок 11. Свободное место tempdb и размер хранилища версий

На рисунке 12 можно увидеть показатели формирования записей в хранилище версий и его очистки.

По умолчанию задача очистки выполняется раз в минуту, а также перед автоматическим увеличением размера базы tempdb.


Рисунок 12. Свободное место tempdb и размер хранилища версий

Есть еще 3 счетчика производительности, связанных с оптимистическими уровнями изоляции:

  1. Snapshot Transactions. Счетчик показывает количество активных транзакций, использующих уровень изоляции snapshot.
  2. Update Conflict Ratio. Показывает соотношение количества конфликтов обновления к общему количеству операций обновления с уровней изоляции snapshot.
  3. Longest Transaction Running Time. Показывает длительность в секундах самой старой активной транзакции, использующей версии строк.

Существует несколько динамических представлений (Dynamic Management Views - DMVs), которые могут быть полезны при решении вопросов, связанных с хранилищем версий и транзакций в целом. Подробнее на http://technet.microsoft.com/en-us/library/ms178621.aspx (Transaction Related Dynamic Management Views and Functions section).

Резюме

SQL Server в оптимистических уровнях изоляции использует версионирование строк. Транзакции не блокируются из-за несовместимости разделяемых (S) блокировок с блокировками обновления (U) и монопольными (X) блокировками, а используют "старые", зафиксированные ранее версии строк. Существуют два оптимистических уровня изоляции транзакции: READ COMMITTED SNAPSHOT и SNAPSHOT.

READ COMMITTED SNAPSHOT - это параметр базы данных, который влияет на поведение операций чтения, использующих режим READ COMMITTED. При этом он не влияет на операции записи - по прежнему сохраняются несовместимости блокировок (U)/(U) и (U)/(X). READ COMMITTED SNAPSHOT не требует изменений в код и может быть использован как "волшебная палочка", если в системе наблюдаются конфликты блокировок.

READ COMMITTED SNAPSHOT обеспечивает согласованность данных на момент выполнения операции, т.е. запрос читает данные, которые были зафиксированы на момент, когда запрос начался.

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

При использовании уровня изоляции SNAPSHOT операции записи не блокируют друг друга, за исключением тех случаев, когда они меняют одни и те же строки. Это приводит либо к блокировке, либо к ошибке 3960.

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

--------------------------------
При написании статьи использовались материалы из книги Дмитрия Короткевича «Pro SQL Server Internals» (2014 г.)

MS SQL Server блокировки уровни изоляции

См. также

Автоподбор ролей для профилей и групп доступа в любых типовых базах 1С УТ 11, КА 2, ERP2, Розница 2/3, УНФ 16/3, БП 3, ЗУП 3 и подобных (УФ, Платформа 8.3.14+)

Инструменты администратора БД Роли и права 8.3.14 1С:Розница 2 1С:Управление нашей фирмой 1.6 1С:Документооборот 1С:Зарплата и кадры государственного учреждения 3 1С:Бухгалтерия 3.0 1С:Управление торговлей 11 1С:Комплексная автоматизация 2.х 1С:Зарплата и Управление Персоналом 3.x 1С:Управление нашей фирмой 3.0 1С:Розница 3.0 Платные (руб)

Роли… Вы тратите много времени и сил на подбор ролей среди около 2400 в ERP или 1500 в Рознице 2, пытаясь понять какими правами они обладают? Вы все время смотрите права в конфигураторе или отчетах чтоб создать нормальные профили доступа? Вы хотите наглядно видеть какие права дает профиль и редактировать все в простом виде? А может хотите просто указать подсистему и дать права на просмотр и добавление на объекты и не лезть в дебри прав и чтоб обработка сама подобрала нужные роли? Все это теперь стало возможно! Обновление от 15.12.2023, версия 1.1.

12000 руб.

06.12.2023    2976    13    1    

34

SALE! 20%

Infostart УДиФ: Управление данными и формами

Инструменты администратора БД Инструментарий разработчика Роли и права Платформа 1С v8.3 Конфигурации 1cv8 Россия Платные (руб)

Расширение позволяет без изменения кода конфигурации выполнять проверки при вводе данных, скрывать от пользователя недоступные ему данные, выполнять код в обработчиках. Не изменяет данные конфигурации, легко устанавливается практически на любую конфигурацию на управляемых формах.

10000 8000 руб.

10.11.2023    3531    11    1    

34

SALE! 30%

PowerTools

Инструментарий разработчика Инструменты администратора БД Платформа 1С v8.3 Управляемые формы Конфигурации 1cv8 Россия Платные (руб)

Универсальный инструмент программиста для администрирования конфигураций. Сборник наиболее часто используемых обработок под единым интерфейсом.

3600 2520 руб.

14.01.2013    177744    1073    0    

849

Ускоренное проведение документов (x4), устранение ошибок 60/62 счетов и зачет авансов (Бухгалтерия 3.0)

Закрытие периода Инструменты администратора БД Корректировка данных Бухгалтерский учет 1С:Бухгалтерия 3.0 Россия Бухгалтерский учет Платные (руб)

Расширение «Оперативное проведение» в 4 раза уменьшает время проведения документов и закрытия месяца. Является комплексным решением проблем 62 и 60 счетов. Оптимизирует проведение при включенной функциональной опции «Раздельный учет НДС». Используется в более 10 организациях уже 2 года. Совместимо с конфигурацией Бухгалтерия 3.0 (+КОРП).

14400 руб.

29.04.2020    27378    79    146    

59

Система хранения присоединенных файлов в томах на диске

Инструменты администратора БД Платформа 1С v8.3 1С:Комплексная автоматизация 1.х 1С:Управление производственным предприятием Платные (руб)

Конфигурация Комплексная автоматизация 1.1 (и УПП 1.3 тоже) хранит файлы и изображения в справочнике Хранилище дополнительной информации в реквизите Хранилище типа ХранилищеЗначений. Та же история с ВложениямиЭлектроннойПочты. Но при этом присоединенные файлы в Электронном документообороте хранит в томах на диске. Эта доработка позволяет использовать стандартный механизм хранения файлов, изображений и вложений электронных писем в томах на диске. При этом можно разделить тома хранения по объектам конфигурации.

4200 руб.

10.11.2015    61317    88    59    

73

"Менеджер потоков 2.1": УПП: "Восстановление партий"

Инструменты администратора БД Платформа 1С v8.3 1С:Управление производственным предприятием Россия Бухгалтерский учет Управленческий учет Платные (руб)

Как оптимизировать то, что, считалось, не поддается оптимизации? Как повысить доступность базы данных? Как проводить самую «времяемкую» операцию не по паре раз в неделю, а по несколько раз в день*? Ответ есть!

20000 руб.

12.09.2019    11746    5    9    

7

Брандмауэр для сервера 1С Предприятие 8 - внешнее управление сеансами

Инструменты администратора БД Платформа 1С v8.3 Конфигурации 1cv8 Платные (руб)

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

3600 руб.

06.02.2017    31111    31    18    

47

Хранилище файлов на SQL

Инструменты администратора БД Платформа 1С v8.3 Управляемые формы Конфигурации 1cv8 Управленческий учет Платные (руб)

Привязка файлов / сканов к объектам 1С с сохранением их на SQL-сервере

12000 руб.

09.10.2019    10984    5    8    

9
Комментарии
В избранное Подписаться на ответы Сортировка: Древо развёрнутое
Свернуть все
1. Darklight 32 01.12.17 12:14 Сейчас в теме
Хороший цикл статей, в конце статьи не хватает ссылок на другие статьи цикла.
И хорошо бы раскрыть аналогичные темы (цикла статей), но для PostrgreSQL
pragmatic; +1 Ответить
2. Dach 372 04.12.19 11:33 Сейчас в теме
"При использовании уровня изоляции SNAPSHOT операции записи не блокируют друг друга, за исключением тех случаев, когда они меняют одни и те же строки. Это приводит либо к блокировке, либо к ошибке 3960"

Странная фраза. Непересекающиеся наборы строк и так друг друга не блокируют.

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

Не поленился и провел эксперимент. Включил в своей базе снапшот. Также у меня в базе включен RCSI. Затем в первом сеансе в транзакции записываю большой набор строк (600 тыс). Это привело к наложению Х-блокировки на всю таблицу (у меня периодический регистр сведений, цены номенклатуры). Во втором сеансе пытаюсь записать другой набор строк с другим периодом, то есть первый и второй наборы точно не пересекаются. Транзакция второго сеанса отваливается с ошибкой ожидания на транзакционной блокировке. Точно также, как это происходит в чистом RCSI.

Таким образом, непонятно, что Вы имеет ввиду, когда говорите что в снапшот "записи друг друга не блокируют".

Расшифруйте подробнее, плз. Какой натурный тест можно выполнить, чтобы воспроизвести ваши слова?

Кстати, с точки зрения чтения, при включении снапшота дополнительно к RCSI - ничего не поменялось. Запрос внутри транзакции по прежнему получает данные на момент выполнения самого запроса, а не на момент начала транзакции, как в "чистом" снапшот.
3. jan27 732 19.10.22 14:54 Сейчас в теме
должен отметить, что использование хинта with (snapshot) работает только для таблиц, оптимизированных для памяти
Оставьте свое сообщение