Конфликты параллелизма: пессимистический параллелизм

33

»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ

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

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

Чтобы проверить это, создается объект TransactionScope и получается сущностный объект для заказчика LAZYK. Затем создается другой объект TransactionScope со свойством TransactionScopeOption, равным RequiresNew. При этом код ADO.NET не участвует в объемлющей транзакции, принадлежащей ранее созданному объекту TransactionScope.

После этого предпринимается попытка обновить ту же запись в базе данных с помощью ADO.NET. Но поскольку уже существует открытая транзакция, блокирующая базу данных, оператор обновления ADO.NET будет блокирован и в конечном итоге отменен по таймауту. Далее обновляется свойство ContactName сущностного объекта, вызывается метод SubmitChanges, снова запрашивается запись заказчика для вывода на консоль значения ContactName, чтобы доказать, что оно было обновлено LINQ to SQL, и транзакция завершается.

Чтобы следующий пример скомпилировался, в проект потребуется добавить ссылку на сборку System.Transactions.dll.

Код описанного примера приведен ниже:

// Используйте свое подключение
            Northwind db = new Northwind(@"Data Source=MICROSOF-1EA29E\SQLEXPRESS;
                       Initial Catalog=C:\NORTHWIND.MDF;
                       Integrated Security=True");

            using (System.Transactions.TransactionScope transaction =
                new System.Transactions.TransactionScope())
            {
                Customer cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault();

                try
                {
                    Console.WriteLine("Попытаемcя обновить ContactName с помощью ADO.NET.");
                    Console.WriteLine("  Понадобиться подождать истечения таймаута ...");
                    using (System.Transactions.TransactionScope t2 = new System.Transactions.TransactionScope(
                        System.Transactions.TransactionScopeOption.RequiresNew))
                    {
                        ExecuteStatementInDb(String.Format(@"update Customers set ContactName = 'Samuel Arthur Sanders' 
                               where CustomerID = 'LAZYK'"));
                        t2.Complete();
                    }

                    Console.WriteLine("Значение ContactName для LAZYK обновлено.\n");
                }
                catch (Exception ex)
                {
                    Console.WriteLine(
                      "\nПри попытке обновления LAZYK с помощью ADO.NET возникло исключение:\n\n  {0}\n",
                      ex.Message);
                }

                cust.ContactName = "Viola Sanders";
                db.SubmitChanges();

                cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault();
                Console.WriteLine("Contact Name: {0}", cust.ContactName);

                transaction.Complete();
            }

            // Вернуть базу данных в исходное состояние.
            ExecuteStatementInDb(String.Format(@"update Customers set ContactName = 'John Steel',
                          ContactTitle = 'Marketing Manager' where CustomerID = 'LAZYK'"));

Если при работе с любым примером, использующим объект TransactionScope, вы получаете исключение типа MSDTC on server '[сервер]\SQLEXPRESS' is unavailable (Служба MSDTC на сервере '[сервер]\SQLEXPRESS' не доступна), удостоверьтесь, что запущена служба по имени Distributed Transaction Coordinator (Распределенный координатор транзакций).

Этот код не так сложен, как может показаться на первый взгляд. Первое, что здесь делается — это создание объекта TransactionScope. Теперь используется пессимистический подход к параллелизму, предотвращающий модификацию данных кем-либо. Затем с помощью LINQ to SQL запрашивается заказчик. Далее создается другой объект TransactionScope, чтобы помешать коду ADO.NET, который будет вызываться, участвовать в транзакции исходного объекта TransactionScope.

После создания второго объекта TransactionScope предпринимается попытка обновить заказчика в базе данных с использованием ADO.NET. Код ADO.NET не получит возможности выполнить обновление из-за начальной транзакции, в результате чего будет сгенерировано исключение таймаута. Затем изменяется значение ContactName заказчика, изменение сохраняется в базе данных вызовом метода SubmitChanges, заказчик запрашивается снова и его значение ContactName выводится на консоль, чтобы доказать, что изменение сохранено. После этого исходная транзакция завершается вызовом на ней метода Complete.

Как обычно, в конце кода состояние базы данных восстанавливается. Ниже показаны результаты выполнения:

Пример пессимистического параллелизма

Обратите внимание, что при попытке обновить базу данных посредством ADO.NET генерируется исключение таймаута.

Пусть не вводит в заблуждение отложенное выполнение запроса. Помните, что многие из операций LINQ являются отложенными. В случае этого примера мой запрос LINQ to SQL вызывает операцию SingleOrDefault, так что запрос не является отложенным, что требует объявления запроса внутри контекста объекта TransactionScope.

Если бы операция SingleOrDefault не вызывалась, этот запрос мог быть объявлен перед созданием объекта TransactionScope, если действительное выполнение запроса происходило бы внутри контекста TransactionScope. Таким образом, можно было бы просто заставить запрос LINQ вернуть последовательность IEnumerable<T> до создания объекта TransactionScope и затем внутри контекста объекта TransactionScope вызвать операцию SingleOrDefault на возвращенной последовательности, возвращая единственный объект Customer, соответствующий запросу.

При таком подходе всегда необходимо оценивать, насколько много работы будет выполняться внутри контекста объекта TransactionScope, потому что на все это время база данных будет блокирована.

Альтернативный подход для средних звеньев и серверов

Существует и альтернативный подход для обработки конфликтов параллельного доступа, когда они случаются в среднем звене или на сервере. Иногда, когда возникают такие конфликты, может быть проще только создать новый объект DataContext, применить изменения и вызвать SubmitChanges.

Рассмотрим пример веб-приложения ASP.NET. Из-за характера коммуникаций между клиентом-браузером и веб-сервером, предполагающим работу без установки соединения, вы с успехом можете создавать новый объект DataContext при всякой отправке HTTP на веб-сервер, когда требуется выполнить запрос LINQ to SQL. Помните, что поскольку прочтенные из базы данных считаются немедленно устаревающими, будет не слишком хорошей идеей удерживать объект DataContext открытым в течение длительного времени, если вы намерены проводить изменения.

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

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

Если это так, и возникает конфликт параллельного доступа, не будет большого вреда от создания еще одного DataContext, повторного применения изменений и нового вызова метода SubmitChanges. И поскольку задержка между первоначальным чтением данных при обратной отправке, применением изменений и вызовом метода SubmitChanges будет очень короткой, маловероятно, что вы столкнетесь с конфликтом параллельного доступа при первой попытке, и гораздо меньше — при второй.

Если решено использовать такой подход, то при обратной отправке после конструирования нового DataContext можно извлечь необходимый сущностный объект, как было только что описано, или же поступить иначе. Вместо извлечения сущностного объекта можно создать новый сущностный объект, наполнить необходимые его свойства соответствующими значениями и присоединить их к соответствующей таблице, применив метод Attach объекта Таblе<Т>. В этой точке все будет выглядеть так, будто сущностный объект был извлечен из базы данных, несмотря на тот факт, что не каждое поле этого объекта могло быть заполнено значениями.

Прежде чем присоединять сущностный объект к Tablе<Т>, потребуется установить необходимые свойства сущностного класса в соответствующие значения. Это не значит, что для получения этих значений должна запрашиваться база данных; они могут поступать откуда угодно, например, из другого звена.

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

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

Также потребуется включить все свойства сущностного класса, участвующие в проверке обновления, для обнаружения конфликтов параллельного доступа. Если сущностный класс имеет свойство, указывающее свойство IsVersion атрибута Column со значением true, такое свойство класса должно быть установлено перед вызовом метода Attach.

Давайте в следующем примере посмотрим, как это делается:

// Используйте свое подключение
            Northwind db = new Northwind(@"Data Source=MICROSOF-1EA29E\SQLEXPRESS;
                       Initial Catalog=C:\NORTHWIND.MDF;
                       Integrated Security=True");

            // Создать сущностный объект.
            Console.WriteLine("Конструирование пустого объекта Customer.");
            Customer cust = new Customer();

            // Сначала должны быть установлены все поля, определяющие идентичность
            Console.WriteLine("Установка первичных ключей, которые будут изменяться...");
            cust.CustomerID = "LAZYK";

            // Затем установить каждое изменяемое поле
            Console.WriteLine("Установка полей, которые будут изменяться.");
            cust.ContactName = "John Steel";

            // И наконец, установить все поля, участвующие в проверке обновления
            Console.WriteLine("Установка всех полей, принимающих участие в проверке обновления...");
            cust.CompanyName = "Lazy K Kountry Store";
            cust.ContactTitle = "Marketing Manager";
            cust.Address = "12 Orchestra Terrace";
            cust.City = "Walla Walla";
            cust.Region = "WA";
            cust.PostalCode = "99362";
            cust.Country = "USA";
            cust.Phone = "(509) 555-7969";
            cust.Fax = "(509) 555-6221";

            // Присоединение к Table<T> по имени Customers
            Console.WriteLine("Присоединение к Table<T> по имени Customers...");
            db.Customers.Attach(cust);

            // В этой точке можно внести изменения и вызвать SubmitChanges().
            Console.WriteLine("Внесение изменений и вызов SubmitChanges().");
            cust.ContactName = "Vickey Rattz";
            db.SubmitChanges();

            cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault();
            Console.WriteLine("\n  ContactName в базе данных = {0}", cust.ContactName);

            Console.WriteLine("\nВосстановление изменений и вызов SubmitChanges().");
            cust.ContactName = "John Steel";
            db.SubmitChanges();

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

Затем вызывается метод Attach на Customers Table<Customer>. Далее изменения вносятся и, наконец, вызывается метод SubmitChanges. После этого из базы данных запрашивается заказчик, значение ContactName которого выводится на консоль, чтобы доказать, что оно было изменено в базе данных. И, наконец, как всегда, база данных восстанавливается в исходное состояние. Вывод кода показан ниже:

Пример использования Attach() для подключения вновь созданного сущностного объекта

Вставка и удаление объектов сущностного класса не требует такого подхода. Их можно просто вставлять или удалять перед вызовом метода SubmitChanges.

Пройди тесты
Лучший чат для C# программистов